CTF Walkthrough: Ricochet

Challenge Info:

  • Platform: picoCTF 2025 - Ricochet
  • Category: Web / Cryptography / IoT
  • Difficulty: Hard
  • Techniques: MITM, Diffie-Hellman Attack, HMAC Replay, Nonce Manipulation

TL;DR

We execute a Man-In-The-Middle (MITM) attack by hijacking the Controller's address without authentication. We then steal valid encrypted movement commands by recording them as they pass through our bridge. Finally, we manipulate the Robot's internal Nonce counter using protocol logic bugs to replay specific commands at the correct sequence, allowing us to control the Robot's path to the flag.


System Architecture and Normal Operation

To understand the attack, one must first understand how the system functions out of the box. The environment consists of a Robot (address 0x20) and a Controller (address 0x10) communicating via a central server.

The Boot Sequence

When the system starts, two major security handshakes occur:

  1. Validation: The Robot sends a random 16-byte "challenge" to the Controller. The Controller signs this challenge using a pre-shared authenticity_key (an HMAC) and sends it back. If the Robot verifies the signature, it trusts the Controller.
  2. Key Exchange (Diffie-Hellman): The system uses the Diffie-Hellman (DH) protocol to establish a session key. The Robot and Controller exchange public keys to generate a shared secret (DHSK), which is used to encrypt subsequent traffic.

The Movement Loop

Once the secure channel is established, the Robot enters a continuous loop where it requests directions. The protocol for a single movement follows a four-step "ping-pong" structure involving Nonces (numbers used to prevent replay attacks):

  1. Request: Robot sends secure_data (containing "get_movement") with Nonce N.
  2. Ack: Controller sends secure_data_ack (a blank message) with Nonce N.
  3. Confirm: Robot sends secure_data_request (a blank message) with Nonce N+1.
  4. Command: Controller sends secure_data_response (containing the direction, e.g., "east") with Nonce N+1.

By default, the Controller is programmed to send the Robot in a useless, infinite loop: East $\to$ South $\to$ West $\to$ North.


Vulnerability Analysis

The challenge is solvable because the implementation of these protocols contains several critical logic bugs that allow for a Man-In-The-Middle (MITM) Replay Attack.

Unauthenticated Address Hijacking

The Controller's address (0x10) is not hardcoded permanently; it can be changed via a set_addr command. Crucially, this command requires no authentication. This allows an attacker to send a command changing the Controller's address to an arbitrary location (e.g., 0x30). This effectively "silences" the legitimate Controller, as it will no longer receive the Robot's messages addressed to 0x10.

# In robotcontroller.py
def message_callback(self, msg):
    # ...
    # To avoid the bootstrapping problem, changing the address needs to
    # be an unauthenticated operation
    if msg_type == "set_addr":
        self.address = int(msg["new_addr"]) & 0xFF
        self.send_message({
            "msg_type": "ack_set_addr",
            "src": self.address,
            "dst": msg["src"]
        })

Why it matters: It explicitly states that set_addr is unauthenticated. You exploit this to move the Controller to 0x30 so you can take over 0x10.

The Mathematical Flaw: Diffie-Hellman MITM

The core cryptographic failure is the lack of Authenticated Key Exchange. While the initial handshake proves identity using a pre-shared key, the subsequent Diffie-Hellman (DH) parameters are exchanged in plaintext without signatures. This allows us to mount a "Textbook MITM" attack.

The Protocol Fundamentals

The system operates over a cyclic group $G$ with generator $g$ and a large prime modulus $p$.

  • Ideal World:
    $$ K_{ideal} = (g^a)^b \pmod p = (g^b)^a \pmod p = g^{ab} \pmod p $$
    The security relies on the discrete logarithm problem: given $g, p, g^a,$ and $g^b$, it is computationally ensuring to find $ab$.

The Man-In-The-Middle Execution

Since the public keys ($A = g^a$ and $B = g^b$) are not signed, the Robot cannot verify that $B$ originated from the Controller. We intercept the traffic and inject our own parameters.

Let:

  • $a, A$: Private/Public keys of Robot.
  • $b, B$: Private/Public keys of Controller.
  • $m, M$: Private/Public keys of Attacker (MITM).

Sequence of Operations:

  1. Interception (Robot $\to$ Attacker):

    • Robot sends $A = g^a \pmod p$.
    • Attacker blocks this.
    • Attacker computes shared key $K_{RA} = A^m \pmod p = g^{am} \pmod p$.
  2. Injection (Attacker $\to$ Controller):

    • Attacker sends their own public key $M = g^m \pmod p$ to Controller (impersonating Robot).
    • Controller computes shared key $K_{AC} = M^b \pmod p = g^{mb} \pmod p$.
  3. Reverse Injection (Controller $\to$ Attacker $\to$ Robot):

    • Controller responds with $B = g^b \pmod p$.
    • Attacker blocks this.
    • Attacker sends $M$ to Robot (impersonating Controller).
    • Robot computes shared key $K_{RA} = M^a \pmod p = g^{ma} \pmod p$.

