Ricochet picoCTF 2025 Solution

Published: April 2, 2025

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 1
    Understand the three vulnerabilities
    Observation
    I noticed the challenge description called out three distinct weaknesses (address spoofing, nonce reuse, and a cost-free sync packet), which suggested I needed to understand each bug independently before combining them into an exploit chain.
    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. 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. The reason nonces reset here is that each new session does a fresh DH handshake and starts the counter back at 0; from the controller's perspective, nonce 5 of session 2 is cryptographically identical to nonce 5 of session 1 because both are signed under a key derived from a fresh handshake but with the same nonce input. 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 2
    Collect HMAC-signed packets from debug messages
    Observation
    I noticed the nonce resets to 0 at the start of each session, which suggested that valid HMAC-signed packets captured in one session would be accepted again at the same nonce position in a fresh session, making the debug menu a way to harvest replayable authenticated packets.
    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('000000050' + '1' + '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 requires the shared DH session key, which was derived ephemerally and is never transmitted. Without the key, HMAC-SHA256 output is computationally indistinguishable from random. Capturing a packet the real controller already signed avoids this entirely - the HMAC is valid by construction.

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

    The normal menu output does not print raw packet bytes or HMAC tags - it only shows human-readable state like the robot grid. The debug mode option is the only path that causes the server to emit the wire-format bytes you need. Without those bytes you have 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, an HMAC computed over message + nonce_5 in session 1 is identical to the HMAC the receiver expects for session 2's message at the same nonce position - the HMAC cannot distinguish between the two sessions.

    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 3
    Replay packets to control the robot
    Observation
    I noticed that secure_data_request advances the nonce without consuming a move from the budget, which suggested I could align the session nonce to any target value for free and then paste a captured signed movement packet that the server would accept as 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
    # 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 that the incoming packet's nonce matches the expected session nonce. If you captured a packet at nonce 5 but the new session is still at nonce 0, the HMAC tag is computed over the wrong nonce value from the receiver's perspective and the packet is rejected. You must pump nonce-advancing no-op packets first to align the counter.

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

    Each move command decrements the move-limit budget. If you spend moves just to align the nonce, you exhaust the budget before walking the robot to the flag tile. The secure_data_request packet type advances the nonce counter without touching the move-limit counter, which is exactly the bug that makes the exploit budget-neutral.

    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
  • Cipher Identifier & Auto-DecoderPaste any ciphertext and the tool auto-runs every common decoder (base64, hex, Morse, ROT, Atbash, Bacon, binary, decimal, URL) and ranks the results by English-likeness.
  • Frequency AnalysisAnalyze letter frequencies in a substitution cipher and interactively build the decryption mapping with auto-filled guesses.

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

Replay attacks succeed when authentication tokens can be reused outside their intended context, such as a fresh session that resets the nonce counter back to zero. HMAC alone does not prevent replay if the nonce domain is not properly isolated across sessions; the same applies to OAuth tokens without expiry, API keys without request signing, and wireless protocols that skip sequence-number tracking. Real defenses combine authenticated identity, session-scoped nonces, and logic-level counters that are all validated together, so no single omission allows the chain to be broken independently.

Related reading

Want more picoCTF 2025 writeups?

Useful tools for Cryptography

What to try next