Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
House of Einherjar is the technique that turns a *single null byte* into chunk overlap. It is the canonical exploitation of the ‘poison null byte’ — an off-by-one overflow that writes one 0x00 past the end of a heap buffer, landing on the least-significant byte of the *next* chunk’s size field. That one zeroed byte clears the neighbouring chunk’s PREV_INUSE flag, making glibc believe the chunk *before* it is free. When that chunk is later freed, the allocator performs backward consolidation: it reads a prev_size field the attacker controls and merges backward to a chunk located prev_size bytes earlier — an address the attacker chose. The result is a large free chunk that overlaps memory still in use, the foundation for arbitrary read/write.
Einherjar (published by Hiroki Matsukuma) is prized because its trigger is so small: many real overflows are exactly one byte, often a stray null terminator, and this technique is how that one byte becomes a full compromise. It applies broadly across glibc versions, but it is *constrained* rather than killed by a specific hardening step: since glibc 2.26/2.27 the consolidation path enforces that a chunk’s stored prev_size equals the actual size of the chunk being consolidated (the ‘corrupted size vs. prev_size’ check). Einherjar survives that check — but only by forging a fake chunk whose size field is consistent with the prev_size it plants, which is why the modern form requires a heap leak.
Attack Prerequisites
Einherjar is a precise technique: it needs the off-by-one, control over a prev_size, and enough knowledge of the heap to point the backward merge at a fake chunk. Typical requirements:
- A single-null-byte overflow into the next chunk’s size field (the poison null byte) — enough to clear that chunk’s
PREV_INUSEbit. - Control of a
prev_sizefield in front of the victim chunk, so the allocator computes the backward-merge target asvictim - prev_size. - A fake chunk at the merge target whose size field is forged to satisfy the consolidation check (
chunksize(fake) == prev_size). - A heap address leak on modern glibc, because the fake chunk’s location and the
prev_sizedistance must be computed relative to the real heap layout. - A free of the chunk whose
PREV_INUSEwas cleared, which is what actually triggers the backward consolidation.
How It Works
glibc marks whether the previous adjacent chunk is in use with the PREV_INUSE bit — the lowest bit of a chunk’s size field. When a chunk is freed and its predecessor is *not* in use (bit clear), _int_free consolidates backward: it reads the prev_size stored at the top of the chunk being freed, steps back to p - prev_size to find the predecessor, unlinks that predecessor from its bin, and merges the two into one larger free chunk. Every part of that sequence trusts fields that a one-byte overflow and a controlled prev_size can forge.
The poison null byte supplies the first lie. Overflowing one 0x00 into the next chunk’s size clears its PREV_INUSE bit, so glibc now believes the chunk in front of it (our victim) is free and available for consolidation. The attacker supplies the second lie by writing a chosen prev_size so that p - prev_size points not at the real previous chunk but at a fake chunk the attacker has laid out — commonly earlier in the same heap, or in a controlled global. When the null-poisoned chunk is freed, the allocator computes the merge target from the forged prev_size, unlinks the fake chunk, and produces one giant free chunk that begins at the fake chunk and extends over everything up to the freed chunk. That giant chunk overlaps allocations that are still live: reallocating it hands the attacker a pointer aliasing in-use objects.
Modern glibc adds friction. Since roughly 2.26/2.27 the consolidation path checks chunksize(P) == prev_size(next_chunk(P)) and, on backward consolidation, that the predecessor’s size matches the prev_size used to reach it — aborting with “corrupted size vs. prev_size” otherwise. Einherjar defeats this not by avoiding the check but by satisfying it: the fake chunk’s size field is forged to equal the prev_size distance, so the invariant holds even though the whole layout is fabricated. The older unlink safe-unlinking checks (fd->bk == p && bk->fd == p) similarly force the fake chunk’s fd/bk to be self-consistent, typically by pointing them at a known location (a technique lifted from the classic unlink attack). All of this arithmetic is why the modern technique needs a heap leak: the distances and the fake chunk’s self-referential pointers must be computed from real addresses.
Vulnerable Code / Setup
The enabling bug is an off-by-one that writes a terminating null one byte past a heap buffer — an extremely common mistake in hand-rolled string handling. The overflow lands on the low byte of the adjacent chunk’s size:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Classic poison-null-byte: writes strlen(src) bytes plus a NUL, but the
buffer is exactly the chunk's usable size, so the NUL spills one byte
into the next chunk's size field, clearing its PREV_INUSE bit. */
void copy_into(char *dst, const char *src) {
size_t n = strlen(src);
memcpy(dst, src, n);
dst[n] = '\0'; /* BUG: off-by-one when n == usable size */
}
int main(void) {
char *a = malloc(0xf8); /* usable 0xf8; header of b sits at a+0xf8 */
char *b = malloc(0xf8);
/* an 0xf8-byte src leaves the NUL to clear b's PREV_INUSE bit */
copy_into(a, attacker_string /* 0xf8 non-null bytes */);
/* attacker also plants prev_size in a[] and a fake chunk earlier;
freeing b now consolidates backward onto the fake chunk. */
free(b);
}
CTwo forged fields make the consolidation land where the attacker wants: a prev_size written into the victim so b - prev_size reaches the fake chunk, and the fake chunk’s own size forged to match that prev_size (to pass the size-vs-prev_size check). Triage confirms the technique is reachable and whether a leak is required:
$ checksec --file=./einherjar
RELRO STACK CANARY NX PIE
Full RELRO Canary found NX enab. PIE <-- heap leak needed
$ ldd ./einherjar | grep libc # note version: affects which checks apply
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.31)
BashWalkthrough / Exploitation
Everything hinges on the heap layout and the two forged fields, so drive it in a debugger first. In gdb with pwndbg, confirm the cleared PREV_INUSE bit and watch the backward merge produce an oversized free chunk:
gdb -q ./einherjar
pwndbg> run
pwndbg> vis_heap_chunks # locate a, b, and the fake chunk you planted
pwndbg> # after the off-by-one, inspect b's size: low byte should be 0x00
pwndbg> x/2gx <b_header> # confirm PREV_INUSE (bit 0) is cleared
pwndbg> # after free(b): a single large free chunk starting at the fake chunk
pwndbg> bins # the merged chunk lands in the unsorted bin
BashThe exploit sequence: leak a heap address; lay out a fake chunk whose size matches the prev_size you will plant; write that prev_size into the victim chunk; trigger the one-byte overflow to clear PREV_INUSE; free the victim to consolidate backward; then reclaim the oversized chunk to obtain a pointer that overlaps live objects. A pwntools skeleton:
from pwn import *
io = process('./einherjar')
def alloc(sz, data=b''): ... # menu wrappers
def free(idx): ...
def edit(idx, data): ...
heap = leak_heap() # required on modern glibc
fake = heap + 0x10 # where we forge the merge-target chunk
# 1) forge fake chunk: size == the prev_size distance, self-consistent fd/bk
edit(0, p64(0) + p64(0x100) + p64(fake) + p64(fake))
# 2) plant prev_size in the victim so (victim - prev_size) == fake
prev_size = (victim_addr - fake)
edit(1, b'A'*0xf0 + p64(prev_size))
# 3) off-by-one: clear the next chunk's PREV_INUSE via the poison null byte
trigger_off_by_one()
# 4) free -> backward consolidation onto 'fake' -> one big overlapping chunk
free(2)
# 5) reclaim the oversized chunk; it now overlaps a live object
victim = alloc(0x120) # aliases an in-use chunk -> UAF-like R/W
PythonFrom the overlap, escalation is standard: the reclaimed chunk aliases a live object, so you can overwrite its fields (a function pointer, a size, another chunk pointer) or set up a follow-on tcache/fastbin poison for an arbitrary write. Because the consolidated chunk enters the unsorted bin, Einherjar also pairs naturally with a libc leak (an unsorted-bin chunk’s fd/bk point into main_arena) before pivoting to a hook or GOT overwrite on versions where those targets exist.
Note: The alignment of the poison null byte is everything. The technique needs the victim’s usable size to end exactly on the next chunk’s size-field boundary so the single NUL clears
PREV_INUSEand nothing else — sizes like0xf8/0x108(whose usable size ends on the0x??00boundary) are the classic choices. If the overflow zeroes more than the intended byte, or the neighbouring size does not end in a low byte you can safely null, the layout won’t cooperate. Verify in gdb that only bit 0 of the next size changed.
Opsec: On glibc >= 2.26/2.27 the ‘corrupted size vs. prev_size’ check means the fake chunk’s size must equal the planted
prev_sizeexactly, and the safe-unlinking checks force self-consistentfd/bk— get either wrong andfreeaborts loudly. Because all of these are computed from real heap addresses, a stale or wrong heap leak is the usual cause of a crash rather than a shell. Re-leak and recompute if the layout shifts between runs.
Detection and Defense
Einherjar is powered by an off-by-one and by trust in in-band consolidation metadata, so defenses target both the overflow and the predictability of the heap:
- Kill the off-by-one — bounds-correct string handling (account for the NUL terminator),
_FORTIFY_SOURCE, and compiler__builtin_object_sizechecks stop the poison null byte at its source. - Keep glibc current — the ‘corrupted size vs. prev_size’ consolidation check and safe-unlinking raise the bar; while Einherjar can satisfy them, they force a heap leak and precise forgery that older libc did not.
- PIE + ASLR — randomising the heap denies the attacker the addresses needed to place the fake chunk and compute
prev_size, forcing an extra leak. - AddressSanitizer in CI — ASan detects the one-byte heap overflow deterministically (“heap-buffer-overflow”), catching the root cause during testing rather than in production.
- Hardened allocators — allocators with out-of-line metadata and guard regions between chunks deny the adjacency and in-band
prev_size/PREV_INUSEfields the technique manipulates.
Real-World Impact
The poison null byte is one of the most storied heap bug classes: a single stray terminator is a mistake that appears throughout real C code, and Einherjar (together with the related ‘shrink free chunk’ null-byte techniques) is the standard way researchers convert it into control. Off-by-one and single-byte heap overflows have driven notable memory-corruption CVEs across parsers, servers, and libraries, and CWE-193 (off-by-one) together with CWE-122 (heap overflow) remain perennial entries in weakness rankings. In CTF pwn and allocator research, House of Einherjar is the reference technique for the case where the overflow you have is exactly, frustratingly, one byte — and it is the clearest demonstration that a single null byte is never ‘just’ a null byte when it lands on allocator metadata.
Conclusion
House of Einherjar promotes a one-byte off-by-one into full chunk overlap by abusing backward consolidation: the poison null byte clears a neighbour’s PREV_INUSE bit, a forged prev_size redirects the backward merge to an attacker-placed fake chunk, and freeing the poisoned chunk yields a giant free chunk overlapping live memory. Modern glibc constrains but does not kill it — the size-vs-prev_size and safe-unlinking checks force a precise, leak-dependent forgery rather than an abort. It endures because its trigger is the smallest possible overflow, and it is the definitive reminder that on the heap, one byte in the wrong place is more than enough.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments