A strcpy into a heap buffer writes a terminating \x00. If the buffer is exactly
chunk-sized, that null lands one byte past the end — on the least-significant
byte of the next chunk's size field. That single byte is enough. It's the
off-by-one that turns a typo into overlapping chunks.
- arch
- amd64-64-little
- libc
- 2.27
- technique
- off-by-one NULL overflow -> size/prev_size desync -> chunk overlap
- classic killer
- the 2.29 "corrupted size vs. prev_size" check
what one null byte does to a size field#
Sizes are 0x10-aligned, so a chunk like 0x511 (size 0x510, PREV_INUSE
set). Write \x00 over its low byte and it becomes 0x500:
00000000 00 00 00 00 00 00 00 00 11 05 00 00 00 00 00 00 |................|
00000000 00 00 00 00 00 00 00 00 00 05 00 00 00 00 00 00 |................|
Two things changed at once: PREV_INUSE got cleared (now the allocator thinks the
previous chunk is free), and the size shrank by 0x10. The allocator's mental
map of the heap is now wrong by 16 bytes — and every overlap technique is just
exploiting a map that disagrees with reality.
from desync to overlap#
The classic recipe ("shrink a free chunk") chains that desync into two live chunks sharing memory:
- Lay out three chunks — call them A, B, C — and free B so it sits
in the unsorted bin with a real size of
0x510. - Off-by-one out of A nulls B's size low byte:
0x511 → 0x500. The allocator now believes B is0x500, though it physically spans0x510. - Forge a
prev_sizeinside B (at offset0x4f0) so that C's backward view points into the middle of B, not its true start. - Allocate and free around the seam. When consolidation fires, glibc merges using the forged boundaries, and you end up with one allocation overlapping a still-live chunk.
the exact sizes are a puzzle, not a constant
Which sizes overlap cleanly depends on your libc and whether the tcache is in
play — the 0x510/0x500 here is illustrative. Drive it in pwndbg with vis
and watch the chunk boundaries; the moment two chunks' user data visibly
overlaps, you've won. Don't transplant offsets between challenges.
why overlap is game over#
Once two chunks overlap, one of them controls the other's metadata while it's
still live. That's the strongest position in heap exploitation: you can edit a
free chunk's fd to poison the tcache, forge a
size to reclaim libc pointers, or set up any later House. Overlap is rarely the
exploit itself — it's the primitive that unlocks the real one.
where it leads
Overlap a 0x100 chunk over a freed tcache chunk, rewrite its fd to
__free_hook, allocate twice, write system:
segfault{0n3_byt3_t00_f4r}
modern status#
glibc 2.29 added the check that specifically kills the shrink:
A chunk's size must agree with the prev_size its neighbour records — exactly the
invariant the poison null byte breaks. So the textbook version is ≤ 2.28. The
idea outlives the patch, though: the House of Einherjar
and several modern off-by-null variants still start from "one byte clears
PREV_INUSE." Next, the capstone — the House of Orange,
which gets a free chunk and a shell without ever calling free.