Berkner Tech

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. Timing attacks read that channel.

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: scheduling, network jitter, and other load blur 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.

That clean timing is a gift to the attacker. A logic analyzer on a response line, or a glitch platform’s precise trigger, measures the response time to the cycle. 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 Precisely

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. 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. Any data-dependent branch can leak: MAC and signature tag verifications that compare the computed tag to the received one, key comparisons, and conditional paths inside cryptographic routines that take different time for different secret values.

A MAC check is a particularly dangerous case, because a timing leak there can let an attacker forge a valid tag byte by byte, defeating the integrity protection entirely. Anywhere code compares a secret or an authentication value and can return early, the same attack applies, which 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 Routine

You rarely need to write the constant-time compare yourself. Reputable crypto libraries provide one specifically for comparing secrets and authentication tags, and using it is both 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 and documents why the code is written that way. It also avoids the subtle ways a hand-written version can be defeated by a compiler that optimizes the loop back into an early exit, which is a real hazard.

Watch the Compiler

A constant-time function written in C is only constant-time if the compiler keeps it that way. Aggressive optimization can reintroduce a branch, short-circuit the loop, or otherwise restore the data dependence you carefully removed. This is a known and frustrating failure mode.

The defenses are to use the library routines, which are written and tested with this in mind, to inspect the generated assembly for security-critical comparisons, and to use compiler barriers or volatile accesses where needed to stop the optimizer from being clever. Verifying the machine code, not just the source, is what confirms the comparison is actually constant-time on your target.

Beyond Comparisons

The same principle extends past simple comparisons to any cryptographic operation whose timing depends on secret data: table lookups indexed by key bytes, modular exponentiation that branches on exponent bits, and division by secret values. These leak through timing and through related cache and power channels.

Mitigating them is the domain of constant-time cryptographic implementations, which avoid secret-dependent branches and memory access patterns throughout. For an embedded product, the practical guidance is to use a crypto library that advertises constant-time operation rather than rolling your own, because getting every one of these right by hand is difficult and easy to get subtly wrong.

Where Leaks Hide in a Review

When I review firmware, I look specifically for comparisons of secrets or authentication tags that bail out early, because those are the ones that quietly hand an attacker a way to recover the value. The pattern is easy to spot once you are looking: a loop or a memcmp over a secret with a return inside the loop or a branch on the result.

Finding one is a concrete, fixable finding with a clear remediation: switch to a constant-time comparison. It is also a high-value find, because an early-exit check on a token or a MAC can undermine authentication for the whole device, and the fix is a one-line change to a vetted routine. That ratio, large impact, small fix, is why it is 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.


References and Further Reading