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 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.