Description
A provided PIN checker binary validates an 8-digit code one character at a time. Because it returns more slowly for each correct digit, you can abuse the timing difference to recover the full PIN and then use it to obtain the flag from the remote service.
Setup
Fetch pin_checker, mark it executable, and probe it with sample 8-digit inputs while timing execution.
Notice that each correct digit increases the runtime slightly-classic timing side channel behavior.
Write a script (pwntools + time.perf_counter) to brute-force each position, then submit the final PIN to the remote service.
wget https://artifacts.picoctf.net/c/74/pin_checkerchmod +x pin_checkerecho "11111111" | time ./pin_checkerpython3 solve_pin.pync saturn.picoctf.net 53639Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Confirm the timing leakObservationI noticed the binary validates PIN digits one at a time and exits early on a mismatch, which suggested that measuring how long./pin_checkertakes for different inputs would reveal how many leading digits were correct.Runningecho "11111111" | time ./pin_checkerand then tweaking one digit shows the runtime grows whenever the prefix is correct. That leak lets us learn each digit sequentially.What didn't work first
Tried: Running strings or disassembling the binary to find the PIN stored in plaintext.
The binary does not store the PIN as a literal string - it validates digits procedurally. Strings and objdump show the code structure but not the secret value. The PIN must be recovered at runtime by observing timing behavior.
Tried: Trying only one sample per digit and assuming the slowest digit is correct.
A single measurement is too noisy - process scheduling and OS interrupts can make a wrong digit appear slower than the correct one. You need multiple repeated measurements and a median or averaging step to see the true timing signal reliably.
Learn more
A timing side-channel attack exploits the fact that a program takes measurably different amounts of time depending on secret data. In this case, the PIN checker validates digits left-to-right and exits early on the first mismatch - so a PIN with 3 correct leading digits takes longer than one with 0 correct digits.
This is the same class of vulnerability that affects naive string comparison in authentication code. Languages like Python return from
==as soon as a character mismatches, which leaks information about how far into the string the comparison succeeded. Secure implementations use constant-time comparison - they always check every byte regardless of where a mismatch occurs, so timing reveals nothing.In Python,
hmac.compare_digest(a, b)provides constant-time string comparison. In C,memcmpis not constant-time; security libraries provide safe alternatives likeCRYPTO_memcmp(OpenSSL) ortimingsafe_bcmp(BSD libc).Step 2
Automate the measurementObservationI noticed the timing signal grows one digit at a time, which suggested writing a pwntools script usingtime.perf_counter()to brute-force each position independently, reducing the search from 10^8 to just 80 measurements.Spawn the checker, send a candidate PIN, and time the response withtime.perf_counter()(Python 3.3+; on older interpreters, fall back to thetimeitmodule ortime.process_time()). For each position 0-7 try digits 0-9, repeat each measurement N times to control for noise, take the per-digit median, and pick the slowest as the correct character before locking it in and moving to the next index.A minimal sketch with N samples and a median across runs:
from statistics import median from time import perf_counter from pwn import process N = 20 def measure(pin: str) -> float: samples = [] for _ in range(N): p = process('./pin_checker') t0 = perf_counter() p.sendline(pin.encode()) p.recvall() samples.append(perf_counter() - t0) p.close() return median(samples) found = "" for pos in range(8): timings = {d: measure(found + str(d) + "0" * (7 - pos)) for d in range(10)} best = max(timings, key=timings.get) found += str(best) print(f"pos {pos}: {found} (timings: {timings})") print("PIN:", found)What didn't work first
Tried: Using time.time() instead of time.perf_counter() to measure elapsed time.
time.time() has low resolution (often only millisecond precision) and can jump when the system clock is adjusted. time.perf_counter() uses a high-resolution monotonic clock and is the correct choice for sub-millisecond timing measurements.
Tried: Trying all 10^8 PIN combinations by brute force instead of recovering one digit at a time.
Brute-forcing all 100 million combinations would take an impractical amount of time. The timing leak lets you treat each digit position independently - 10 candidates times 8 positions equals only 80 measurements total, which finishes in seconds.
Learn more
pwntools is a Python library designed for CTF challenges and exploit development. Its
process()andremote()classes make it easy to spawn local processes or connect to remote services, send inputs, and read responses. Combined withtime.perf_counter()for high-resolution timing, it's the standard toolkit for timing attacks.The brute-force strategy here works digit by digit: fix the known-correct prefix, try all 10 digits for the next position, and pick the one that takes longest. This reduces the search space from 10^8 (100 million) combinations to just 10 × 8 = 80 measurements - a massive speedup that makes the attack practical in seconds.
Real-world timing attacks require careful statistics to handle noise. Network latency introduces variance, so attackers typically repeat each measurement hundreds of times and take the median or minimum. Tools like tlsfuzzer implement statistical timing analysis for remote attacks against TLS implementations, where differences as small as nanoseconds can be detected over a network.
Step 3
Redeem the PINObservationI noticed the recovered PIN was obtained locally from the downloadedpin_checkerbinary, which suggested connecting to the remotencservice and submitting it to receive the actual flag.Connect to the remote service. It prints a prompt, expects the 8-digit PIN as a newline-terminated string, and responds with the flag.The exchange looks like this:
$ nc saturn.picoctf.net 53639 Please enter your secret pin to retrieve the secret information: 13371337 Access granted. Here is the flag: picoCTF{t1m1ng_4tt4ck_914c...}The PIN must be terminated with a newline;
ncsends one when you press Enter, but if you script it, rememberecho "13371337" | nc ...rather thanprintf "13371337".What didn't work first
Tried: Sending the PIN with printf without a trailing newline, e.g. printf "13371337" | nc ...
The remote service reads a line terminated by a newline character. Without the newline, nc sends the bytes but the server keeps waiting for input and never responds. Using echo or appending a newline ensures the server processes the PIN immediately.
Tried: Running the timing script directly against the remote service instead of the local binary.
Network latency adds far more noise than the tiny timing difference the checker produces. The correct approach is to recover the PIN locally against the downloaded binary where latency is negligible, then send the recovered PIN to the remote service once.
Learn more
netcat (
nc) is a versatile networking tool that opens raw TCP or UDP connections. It's described as the "TCP/IP Swiss Army knife" - you can use it to connect to any open port and send/receive arbitrary data. For CTF challenges, it's the standard way to interact with remote challenge servers that expose a text-based interface.Timing side channels have been exploited against major real-world systems: early TLS implementations leaked RSA key bits through timing differences during decryption (Bleichenbacher's attack), and CPU branch prediction has been abused in the Spectre and Meltdown vulnerabilities to leak memory across security boundaries using cache timing.
The defense against this class of attack is comprehensive: constant-time algorithms, adding random delays (jitter), and rate-limiting repeated authentication attempts. Modern security standards like FIPS 140-3 require constant-time implementations for cryptographic operations precisely because timing leaks can undermine otherwise mathematically sound algorithms.
Flag
Reveal flag
picoCTF{t1m1ng_4tt4ck_914c...}
Timing side channels are powerful-even without reading the binary, runtime differences disclosed the entire secret.