Skip to main content

Ricochet picoCTF 2025 Solution

The HMAC nonce resets each session, so captured command packets replay with a spoofed controller address to seize the robot.

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

Description

A robot communicates with its controller over an authenticated radio protocol using Diffie-Hellman key exchange and HMAC-signed commands. Three weaknesses - address spoofing, nonce reuse across sessions, and a cost-free sync packet - let you replay captured commands and take control of the robot.

Launch the instance and grab the provided source (server.py / client.py) from the challenge page so you can read the protocol exactly instead of guessing from packet dumps.

Skim the source for three things: where addresses are checked (or not) in incoming packets, how the per-session nonce is initialized, and which packet types increment the move counter.

Connect to the service and pick the debug option from the menu so you can watch packets stream by while you experiment.

bash
nc <INSTANCE_HOST> <PORT_FROM_INSTANCE>
bash
# In another terminal, keep python3 running so you can paste signed packets back
python
python3

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Understand the three vulnerabilities
    Observation
    The description names three separate weaknesses: address spoofing, nonce reuse, and a sync packet that costs nothing. Understand each on its own before chaining them.
    Three independent bugs chain into a full exploit. The controller-address field sits outside the HMAC, so you can claim to be the controller. Nonces reset to 0 at session start, so a packet signed at nonce 5 in session 1 verifies at nonce 5 in session 2, provided the session key is the same. Replay your own handshake value in the new session and the derived shared secret comes out identical, which is what keeps the captured tag valid. And secure_data_request advances the nonce without incrementing the move-limit counter.
    Learn more

    Diffie-Hellman (DH) key exchange lets two parties derive a shared secret over a public channel without ever transmitting the secret itself. The protocol is sound in isolation, but it only establishes a shared key, it does not authenticate who you exchanged keys with. Without certificate-based authentication, a man-in-the-middle can establish a DH session with each party separately, forwarding messages between them while reading all traffic. The challenge's DH implementation is cryptographically correct but lacks this identity verification layer.

    A nonce (number used once) prevents replay attacks in authenticated protocols: each message includes a unique, incrementing counter value covered by the HMAC. If the receiver has already seen nonce N, it rejects any future message with the same nonce. Here each new session starts the counter back at 0, so the same nonce value comes around again. That alone is not enough: an HMAC also depends on the key, so a replayed tag only verifies if the session key repeats too. Send the same DH public value in the new handshake and the shared secret is derived identically, at which point nonce 5 of session 2 is indistinguishable from nonce 5 of session 1. The replay protection is only within a session, not across sessions.

    The move-limit bypass is a logic vulnerability: the protocol specification counts movement commands toward the limit but omits sync/data-request commands from the counter. By sending secure_data_request packets to advance the nonce counter to the right position, you can replay any captured movement command without spending the limited move budget.

  2. Step 2Collect HMAC-signed packets from debug messages
    Observation
    The nonce resets to zero each session, so a signed packet captured in one session is accepted again at the same nonce position in the next. The debug menu is where you harvest them.
    The debug menu prints every packet exchanged between robot and controller along with its HMAC tag. Open one session, drive the robot through every direction you might need (up/down/left/right), and copy each signed movement packet into a notebook keyed by direction so you can replay it in the next session.
    bash
    # Inside the netcat session, pick option 4 (or whatever the menu calls 'debug messages')
    # For each direction, send 'move <dir>' once and copy the printed packet bytes + HMAC tag
    # Packet wire format: <nonce:4 bytes><move:1 byte><HMAC:32 bytes>
    # Hex example: 00000005 | 01 | 9f3a...c0d2  (37 bytes total, no delimiters on the wire)
    # Save them as a Python dict keyed by direction:
    # captured = {b'up': bytes.fromhex('00000005' + '01' + '9f3a...c0d2'), b'down': ..., ...}
    What didn't work first

    Tried: Trying to forge a new signed packet with a known nonce instead of replaying a captured one.

    Forging needs the shared session key, which is derived ephemerally and never sent. Without it, HMAC-SHA256 output is indistinguishable from random. Capturing a packet the real controller already signed sidesteps the problem: the tag is valid by construction.

    Tried: Capturing packets without enabling debug mode, by sniffing traffic from the nc session output alone.

    The normal menu prints human-readable state like the robot grid, never raw packet bytes or HMAC tags. Debug mode is the only path that emits the wire format, and without those bytes there is nothing to replay.

    Learn more

    HMAC (Hash-based Message Authentication Code) provides integrity and authenticity: only someone who knows the secret key can produce a valid tag for a given message. The security guarantee holds only if the key is secret and each message is unique (enforced by the nonce). When the nonce resets across sessions and the key derivation repeats, an HMAC computed over message + nonce_5 in session 1 is exactly the tag the receiver expects at the same nonce position in session 2, because both inputs to the HMAC (key and message) are the same. Nothing in the tag identifies which session it came from.

    Debug endpoints in production protocols are a common real-world vulnerability. If a debug mode emits plaintext or authenticated traffic that an attacker can observe, it turns an active attack into a passive one: collect valid signed messages first, then replay them later. Systems that expose debug ports in production (telnet, JTAG, serial consoles) have been the entry point for several high-profile IoT and industrial control system compromises.

  3. Step 3Replay packets to control the robot
    Observation
    secure_data_request advances the nonce without spending a move. That lets you align the session nonce to any value for free, then replay a captured movement packet the server still considers authentic.
    Open a fresh session, claim the controller address in your handshake, then for every move you want to send: fire secure_data_request until the session nonce matches the captured packet's nonce, paste the captured bytes, and the robot accepts the move without decrementing the budget. Walk it to the flag tile and read the state.
    python
    # Pseudo-code for the replay loop using pwntools (see linked guide)
    from pwn import remote
    io = remote('<INSTANCE_HOST>', <PORT_FROM_INSTANCE>)
    # 1. Do DH handshake claiming controller address, reusing the same public value
    #    so the derived session key matches the one the captured tags were signed under
    # 2. For each move in path:
    #      while session_nonce < captured_nonce: io.sendline(b'secure_data_request')
    #      io.send(captured[direction])
    # 3. io.sendline(b'get_state') and parse the response for picoCTF{...}
    What didn't work first

    Tried: Sending the captured movement packet immediately at nonce 0 in the new session, without using secure_data_request to advance the nonce first.

    The receiver checks the packet's nonce against the session's expected value. Capture at nonce 5 and replay into a session still at zero, and the tag no longer covers the nonce the receiver expects, so the packet is rejected. Advance the counter with no-op packets first.

    Tried: Using move commands instead of secure_data_request to advance the nonce to the correct position.

    Every move decrements the budget, so spending moves to align the nonce leaves you short before the robot reaches the flag tile. secure_data_request advances the nonce without touching the move counter, and that is the bug that makes this free.

    Learn more

    The exploit chain combines all three bugs: address spoofing positions you as the controller, nonce reuse makes your captured packets valid in the new session, and the cost-free sync lets you reach the exact nonce value needed without burning moves. Any one bug alone would be insufficient; all three together give full robot control within the move budget. pwntools and netcat are both useful here; netcat for hand-driving the menu, pwntools when you script the replay loop.

    This mirrors real-world attacks on industrial control systems (ICS) and IoT device protocols. Many legacy protocols (Modbus, DNP3, older Zigbee implementations) lack message authentication entirely and rely on network isolation for security. When these systems are exposed to the internet or when an attacker gains network access, replay attacks are trivial. Adding HMAC authentication is necessary but insufficient: nonce management, session isolation, and address authentication must all be implemented correctly for the protection to hold.

Interactive tools
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • XOR CipherXOR-decrypt hex or text ciphertext with a known key, or brute-force the single-byte key automatically.

Flag

Reveal flag

picoCTF{r1c0ch3t_...}

The full flag string is held back here; the rest of this page walks through the exploit so you can recover it on your live instance. Approach: spoof controller address, collect HMAC-signed packets from debug output, advance the nonce with cost-free sync packets, then replay captured movement commands.

Key takeaway

A replay attack works when an authentication token can be reused outside its intended context, and a session that resets the nonce to zero provides exactly that. HMAC alone stops nothing if the nonce domain is not isolated per session, the same gap that affects OAuth tokens without expiry, API keys without request signing, and wireless protocols that skip sequence numbers. A real defense validates authenticated identity, session-scoped nonces, and logic-level counters together, so no single omission opens the chain.

Related reading

Useful tools for Cryptography

Where to go next