The Resulting State (Two Keys)

Unlike a normal connection, there is no single "Session Key". Instead, two distinct encrypted tunnels exist:

  • Tunnel 1 (Robot $\leftrightarrow$ Attacker): Encrypted with $K_{RA} = g^{ma}$.
  • Tunnel 2 (Controller $\leftrightarrow$ Attacker): Encrypted with $K_{AC} = g^{mb}$.

The Transparent Bridge

The attacker now acts as a translation layer. For every packet $P$ sent by the Robot: $$ P_{decrypted} = \text{Decrypt}(P, K_{RA}) $$ $$ P_{re-encrypted} = \text{Encrypt}(P_{decrypted}, K_{AC}) $$

Figure 1: The MITM architecture showing the cryptographic separation between the two tunnels. The attacker (Eve) holds the "Master Keys" to both sides.

The "Invalid Progression" Logic Bug

The Robot has a flaw in how it handles invalid messages. If the Robot receives a message that fails verification (e.g., a bad HMAC), it often returns or loops incorrectly without crashing.

  • The Infinite Loop: If the Robot receives a "blank" message when it expects data, it may get stuck in a while loop, repeatedly requesting data. This behavior can be exploited to increment the Robot's Nonce counter without actually moving the Robot.
# In robot.py
async def read_command(self):
    await self.send_secure_data("get_movement")
    await asyncio.sleep(0.2)
    while True:
        resp = await self.recv_secure_data()
        if resp != "":
            return resp 
        # CRITICAL FLAW: If resp IS empty (""), it loops forever.
        # This allows you to pump blank messages to increment the nonce 
        # indefinitely while keeping the robot stuck in this loop.

Why it matters: By sending blank messages, you force the robot to stay in this while loop. Each loop iteration increments the internal Nonce counter, allowing you to "fast forward" the counter to match your recorded commands.

  • Tampering: Sending invalid messages can force the Robot's state machine to reset or progress unexpectedly, which is useful for manipulating the Nonce count.

HMAC Key Reuse (The Big Mistake)

The system uses a hash-based message authentication code (HMAC) to sign commands. The logic for the HMAC is:

$$HMAC = \text{SHA256}(\text{Message} \parallel \text{Nonce} \parallel \text{Shared_HMAC_Key})$$

The Vulnerability: The Shared_HMAC_Key (HSK) is static and never rotates. Since the key is constant, the HMAC for a specific command (e.g., "move east") at a specific Nonce (e.g., 9) depends only on that Nonce.

$$ \text{HMAC}(nonce=9) = \text{SHA256}(\text{"east"} \parallel 9 \parallel K_{static}) $$

This creates a Replay Oracle. If we observe a valid "East" packet at Nonce 9 today, that exact same binary blob will be valid for Nonce 9 tomorrow. We do not need to know $K_{static}$ to replay the message; we simply need to possess the recorded packet.

# In crypto.py
def compute_hmac(message, nonce, key):
    # The HMAC depends ONLY on message, nonce, and key
    hmac = (message + str(nonce) + key.hex()).encode()
    # ... (hashing logic) ...

# In robot.py
def hmac_and_encrypt(self, msg, nonce):
    # "shared_hmac_key" is static from keys.py
    msg_with_hmac = json.dumps(crypto.add_hmac(msg, nonce, keys["shared_hmac_key"]))
    # ...

Why it matters: Because keys["shared_hmac_key"] never changes, the HMAC for "East" at Nonce 9 is always the same. This allows you to record a packet once and replay it later.

Figure 2: The Replay Attack Logic. By harvesting valid packets, we build a "library" of moves that can be replayed when the Nonce counter matches.


The Attack Strategy

The attack is performed in three distinct phases: Setup, Harvesting, and Replay Execution.

Phase 1: Setup (The Man-In-The-Middle)

To control the robot, the attacker must first become the bridge between the devices.

  1. Hijack: Send a set_addr command to move the Controller to address 0x30. The Robot still tries to talk to 0x10, where the attacker is now listening.
  2. Bridge Validation: The Robot sends a validation challenge. Since the attacker doesn't know the authenticity_key, they simply forward this packet to the real Controller (at 0x30), get the signed response, and forward it back to the Robot.
  3. Key Swap: When the Robot initiates the key exchange, the attacker intercepts it. They establish a shared key with the Robot (shared_key_robot) and a separate shared key with the Controller (shared_key_controller). The attacker can now read all messages.

Phase 2: Data Harvesting (Scripts & Flowchart)

