Wiki / Concepts

CRC

A cyclic redundancy check: a few bytes of polynomial arithmetic appended to a frame so a receiver can spot corruption. For an analyst it is the single best oracle there is, because it tells you your decode is right without anyone confirming it.

A CRC as a decode oracle
Your hypothesis: bit order, framing, whitening
Recompute the CRC over the frame you recovered
Agrees: the hypothesis is right, move on
Disagrees: change one assumption, try again
A wrong hypothesis that still produces a valid CRC is a one in 256 accident for a CRC-8 and a one in 65536 accident for a CRC-16. Over several frames it stops being an accident at all, which is what makes the check trustworthy.

A cyclic redundancy check is a short value computed over a frame and appended to it, so that a receiver can recompute it and detect that the frame arrived damaged. It is division of the message, treated as a polynomial over GF(2), by a fixed generator polynomial; the check value is the remainder. Almost every framed protocol carries one, from CAN and Modbus to ISO 14443 and every sub-GHz packet engine.

What it is

A CRC is described by five parameters, and quoting a "polynomial" alone is not enough to reproduce one:

Parameter What it decides
width how many bits the check value has, typically 8, 16 or 32
polynomial the generator, written as a hex constant with the top bit implied
init the value the register starts at, commonly 0x0000 or 0xFFFF
refin / refout whether the input bytes and the output value are bit-reflected
xorout a constant XORed into the final value

Two implementations that agree on the polynomial and disagree on the init will never agree on a single frame. This is why a catalogue of named models exists (CRC-8/AUTOSAR, CRC-16/CCITT-FALSE, CRC-32/ISO-HDLC and so on) and why naming the model matters more than naming the polynomial.

Why it matters

For the device, a CRC is error detection: it catches the bit flips a noisy channel produces, and nothing more. It is not a signature, it is not keyed, and it does not stop anyone recomputing it after modifying a frame. A protocol that trusts a CRC for integrity against an attacker has no integrity at all.

For the analyst it is something better. When you are reversing an unknown frame, every step is a hypothesis: which pulse width means a one, which end of a byte was shifted out first, where the frame starts, whether the payload was whitened. A CRC turns all of that from an argument into a test. Get the hypothesis right and the check byte agrees across every frame in the capture at once. Get it wrong and it agrees on none of them.

How you work one out

When the model is not documented, the search space is small enough to sweep:

def crc8_variants(frames, width_bits=8):
    # frames: list of (message, expected_check) pairs
    for poly in range(1 << width_bits):
        residues = set()
        for msg, expected in frames:
            crc = 0
            for byte in msg:
                crc ^= byte
                for _ in range(8):
                    crc = ((crc << 1) ^ poly) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF
            residues.add(crc ^ expected)
        if len(residues) == 1:            # one constant explains every frame
            yield poly, residues.pop()

With messages that are all the same length, init and xorout fold into a single constant, which is why the sweep above reports a polynomial and one constant rather than both. Messages of differing lengths separate them. The dedicated tool for this is CRC RevEng, which searches the parameter space properly and reports named models; it wants at least four codewords and does better when they differ in length.

Pitfalls

  • A CRC is not a MAC. Anyone who edits the frame recomputes it. If a protocol's integrity story is "it has a CRC", that is the finding.
  • The bytes the CRC covers are rarely obvious. Some cover the length byte, some do not; some cover an address field, some start after it. Getting the span wrong looks exactly like getting the polynomial wrong.
  • Reflection is a real trap. A reflected implementation of the same polynomial produces completely different check values, and cheap radio protocols shift bytes out least significant bit first often enough that you should always try both.
  • One frame is never enough. A single frame can be explained by many parameter sets. Four or more, of different lengths, is what pins the model down.
  • A valid CRC says the frame is intact, not that it is genuine. Replay is untouched by it.

Further reading