The poison null byte used an off-by-one to overlap two
adjacent chunks. The House of Einherjar takes the same null byte and aims it across
the whole address space: clear a chunk's PREV_INUSE, lie about how far back the
"previous chunk" starts, and free will consolidate backward into a fake chunk
you control — wherever you put it.
- arch
- amd64-64-little
- libc
- 2.27
- technique
- off-by-one NULL + forged prev_size -> backward consolidate into a fake chunk
- needs
- a heap leak (to measure the distance to your fake chunk)
the lever: prev_size + a cleared PREV_INUSE#
When free(B) runs and B's PREV_INUSE is clear, glibc believes the chunk
before B is free and consolidates backward. It finds that previous chunk by
subtracting B's prev_size from B's address — prev_chunk = &B - B.prev_size.
Both inputs are attacker-reachable. The single null byte from an off-by-one clears
PREV_INUSE; and prev_size lives in B's first quadword, which a heap overflow
(or B's own contents, if it's the chunk before B you control) can set. Set
B.prev_size = &B - &fake, and the consolidation reaches all the way to fake.
the fake chunk#
fake is a chunk header you place in any writable region you can leak the address
of — another heap buffer, a global. It has to survive the backward
consolidation's unlink, so it uses the same self-referencing trick as unsafe
unlink: point its fd/bk at a location that points back at
fake, or at itself.
00000000 00 00 00 00 00 00 00 00 21 01 00 00 00 00 00 00 |........!.......| 00000010 b0 c0 60 55 55 55 00 00 b0 c0 60 55 55 55 00 00 |..`UUU....`UUU..|
When free(B) fires, glibc unlinks fake, merges [fake … end-of-B] into one big
free chunk, and drops it in the unsorted bin. The very next allocation of that size
returns fake — an arbitrary chunk, sitting on top of whatever you targeted.
|
the payoff
Put fake over a freed tcache chunk's neighbourhood and the reclaimed chunk
overlaps it — edit its fd, poison, win:
segfault{0n3_byt3_3v3rywh3r3}
limitations#
- you need a heap leak —
prev_sizeis a distance, and the distance depends on ASLR; - the fake chunk must pass
unlink(the self-referencingfd/bk); - glibc 2.29's
corrupted size vs. prev_sizecheck (from the poison null byte) constrains the modern version — the fake chunk'ssizemust equal theprev_sizeyou forged.
Einherjar is the off-by-one's long-range cousin: same null byte, but the blast radius is the whole address space. Next, the doubly-linked smallbins give the same "allocate anywhere" power without the null byte — the House of Lore.