Since we cannot generate valid HMACs (we don't have the static key), we must steal valid ones. This is done using specific harvesting scripts (harvesting_scripts.py).

The Harvesting Logic

  1. Establish MITM: The script runs the hijacking and key exchange setup defined in common.py.
  2. Pass-Through Mode: The script enters a loop where it forwards every message between Robot and Controller.
  3. Decrypt & Log:
    • It decrypts the encrypted blob from the Controller.
    • It extracts the Command, Nonce, and HMAC.
    • It saves this triplet into a dictionary (e.g., harvested_commands = [...]).
  4. Repeat: We let the robot complete several laps of its default path (East -> South -> West -> North) to build a large library of valid moves.

Figure 2a: The logic flow for the automated harvesting scripts, showing how valid packets are intercepted, logged, and stored for later replay.

Important Note: We also run a separate harvesting loop to collect Blank Messages (Ack/Confirm packets). These are essential for the "Nonce Manipulation" phase, allowing us to increment the counter without moving the robot.

Figure 2b: Execution of the harvesting script. The console output demonstrates the successful MITM interception, showing decrypted messages from both the Robot and Controller (confirming the key exchange) and the real-time collection of valid directional commands with their HMAC signatures.

Phase 3: The Exploit Navigation (Nonce Manipulation)

"Fast-Forwarding to the Right Moment"

With our harvested valid packets, the final phase is pure choreography. We cannot generate new valid signatures, but we possess a library of valid moves that will be accepted if and only if the Robot's internal tracking Nonce matches the Nonce in our packet.

The Mindset: Crypto-Chronometry

Think of the Robot's state (its expected Nonce) as a clock. We have a set of "Keys" (Harvested Commands) that only work at specific times:

  • Key A (Move East): Works only at 12:00 (Nonce 9).
  • Key B (Move North): Works only at 12:15 (Nonce 15).
  • Key C (Move West): Works only at 12:30 (Nonce 21).

If the current time is 11:30, Key A won't work. We need to manually fast-forward the clock to 12:00 without moving the robot.

Our Plan

  1. Inventory Analysis: We organize our harvested secure_data_response packets. We see that our first valid "East" move was captured with Nonce 9.
  2. State Check: We start the robot. It handshakes. Current Nonce is usually low (e.g., 2).
  3. Artificial Aging (The "Invalid" Glitch):
    • This is the critical vulnerability. By sending Invalid or Blank messages to the Robot, we trigger an error handling path that increments the Robot's expected Nonce but does not execute a move.
    • This allows us to burn through Nonces cheaply.
  4. The Attack Loop:
    • Target: Next valid move is at Nonce $N_{target}$ (e.g., 9).
    • Current: Robot is at Nonce $N_{current}$ (e.g., 2).
    • Action: Send ($N_{target} - N_{current}$) invalid messages.
    • Execution: Robot's Nonce now equals 9. We inject the stored "East" packet.
    • Result: Signature matches. Nonce matches. Robot moves East.
  5. Repeat: We repeat this for every leg of the maze until the flag is reached.

Figure 3: Finding the optimal path through the grid by replaying intercepted commands.

Final Outcome

By strictly following this mathematical choreography, we guide the robot through the maze without ever knowing the HMAC key. The flag is revealed upon reaching the end of the path.

Navigating the Grid: By repeating this process—burning Nonces using blank/invalid messages until they match a harvested command—the attacker can pick and choose directions from the "menu" of recorded packets.

  • Need to go North? Increment Nonce until it matches a recorded "North" packet (e.g., Nonce 15).
  • Need to go West? Increment Nonce until it matches a recorded "West" packet (e.g., Nonce 21).

Figure 4: The actual maze layout showing the robot's required path from start to the flag location. Each movement must be precisely timed using nonce manipulation to replay the correct harvested command.


Summary of the Exploit

The system fails because it relies on a static secret key for message integrity. By hijacking the communication channel (MITM), the attacker can learn the encrypted forms of valid commands associated with specific sequence numbers (Nonces). By manipulating the Robot's sequence counter using logic bugs (sending blank or invalid messages), the attacker aligns the Robot's state with the recorded data, allowing them to "replay" specific movements at will to reach the flag.


Success: Flag Captured

We successfully navigated the maze and retrieved the flag!

Figure 4: The flag successfully retrieved after navigating the robot through the maze using replayed commands.


Key Takeaways

Vulnerability Chain

  • Unauthenticated address hijacking enables MITM positioning
  • Unauthenticated Diffie-Hellman key exchange vulnerable to MITM
  • Static HMAC keys enable replay attacks
  • Nonce counter manipulation through invalid message handling
  • Combined exploitation creates complete remote control

Attack Methodology

  1. Establish MITM position via address hijacking
  2. Intercept key exchange and establish dual tunnels
  3. Harvest valid HMAC-signed commands during normal operation
  4. Manipulate nonce counter using protocol bugs
  5. Replay harvested commands at correct nonce values
  6. Navigate robot to flag through controlled movement