Skip to main content

Berkner Tech

Timing Attacks on Embedded Comparisons

A comparison that returns early leaks how much of a secret you got right. Here is how timing attacks work and why constant-time code matters.

Timing Attacks on Embedded Comparisons

A timing attack recovering a secret one byte at a time from an early-exit comparison on an embedded device

A timing attack recovers secrets by measuring how long an operation takes. The classic case is a password or token comparison that bails out at the first mismatched byte, leaking exactly how much you guessed correctly. On a microcontroller, where timing is clean and repeatable, that leak is very practical to exploit.

Secrets That Leak through Time

Most of the time we think of a comparison as returning a yes or a no, and assume that is all an attacker learns. But the comparison also takes a certain amount of time, and if that time depends on the secret, the duration is a second channel of information leaking alongside the answer.

The canonical example is checking a password or an authentication token. A function that returns as soon as it finds a mismatch takes slightly longer when more of the leading bytes are correct, because it examines more of them before bailing out. That tiny difference, measured carefully, tells an attacker how close a guess was, which is enough to solve the secret without ever guessing it whole.

The Early-Exit Leak

A naive byte-by-byte comparison stops the moment two bytes differ. A guess with the first byte correct runs one comparison further before returning than a guess with the first byte wrong, so it takes marginally longer. That difference is the leak.

// the mistake: returns as soon as a byte differs -> time depends on the secret
int check_token(const uint8_t *guess, const uint8_t *secret, int n) {
    for (int i = 0; i < n; i++)
        if (guess[i] != secret[i]) return 0;   // early exit leaks position
    return 1;
}

Each additional correct leading byte adds one more loop iteration before the function returns, so response time rises in steps as the guess gets closer. An attacker does not need to see the secret; they only need to see which guesses take longer, and the function tells them.

Solving a Secret One Byte at a Time

The attack turns that leak into a byte-by-byte search. Fix all but the first byte, try every value for it, and keep the value whose rejection took longest, because that is the one that matched and pushed the comparison one iteration further. Then move to the next byte and repeat.

# attack loop: for each position, the slowest-rejected guess is the correct byte
for pos in range(token_len):
    timings = {b: measure_time(guess_with(pos, b)) for b in range(256)}
    best = max(timings, key=timings.get)   # longest time -> correct byte
    known[pos] = best
Example output
pos 0: 0x4a (mean 41.2us vs 39.8us for others)
pos 1: 0x91 (mean 42.6us vs 41.2us)
# secret recovered byte by byte; 256*len guesses instead of 256^len

This collapses the search from astronomically large, every possible full token, to merely 256 tries per byte. A 16-byte token goes from infeasible to about four thousand measured guesses. The early-exit comparison turned a brute force that would never finish into one that completes in minutes.

Why Embedded Makes It Easier

Timing attacks on busy network servers are noisy, with scheduling, network jitter, and other load blurring the small differences. On a microcontroller, timing is clean and repeatable: the same code path takes the same number of cycles every time, with little background noise to hide the signal. A logic analyzer on a response line, or a glitch platform’s precise trigger, measures the response time to the cycle, so a difference of a few cycles per matched byte, invisible across the internet, is trivially measurable on a chip you have on the bench, which is exactly where embedded targets live.

Measuring the Difference

The measurement does not need exotic gear. A GPIO that toggles when the comparison starts and ends, watched on a logic analyzer, gives cycle-accurate timing, and even response latency over a serial or network link is often clean enough on embedded targets.

# measure the response time of each guess on a logic analyzer channel
sigrok-cli -d fx2lafw -C D0 --time 200ms -o guess.sr
# delta between request edge and response edge = comparison duration
Example output
guess 0x4a -> 41.2 us
guess 0x4b -> 39.8 us
guess 0x4c -> 39.8 us
# 0x4a stands out by ~1.4 us: one extra matched byte

A consistent extra microsecond on one guess out of 256 is the signature of a matched byte. Averaging a handful of measurements per guess removes what little noise exists, and the correct value separates cleanly from the rest. The leak that was theoretical on a server is plainly visible here.

It Is Not Just Passwords

The early-exit pattern hides in more places than login checks, anywhere code compares a secret or an authentication value and can return early:

  • Password and token checks. The canonical early-exit case.
  • MAC and signature-tag verification. A leak here can let an attacker forge a valid tag byte by byte, defeating integrity entirely.
  • Key comparisons. Any direct compare of secret key material.
  • Secret-dependent branches in crypto. Table lookups indexed by key bytes, modular exponentiation that branches on exponent bits, division by secret values, all of which leak through timing and related cache and power channels.

The MAC case is the most dangerous, because forging a tag defeats the integrity protection the device relies on. That is why the fix has to be applied broadly, not just to the password routine.

Constant-Time Is the Fix

The defense is to make the comparison take the same time regardless of how much matched. A constant-time compare examines every byte, accumulates any differences into one value, and returns based on that value at the end, so the duration carries no information about where or whether a mismatch occurred.

// constant-time: always examines every byte, no early exit
int ct_compare(const uint8_t *a, const uint8_t *b, int n) {
    uint8_t diff = 0;
    for (int i = 0; i < n; i++)
        diff |= a[i] ^ b[i];     // accumulate; never branch on the secret
    return diff == 0;            // single result at the end
}

Because this version always runs the full loop and never branches on the secret, every guess takes the same time, and the timing channel goes silent. The attacker measuring response times sees no difference between a near-miss and a total miss, so the byte-by-byte search has nothing to work with.

Use the Library, and Watch the Compiler

You rarely need to write the constant-time compare yourself; reputable crypto libraries provide one for comparing secrets and tags, and using it is safer and clearer about intent than a hand-rolled loop.

// prefer the vetted library function for secret comparisons
if (mbedtls_ct_memcmp(received_tag, computed_tag, TAG_LEN) != 0)
    reject();   // constant-time, no early-exit timing leak

Calling the library’s constant-time comparison for every secret or tag check makes the safe behavior the default. It also avoids a real hazard: a C function written to be constant-time is only constant-time if the compiler keeps it that way, and aggressive optimization can reintroduce a branch or short-circuit the loop. Inspecting the generated assembly for security-critical comparisons, and using compiler barriers or volatile accesses where needed, is what confirms the comparison is actually constant-time on your target.

What I Check in a Review

When I review firmware, I look specifically for comparisons of secrets or authentication tags that bail out early, the loop or memcmp over a secret with a return inside the loop or a branch on the result. The same principle extends to any cryptographic operation whose timing depends on secret data, which is why the practical guidance is to use a crypto library that advertises constant-time operation rather than rolling your own. Finding an early-exit check is a high-value, easily fixable finding: it can undermine authentication for the whole device, and the fix is a one-line change to a vetted routine, the kind of large-impact, small-fix ratio worth hunting for.

Where This Fits

Hunting for timing leaks in authentication and cryptographic code, and confirming the fixes hold at the assembly level, is part of a product security assessment. If you want your comparisons and crypto routines reviewed for timing leaks, that is the kind of work we do at Berkner Tech.

Share:

More Posts