Tcache poisoning to the stack on a forking heap service
noted is an x86-64 Linux service on Ubuntu 22.04 (glibc 2.35), started by systemd with Restart=always. The parent binds the socket and forks a child per connection; it never re-execs and never re-randomizes. checksec reports Full RELRO, PIE, NX and stack canary. The menu offers new/show/edit/free with 16 slots and sizes 0x18-0x400; free() does not clear the slot pointer, edit() still accepts a freed index, and show() writes up to 0x20 bytes of a slot. new() prints "allocated at %p" for each chunk. A 0x410 chunk, freed after the 0x410 tcache bin was filled, now reads 0x00007f3c1e219ce0 and 0x00007f3c1e21ac80 in its first 16 bytes, and you have confirmed these are libc+0x219ce0 and libc+0x21ac80 for the shipped libc. You also hold a heap leak of 0x5555555596b0 from a fresh 0x30 chunk. There is no compiler on the host and no writable-exec mapping outside the heap. Goal: a shell.
1. Compute the exact 8 bytes you write into the freed chunk's fd to make the next-next malloc land on a libc pointer that gives you a stack address, and outline the rest of the chain.
Read the worked answer
Derive libc base first. The unsorted-bin fd leaks main_arena, not the base:
0x00007f3c1e219ce0 - 0x219ce0 = 0x7f3c1e000000.- Sanity check with the second pointer:
0x00007f3c1e21ac80 - 0x21ac80 = 0x7f3c1e000000. Same base, so the build matches.
Choose the target. environ at libc+0x221200 = 0x7f3c1e221200. Reading it gives a real stack address on the child that reads it.
Apply safe-linking. glibc 2.35 stores fd = next ^ (pos >> 12), where pos is the user-data address of the chunk holding the pointer:
pos >> 12 = 0x5555555596b0 >> 12 = 0x555555559.0x7f3c1e221200 ^ 0x0000000555555559 = 0x7a694b774759.- Little-endian on the wire:
59 47 77 4b 69 7a 00 00.
Chain. Poison, then two news of the same size: the first returns 0x5555555596b0, the second returns 0x7f3c1e221200. show on that slot prints the leaked stack pointer. Poison again toward stack - offset_to_saved_rip, choosing an address that satisfies aligned_OK (16-byte aligned) - if the saved RIP slot is not aligned, aim at the aligned address below and pad the ROP. Write pop rdi; ret / pointer to /bin/sh / ret (stack alignment) / system. Note 2.35 has no __free_hook, which is why the stack is the target and not a hook.
2. Why does the fork-per-connection parent make this reliable, and what would break if it re-exec'd per connection? Also: how do you confirm the offset to the saved RIP without the same libc version issues?
Read the worked answer
Why fork helps. The parent's mappings, the master canary, and the stack layout are inherited verbatim by every child. Two consequences:
environ-> saved RIP is a constant offset, measured once locally under the same binary and libc.- You can split the attack: connection A leaks libc and stack, connection B reuses them, because ASLR does not re-run between forks. That is what makes a two-stage approach possible on a service where each connection looks one-shot.
If it re-exec'd, each connection would get fresh ASLR: the stack pointer leaked in connection A would be meaningless in connection B, the canary would be new every time, and you would be forced into a single-connection chain or a brute force that PIE plus canary makes infeasible.
Confirming the offset without trusting a version guess:
- Prove the libc build from the leak itself - the delta
0x219ce0 - 0x21ac80 = 0x10a0identifies it, and a mismatch would show up immediately in the second pointer. - Compile a local replica of the same systemd unit and binary, run it with the same ncurses... with the same interactive sequence, and break at the
retof the request handler to measureenviron-> saved RIP; the difference must be stable across 20+ runs, which it will be precisely because of the fork model. - If the target libc differs from yours, get it from the container image path in the unit file (no internet needed) rather than guessing offsets, and re-derive every delta against
main_arenainstead of against absolute numbers.