Tag: HMAC

  • Flipper Zero, part 3: Sub-GHz remotes, rolling codes and how gates should be built

    Flipper Zero, part 3: Sub-GHz remotes, rolling codes and how gates should be built

    Part 3 of 5 in the Flipper Zero series. Part 1: what it actually is and what it can’t do. Part 2: why so many office badges are weaker than they look.

    Garage doors, gates, car fobs, doorbells and cheap alarm sensors all talk on a handful of license-free bands, mostly around 315, 433.92, 868 and 915 MHz depending on where you live. The Flipper Zero has a CC1101 transceiver that covers 300 to 348, 387 to 464 and 779 to 928 MHz, with regional limits on where it will transmit. That makes it a convenient way to look at your own remotes and ask whether the thing guarding your driveway was designed by someone who had met an attacker.

    Same rules as the earlier parts: my own device, my own remotes, and a defender’s goal. Transmitting captured codes at a gate or car you do not own is illegal in most places, and the receiver you are testing may belong to a landlord or an employer. Get permission first.

    What the Flipper shows you

    The Sub-GHz menu has Read, Read RAW, Saved and Add Manually. Read tries to decode a signal against the protocols it knows and prints the frequency, modulation and the bits. Read RAW records the pulse timing without interpreting it.

    The first photo is my own Flipper’s main menu with Sub-GHz selected, and the second is its Sub-GHz submenu.

    Flipper Zero main menu with Sub-GHz highlighted
    Flipper Zero Sub-GHz submenu: Read, Read RAW, Saved, Add Manually

    Press a cheap fixed-code remote near it and the Flipper will show you the same bits every time. Press a rolling code remote and the bits change on every press. Seeing that difference on your own remote takes about ten seconds and is a better lesson than any slide.

    Fixed codes: nothing to defend

    Old remotes with DIP switches, or the cheapest 433 MHz clones, send the same code on every press. Anyone who hears it once can send it forever. There is no cryptography to attack, which is the whole problem. If your gate still uses one, the fix is to replace the receiver and remotes. Nothing you can configure makes a fixed code safe.

    Rolling codes: what they actually are

    A rolling code remote sends a serial number and a counter, protected so that a receiver can check the counter has moved forward. The best known design is Microchip’s KeeLoq. It uses a 32 bit block cipher with a 64 bit key and 528 rounds. The hopping part of the message carries a 16 bit synchronisation counter, and the serial number goes out in the clear next to it. In the common learning schemes the per-remote key is derived from the serial number and a manufacturer key.

    That last detail is where the trouble started. Several groups published cryptanalysis of KeeLoq in 2007 and 2008, and in 2008 Eisenbarth and colleagues presented “On the Power of Power Analysis in the Real World” at CRYPTO. They used power analysis on the receiver side to extract the manufacturer key. With that key, anyone who hears one transmission from a remote can compute its key from the serial number. That is a break of the whole product family, not of one gate.

    Receivers also have to tolerate lost presses. You pressed the button in your pocket twice, out of range, and the receiver has to accept counter 5 after seeing counter 2. So it accepts any value inside a look-ahead window. Here is a toy model of that logic. It is not KeeLoq, it uses HMAC-SHA256 truncated to 4 bytes, but the receiver behaviour is the same shape:

    import hmac, hashlib
    
    KEY = b"per-remote-secret"
    WINDOW = 16
    
    def code(counter):
        msg = counter.to_bytes(4, "big")
        return hmac.new(KEY, msg, hashlib.sha256).digest()[:4].hex()
    
    class CounterReceiver:
        def __init__(self):
            self.last = 0
        def press(self, c):
            for n in range(self.last + 1, self.last + 1 + WINDOW):
                if hmac.compare_digest(code(n), c):
                    self.last = n
                    return f"OPEN (counter {n})"
            return "rejected"

    Jam and capture

    Samy Kamkar showed the practical weakness at DEF CON 23 in 2015 with a device he called RollJam. The attacker jams the receiver and records the owner’s press at the same time. The owner presses again, the attacker jams and records that too, but this time releases the earlier code to the receiver, so the door opens and nobody suspects anything. The attacker now holds a valid code the receiver has never seen.

    The diagram below compares the two receiver designs side by side.

    Sequence diagram comparing a counter-only receiver, where jammed and recorded codes open the gate twice, with a challenge-response receiver, where a recorded answer fails against a fresh nonce. Annotation boxes added for explanation.

    The key point is that a counter-only receiver has no idea when a code was made. A code from yesterday is as good as one from a second ago, as long as the counter has not passed it. I ran the toy model through the scenario and rendered the real output below. The jammed presses are generated by my script, so this is a simulation of the logic, not a capture of any real remote.

    Terminal output of the toy rolling code receiver showing a replay rejected, a jam-and-capture attack succeeding twice, and a challenge-response receiver rejecting a captured answer

    Note the third line of the middle block. The receiver accepts the stored second code long after the owner walked off. Once the stored code is used, replaying it fails, so the attack is a one-shot, but one shot is enough to open a gate.

    Counter windows have caused real vulnerabilities too. CVE-2021-46145 covers the Honda Civic 2012 key fob, where the rolling code does not expire and the counter resynchronisation can be abused to replay unlock commands. CVE-2022-27254 is a worse case on the 2018 Civic, where the remote sends the same signal for every unlock request, so it was never rolling at all.

    How a gate should be built

    If I were specifying a new gate or garage opener, the list would be short.

    First, the receiver should reject codes older than a few seconds, or use challenge-response. In the last block of the demo the receiver sends a random nonce and the remote answers with an HMAC of it. A captured answer fails against the next nonce, so storing it gains nothing. This needs a two way radio, which costs a little more, and that is the price of getting rid of the stored-code attack.

    Second, use a modern authenticated protocol with a per-device key that is not derived from a shared manufacturer secret. AES-based systems exist. Ask the installer what the remote actually uses, and if the answer is “rolling code” with no name attached, treat that as a fixed code until proven otherwise.

    Third, keep the radio the weaker half of the system. A gate that opens on a single radio message should not also be the only barrier to something you care about. Put cameras on it, put a lock on the inner door, and do not rely on it to separate a vehicle from the street.

    Fourth, plan to replace the remotes. Lost ones should be removed from the receiver, not just forgotten, which means choosing a receiver that actually lets you delete individual remotes.

    Testing your own gear

    With the Flipper you can check three things on your own remote. Does Read decode it as a known protocol or leave you with raw pulses only? Do the bits change between presses? If they do not, you have a fixed code and a replacement project. And can you find the manufacturer and model on the receiver, so you can read its actual security claims rather than guessing?

    I would not go further than that on equipment I do not control.

    Part 4 moves to USB, keystroke injection from the Bad USB menu, and how to lock endpoints down.

    Other parts of the Flipper Zero series: part 1, part 2, part 3, part 4, part 5.

  • Timing side channels: why == on a secret is a bug, and how constant-time comparison works

    Timing side channels: why == on a secret is a bug, and how constant-time comparison works

    Here’s a line of code I still find in reviews every few months:

    if request.headers["X-Signature"] == hmac.new(key, body, "sha256").hexdigest():
        process(body)

    It looks fine. The HMAC is correct, the key is secret, the comparison is a comparison. The bug is that == on strings returns as soon as it finds the first byte that differs, so the time it takes leaks how many leading bytes of the attacker’s guess were right. Given enough measurements, an attacker can recover a valid signature one byte at a time without ever knowing the key.

    How the attack works

    Illustrative chart: time of an early-exit == comparison rises in steps with each correct leading byte of a 32-byte MAC guess, while a constant-time comparison stays flat

    A short circuiting comparison does roughly one unit of work per matching byte before it bails out. If the attacker submits a signature starting with a, then b, then c and so on, the guess with the correct first character takes very slightly longer than the other fifteen hex digits. Fix that character, move to the second one, repeat. For a 64 character hex digest that’s at most 16 x 64 = 1,024 candidate positions instead of 16^64 possible values. The brute force problem goes from impossible to linear.

    The obvious objection is that a few nanoseconds per byte can’t survive network jitter. That objection is weaker than it sounds. Jitter is noise, and noise averages out with repetition. Crosby, Wallach and Riedi’s 2009 paper “Opportunities and Limits of Remote Timing Attacks” showed that with careful statistics an attacker could distinguish differences in the tens of microseconds across the internet and around 100 nanoseconds on a LAN. In 2020, “Timeless Timing Attacks” by Van Goethem and colleagues removed much of the network jitter entirely by sending two requests in the same HTTP/2 packet and only looking at which response came back first. And an attacker who can get a VM in the same cloud region as your service is not measuring across the internet at all.

    I treat any comparison against a secret value as attackable. Whether a particular one is exploitable this week is not the question I want to be answering during an incident.

    A real example: Rails basic auth

    CVE-2015-7576 is a clean case. Action Controller’s http_basic_authenticate_with compared the supplied username and password with a normal string comparison, which, in the words of the advisory, made it easier for remote attackers to bypass authentication by measuring timing differences. The fix, released in January 2016 across the supported Rails branches, switched to a constant-time comparison.

    TLS has a much more famous version of the same class of problem. Lucky Thirteen (CVE-2013-0169) recovered plaintext from CBC mode TLS because the time taken to check the MAC depended on how much padding was removed. That one took years of careful patching across every TLS library, and it’s a big part of why TLS 1.3 dropped CBC cipher suites entirely.

    What constant-time comparison does

    The idea is to do the same amount of work regardless of where, or whether, the inputs differ. Instead of returning at the first mismatch, you accumulate the differences and only look at the result at the end:

    int ct_equal(const unsigned char *a, const unsigned char *b, size_t len)
    {
        unsigned char diff = 0;
        for (size_t i = 0; i < len; i++)
            diff |= a[i] ^ b[i];
        return diff == 0;
    }

    XOR is zero only when two bytes match, and OR-ing every result together means a single mismatch anywhere leaves diff non-zero. There’s no branch that depends on the data inside the loop, so the loop takes the same time for a guess that’s wrong in the first byte as for one that’s wrong in the last.

    Please don’t write this yourself in production, though. Optimising compilers are allowed to notice that once diff is non-zero the answer is decided, and to turn your careful loop back into an early exit. Library implementations use compiler barriers or assembly to stop that, and they get tested for it. Use them:

    import hmac
    if hmac.compare_digest(received_sig, expected_sig):
        process(body)
    import "crypto/subtle"
    
    if subtle.ConstantTimeCompare(receivedMAC, expectedMAC) == 1 {
        process(body)
    }
    const crypto = require("crypto");
    const a = Buffer.from(receivedSig, "hex");
    const b = Buffer.from(expectedSig, "hex");
    if (a.length === b.length && crypto.timingSafeEqual(a, b)) {
      process(body);
    }

    Other ecosystems have the same thing: hash_equals in PHP, MessageDigest.isEqual in Java, ActiveSupport::SecurityUtils.secure_compare in Rails, CRYPTO_memcmp in OpenSSL and sodium_memcmp in libsodium.

    Length is a leak too

    Most constant-time functions are only constant-time for inputs of equal length. Go’s ConstantTimeCompare documents that it returns 0 immediately when lengths differ, and Node’s timingSafeEqual throws, which is why the JavaScript example checks length first. For HMACs and hashes that’s fine, since the length is public anyway.

    For things like API tokens where you’d rather not reveal the length, a common trick is to hash both values first (SHA-256 or an HMAC with a per-process key) and compare the fixed length digests.

    The comparisons people forget

    Token lookups are the subtle one. If your API looks up a key with SELECT ... WHERE api_key = ?, the database’s index search is itself a comparison that can leak timing about the stored values. The pattern I prefer is a split token: the first part is a non-secret identifier you look up by, and the second part is a secret you verify against a stored hash with a constant-time comparison. A leaked database then also doesn’t contain usable tokens, which is a nice side benefit.

    Login endpoints leak in a different way. If you return immediately for an unknown username but spend 200 ms running Argon2 for a known one, the response time tells an attacker which accounts exist. Run a dummy hash against a fixed value when the user isn’t found, so both paths cost the same. This ties back to the password storage post from earlier this week: a slow hash makes the timing difference larger, not smaller.

    Error paths count as well. Returning “invalid padding” and “invalid MAC” as different errors, or failing at different points, turns a timing oracle into an even easier one. Verify the MAC first, over the whole message, and give one error for everything.

    What I check in review

    Any ==, !=, equals(), strcmp or memcmp that touches a signature, MAC, token, password reset code, session identifier or API key gets flagged. A Semgrep rule for this takes a few minutes to write and is worth running in CI, because the bug is easy to reintroduce. It costs nothing to fix and doesn’t change behaviour. It removes a whole class of attack that needs nothing more than a stopwatch and patience.