Skip to main content

SideChannel picoCTF 2022 Solution

Exploit a measurable behavioral difference in a PIN checker to recover an 8-digit secret without brute force.

Published: July 20, 2023Updated: August 25, 2026

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.

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.

bash
wget https://artifacts.picoctf.net/c/74/pin_checker
bash
chmod +x pin_checker
bash
echo "11111111" | time ./pin_checker
python
python3 solve_pin.py
bash
nc saturn.picoctf.net <PORT_FROM_INSTANCE>

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Confirm the timing leak
    Observation
    The binary checks PIN digits one at a time and exits early on a mismatch. So timing how long it runs for different inputs reveals how many leading digits were right.
    Running echo "11111111" | time ./pin_checker and 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, memcmp is not constant-time; security libraries provide safe alternatives like CRYPTO_memcmp (OpenSSL) or timingsafe_bcmp (BSD libc).

  2. Step 2Automate the measurement
    Observation
    The timing signal grows one digit at a time. A pwntools script around perf_counter can brute-force each position independently, taking the search from 10^8 candidates down to about 80 measurements.
    Spawn the checker, send a candidate PIN, and time the response with time.perf_counter() (Python 3.3+; on older interpreters, fall back to the timeit module or time.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() and remote() classes make it easy to spawn local processes or connect to remote services, send inputs, and read responses. Combined with time.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.

  3. Step 3Redeem the PIN
    Observation
    The PIN came from the downloaded binary running locally. Connect to the remote service and submit it there for 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 <PORT_FROM_INSTANCE>
    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; nc sends one when you press Enter, but if you script it, remember echo "13371337" | nc ... rather than printf "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 private-key bits through timing differences during modular exponentiation (Kocher's timing attack, later extended to remote servers by Brumley and Boneh), a separate weakness from Bleichenbacher's PKCS#1 v1.5 padding oracle, whose modern variants often use timing as the oracle signal. Likewise, 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.

Interactive tools
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
  • Frequency AnalysisAnalyze letter frequencies in a substitution cipher and interactively build the decryption mapping with auto-filled guesses.

Flag

Reveal flag

picoCTF{t1m1ng_4tt4ck_914c...}

Timing side channels are powerful-even without reading the binary, runtime differences disclosed the entire secret.

Key takeaway

Side-channel attacks read observable physical or computational behavior instead of breaking the algorithm. An early-exit comparison leaks how many leading characters matched, which turns an exponential search into a linear one recovered position by position. Constant-time comparisons, hmac.compare_digest in Python or CRYPTO_memcmp in OpenSSL, fix it by always examining every byte, so runtime says nothing about the secret. The same principle drove Kocher's timing attack on RSA in TLS, Spectre and Meltdown against CPU caches, and timing attacks on remote services.

Related reading

Useful tools for Forensics

Where to go next