algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

the House of Einherjar

2026-06-143 min readheap pwn glibc

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.

target./einherjar
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.

what the off-by-one does to Bdiff
- B.size = 0x...1     # PREV_INUSE set: prev chunk in use, no backward merge
+ B.size = 0x...0     # one null byte: PREV_INUSE clear -> "prev chunk is free"

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.

fake chunk: size matches prev_size, fd/bk survive unlinkhex
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.

einherjar.pypython
from pwn import *
io = process("./einherjar"); 
heap = leak_heap()                          # Einherjar needs to know where `fake` is

fake = heap + 0x10                          # craft a fake chunk in a controlled buffer
forge_fake_chunk(fake, size=0x120, fd=fake-0x18, bk=fake-0x10)
overflow_prev_size(B, value = B_addr - fake)    # prev_size = distance back to fake
off_by_one_null(A)                          # clear B's PREV_INUSE
free(B)                                     # backward consolidate -> fake in unsorted bin
p = alloc(0x110)                            # p == fake
the payoff

Put fake over a freed tcache chunk's neighbourhood and the reclaimed chunk overlaps it — edit its fd, poison, win:

txt
segfault{0n3_byt3_3v3rywh3r3}

limitations#

  • you need a heap leakprev_size is a distance, and the distance depends on ASLR;
  • the fake chunk must pass unlink (the self-referencing fd/bk);
  • glibc 2.29's corrupted size vs. prev_size check (from the poison null byte) constrains the modern version — the fake chunk's size must equal the prev_size you 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.