같은 바이트인데 CRC-16 결과가 네 가지로 갈리는 이유
CRC-16은 알고리즘 이름이 아니라 계열의 이름이고, 그 계열에 속한 구성원들은 서로 답이 다릅니다. 똑같은 바이트를 MODBUS와 CCITT-FALSE, XMODEM에 각각 넣으면 서로 공통점이 하나도 없는 16비트 값 세 개가 돌아옵니다.
지금 돌아가는 것이 어느 구성원인지는 다섯 개의 상수가 결정합니다. poly, init, refin, refout, xorout입니다. 이 중 하나만 바꿔도 출력이 통째로 달라지며, 「비슷하게라도 맞았다」는 식의 경고 신호는 전혀 남지 않습니다. 「내 CRC가 왜 장비 값과 안 맞을까」류의 질문은 대부분 여기서 끝납니다. 양쪽이 서로 다른 변형을 돌리고 있는데, 어느 쪽을 쓰는지 아무도 적어두지 않은 것입니다.
이 글의 모든 값은 macOS의 Python 3.14.7에서 파라미터화된 CRC 모델을 실행해 얻었고, 네 가지 방법으로 교차 검증했습니다. 표준 라이브러리의
zlib.crc32와binascii.crc_hqx, RevEng CRC 카탈로그에 공개된 check 값, 그리고 각 파라미터 세트에 대한 벤더 문서입니다.
1. 같은 프레임, 네 가지 CRC-16 결과
실제 Modbus RTU 요청 하나입니다. 슬레이브 01, 기능 코드 03(보유 레지스터 읽기), 시작 주소 0x0000, 개수 0x000A입니다.
01 03 00 00 00 0A
같은 6바이트를 네 가지 변형에 통과시킨 결과입니다.
| 변형 | 결과 | 전송 순서(하위 바이트 먼저) |
|---|---|---|
| CRC-16/MODBUS | 0xCDC5 | C5 CD |
| CRC-16/IBM-3740 (CCITT-FALSE) | 0x0428 | 28 04 |
| CRC-16/XMODEM | 0x0A38 | 38 0A |
| CRC-16/ARC | 0xD6C5 | C5 D6 |
네 값 사이에는 아무 연결 고리도 없습니다. 니블 패턴이 겹치지도 않고, 순서를 어떻게 바꿔도 하나가 다른 하나로 넘어가지 않습니다. 그중 둘이 전송 순서상 같은 바이트로 시작하는 것은 우연입니다.
그래서 데이터시트에 적힌 “CRC-16”이라는 문자열로는 출력이 16비트 폭이라는 것 말고 알아낼 수 있는 게 없습니다. 2진수를 출력하는 장비와 대조하면서 결과를 다른 진법으로 보고 싶다면, 진법 변환기가 자릿수 채우기를 신경 쓰지 않아도 16비트 값을 16진수와 2진수 사이에서 바꿔줍니다.
2. 다섯 파라미터는 각각 무엇을 바꾸는가
모든 변형의 밑바닥에는 같은 기계가 있습니다. 한 번에 한 비트씩 삼키는 시프트 레지스터가 있고, 최상위 비트에서 1이 떨어져 나갈 때마다 상수를 XOR로 되먹입니다. 파라미터는 레지스터에 무엇이 들어가는지와 비트가 어느 방향으로 움직이는지를 정하고, 마지막 출구에서 XOR을 한 번 더 걸지 말지도 여기서 갈립니다.
엔진 전체가 파이썬 14줄입니다. 비트 단위로 도는 구현이라 느리지만 읽기 쉽고, 이 글에 나오는 모든 값을 그대로 재현합니다.
def crc(data, width, poly, init, refin, refout, xorout):
top = 1 << (width - 1)
mask = (1 << width) - 1
reg = init
for byte in data:
if refin:
byte = int(f"{byte:08b}"[::-1], 2)
reg ^= byte << (width - 8)
for _ in range(8):
reg = ((reg << 1) ^ poly) if reg & top else (reg << 1)
reg &= mask
if refout:
reg = int(f"{reg:0{width}b}"[::-1], 2)
return reg ^ xorout
이 엔진의 출력은 파이썬 표준 라이브러리로 교차 검증할 수 있습니다. zlib.crc32와 binascii.crc_hqx가 각각 독립적인 답을 내놓고, 셋 모두 일치합니다.
zlib.crc32(b"123456789") = 0xCBF43926 engine above = 0xCBF43926 MATCH
binascii.crc_hqx(b"123456789", 0x0000) = 0x31C3 CRC-16/XMODEM = 0x31C3 MATCH
binascii.crc_hqx(b"123456789", 0xFFFF) = 0x29B1 CCITT-FALSE = 0x29B1 MATCH
poly: 0x8005와 0x1021 두 진영
다항식은 레지스터에 XOR로 되먹이는 상수입니다. 최상위 비트를 생략한 “normal” 표기를 쓰기 때문에 0x8005는 x^16 + x^15 + x^2 + 1을, 0x1021은 x^16 + x^12 + x^5 + 1을 뜻합니다. 시프트와 XOR을 반복하는 이 루프는 GF(2) 위에서의 다항식 나눗셈이고, 다항식이 제수 역할을 합니다. 레지스터는 진행 중인 나머지를 들고 있으며, 메시지의 비트 하나가 들어올 때마다 나눗셈이 한 단계씩 진행됩니다.
실무에서 만나는 CRC-16은 거의 전부 이 둘 중 하나를 씁니다. 0x8005 쪽에는 ARC와 MODBUS, USB가 있고, 0x1021 쪽에는 CCITT 계열 전체에 더해 XMODEM과 KERMIT, RFID 변형들이 붙습니다. 다항식만 알아서는 변형을 절대 특정할 수 없습니다.
init: 레지스터의 시작값
0x0000과 0xFFFF 두 값이 지배적입니다. 0에서 시작하면 0 바이트가 들어와도 레지스터가 0에 머물기 때문에 00 00 01과 01이 똑같은 CRC를 냅니다. 전송 도중 앞쪽 0이 붙거나 떨어져 나간 메시지가 검사를 통과해버리는 것입니다. 0xFFFF에서 시작하면 이 사각지대가 사라지고, Modbus와 CCITT-FALSE, USB가 모두 이 값을 쓰는 이유가 여기에 있습니다.
refin과 refout: 바이트 순서가 아니라 비트 순서
refin은 입력 바이트가 레지스터에 들어가기 전에 그 8비트를 뒤집습니다. refout은 마지막 XOR 직전에 최종 레지스터를 뒤집습니다. 데이터를 LSB부터 밀어내는 하드웨어는 이 반전을 공짜로 얻기 때문에, 반전을 쓰는 변형은 대체로 시리얼 프로토콜에서 나온 것들입니다.
바로 이 파라미터 쌍이 엔디언과 가장 자주 혼동됩니다. 층위 자체가 다른 별개의 개념이며, 둘을 갈라놓는 설명은 6절에 있습니다.
xorout: 마지막 XOR
refout 다음, 레지스터에 적용되는 마지막 단계입니다. CRC-16에서는 보통 0x0000 아니면 0xFFFF이고, CRC-32 계열은 0xFFFFFFFF를 씁니다. 틀리기 가장 만만한 파라미터인데, 여기서 어긋난 결과가 다른 어디서 어긋난 결과와 겉모습이 똑같기 때문입니다.
width: 8, 16, 32비트
폭은 검출 성능의 상한을 정합니다. 폭이 n인 CRC는 n비트 이내의 버스트 오류를 전부 잡아내고, 무작위 손상을 놓칠 확률은 대략 2^-n입니다. CRC-16은 65,536분의 1, CRC-32는 42.9억분의 1입니다.
CRC-16/XMODEM에서 출발해 파라미터를 정확히 하나씩만 바꿔가며, 표준 탐침 입력인 123456789에 대해 돌린 결과입니다.
baseline CRC-16/XMODEM = 0x31C3
init 0x0000 -> 0xFFFF (becomes CCITT-FALSE) = 0x29B1
refin/refout -> true (becomes KERMIT) = 0x2189
xorout 0x0000 -> 0xFFFF = 0xCE3C
poly 0x1021 -> 0x8005 = 0xFEE8
플래그 하나만 뒤집혀도 값은 아무 관계 없는 자리로 튑니다. CRC에는 “가깝다”라는 개념이 없습니다. 두 결과는 일치하거나, 아니면 입력이 얼마나 벌어져 있었는지에 대해 아무것도 알려주지 않습니다. 그래서 불일치를 아무리 들여다봐도 원인의 위치를 좁힐 수 없습니다. 내부 루프는 XOR과 시프트가 전부인데, 그 연산자 자체는 비트 연산 완전 가이드에서 다루고 이 글은 그 내용을 안다고 전제합니다.
3. “CCITT”는 좋은 이름이 아니다
RevEng CRC 카탈로그에는 2026년 9월 기준으로 16비트 CRC 정의가 31개 등재돼 있고, 그중 셋이 CCITT라는 이름표를 달고 있습니다. 그 셋은 서로 값이 맞지 않습니다. 아래 네 행은 다항식과 입력을 공유하는데도 서로 무관한 네 개의 결과에 도달합니다.
| 통용 명칭 | 카탈로그 명칭 | check | init | refin/refout | xorout |
|---|---|---|---|---|---|
| CCITT-FALSE | CRC-16/IBM-3740 | 0x29B1 | 0xFFFF | false | 0x0000 |
| XMODEM | CRC-16/XMODEM | 0x31C3 | 0x0000 | false | 0x0000 |
| “진짜” CCITT | CRC-16/KERMIT | 0x2189 | 0x0000 | true | 0x0000 |
| — | CRC-16/MCRF4XX | 0x6F91 | 0xFFFF | true | 0x0000 |
이름의 내력을 따라가 봐도 정리가 되지는 않습니다. 카탈로그는 CRC-16/KERMIT을 CRC-CCITT, CRC-16/CCITT-TRUE라는 별칭과 함께 올려두었고, 이쪽이 반전을 쓰는 변형입니다. 한편 반전 없이 init을 0xFFFF로 둔 구현도 CCITT라는 이름으로 널리 퍼졌고, 그래서 카탈로그가 “CCITT-FALSE”를 CRC-16/IBM-3740의 별칭으로 기록해 둔 것입니다. XMODEM은 그 사이에 있습니다. 다항식은 같고, 반전은 없고, init은 0입니다.
그러니 데이터시트에 CCITT라고 적혀 있다면, 거기서 확정되는 것은 다항식이 0x1021이라는 사실뿐입니다. 후보는 여전히 넷이고, 잘못 고른 값도 맞는 값 못지않게 그럴듯해 보입니다.
이름 말고 파라미터를 적으십시오. 프로토콜 문서에 poly=0x1021, init=0xFFFF, refin=false, refout=false, xorout=0x0000이라고 한 줄 써두면 이 문제는 다시 돌아오지 않습니다. “CRC-16/CCITT”라는 표기는 그 한 줄을 대신하지 못합니다.
4. 지금 쓰는 CRC-16이 어느 변형인지 알아내는 법
카탈로그는 이 문제를 지문으로 해결합니다. 모든 항목이 check 값을 공개합니다. ASCII 9바이트 123456789의 CRC입니다. 눈앞의 구현에 그 문자열을 통과시킨 뒤 결과를 표에서 찾으면 됩니다.
| 변형 | check | poly | init | refin | refout | xorout |
|---|---|---|---|---|---|---|
| CRC-16/ARC (IBM/LHA) | 0xBB3D | 0x8005 | 0x0000 | true | true | 0x0000 |
| CRC-16/MODBUS | 0x4B37 | 0x8005 | 0xFFFF | true | true | 0x0000 |
| CRC-16/USB | 0xB4C8 | 0x8005 | 0xFFFF | true | true | 0xFFFF |
| CRC-16/IBM-3740 (CCITT-FALSE) | 0x29B1 | 0x1021 | 0xFFFF | false | false | 0x0000 |
| CRC-16/XMODEM | 0x31C3 | 0x1021 | 0x0000 | false | false | 0x0000 |
| CRC-16/KERMIT (진짜 CCITT) | 0x2189 | 0x1021 | 0x0000 | true | true | 0x0000 |
| CRC-16/GENIBUS | 0xD64E | 0x1021 | 0xFFFF | false | false | 0xFFFF |
| CRC-16/MCRF4XX | 0x6F91 | 0x1021 | 0xFFFF | true | true | 0x0000 |
| CRC-32/ISO-HDLC (zip, PNG, zlib) | 0xCBF43926 | 0x04C11DB7 | 0xFFFFFFFF | true | true | 0xFFFFFFFF |
| CRC-32/BZIP2 | 0xFC891918 | 0x04C11DB7 | 0xFFFFFFFF | false | false | 0xFFFFFFFF |
| CRC-32C (Castagnoli, iSCSI) | 0xE3069283 | 0x1EDC6F41 | 0xFFFFFFFF | true | true | 0xFFFFFFFF |
| CRC-8/SMBUS | 0xF4 | 0x07 | 0x00 | false | false | 0x00 |
| CRC-8/MAXIM-DOW (1-Wire) | 0xA1 | 0x31 | 0x00 | true | true | 0x00 |
이 대조는 의외로 엉뚱한 데서 자주 무너집니다. 123456789는 31 32 33 34 35 36 37 38 39 이 9바이트를 뜻하지, 숫자 123456789도 아니고 널로 끝나는 문자열도 아닙니다. 쓰는 언어가 함수에 와이드 문자열을 넘기거나 종단 문자를 덧붙인다면 다른 입력을 해싱하는 것이고, 표의 모든 행이 빗나갑니다. 비-ASCII 페이로드에는 함정이 하나 더 있습니다. 같은 텍스트라도 UTF-8로 인코딩한 것과 Windows-1252로 인코딩한 것은 서로 다른 바이트 열이고, 따라서 CRC도 서로 다릅니다. ASCII 코드표와 변환기는 각 문자 뒤의 바이트를 보여주므로 이 문제는 몇 초면 정리됩니다.
check 값이 어느 행과도 맞지 않는다면, 커스텀 다항식을 의심하기 전에 시시한 원인부터 지우십시오.
- 전송 순서에 놓인 결과를 거꾸로 읽고 있을 수 있습니다.
- 송신 측이 주소 바이트나 길이 바이트를, 혹은 CRC 필드 자체를 계산 범위에 넣었거나 뺐을 수 있습니다.
init이 벤더 고유값일 수 있습니다.0x0000도0xFFFF도 아닌 값이 독자 규격 계량기 프로토콜에서 실제로 나타나고, 대개 각주에 파묻혀 있습니다.
파라미터를 전수 조사로 찾아내기
벤더가 알려주지 않고 펌웨어도 읽을 수 없다면, 대신 이것 하나만 요청하십시오. 캡처한 프레임 하나와 그 프레임에 대해 장비가 만들어낸 CRC입니다. 그다음에는 열거하면 됩니다. 다항식 둘, init 값 둘, refin과 refout 각각 독립적으로, xorout 값 둘이면 조합은 32가지이고, 이 정도는 아무것도 아닙니다.
target = 0x4B37 # 그쪽 구현이 반환한 값
for poly in (0x8005, 0x1021):
for init in (0x0000, 0xFFFF):
for refin in (False, True):
for refout in (False, True):
for xorout in (0x0000, 0xFFFF):
if crc(b"123456789", 16, poly, init, refin, refout, xorout) == target:
print(f"poly=0x{poly:04X} init=0x{init:04X} "
f"refin={refin} refout={refout} xorout=0x{xorout:04X}")
poly=0x8005 init=0xFFFF refin=True refout=True xorout=0x0000
하나가 걸리고, 그것이 CRC-16/MODBUS입니다. 123456789 대신 실제 프레임을 넣으면, 정상 CRC를 알고 있는 어떤 메시지에 대해서도 같은 32가지 훑기가 통합니다. 표본 하나로 여러 조합이 살아남으면 두 번째 프레임을 돌려 교집합을 취하십시오.
5. 실무에서의 Modbus RTU
Modbus RTU는 CRC-16/MODBUS를 씁니다. 다항식 0x8005, init 0xFFFF, 입출력 모두 반전, 마지막 XOR 없음입니다. 앞서 본 6바이트 요청의 CRC는 0xCDC5이고, 완성된 프레임은 이렇습니다.
01 03 00 00 00 0A C5 CD
전송 순서는 값을 적는 순서와 반대다
값은 0xCDC5입니다. 프레임에 덧붙는 바이트는 C5 CD입니다. Modbus는 CRC의 하위 바이트를 먼저 보내도록 규정하는데, 이는 16진수를 읽는 순서와 정반대라 늘 사람을 걸려 넘어뜨립니다. 같은 프레임의 다른 다중 바이트 필드는, 저 00 0A 레지스터 개수를 포함해 전부 빅엔디언입니다. CRC만 예외입니다.
여기에는 쓸모 있는 부작용이 하나 있습니다. CRC 바이트까지 포함해 프레임 전체에 CRC-16/MODBUS를 돌리면 결과는 이렇게 됩니다.
0x0000
이것이 수신 측 검증 루틴의 전부입니다. 끝의 두 바이트를 떼어내 바이트 순서를 뒤집고 로컬에서 계산한 값과 비교할 필요가 없습니다. 도착한 것 전부에 CRC를 돌리고 0인지만 확인하면 되고, 이렇게 하면 바이트 순서를 잘못 뒤집을 자리 자체가 없어집니다.
Modbus 문서에 0xA001이 나오는 이유
아무 Modbus 구현이나 열어보면 소스의 상수는 0x8005가 아니라 0xA001입니다. 둘 다 맞습니다. 0xA001은 0x8005의 16비트를 뒤집은 값이고, 레지스터가 왼쪽이 아니라 오른쪽으로 시프트하며 입력 바이트를 따로 뒤집을 필요가 없는 반전형 알고리즘에 속합니다. 두 구현의 출력은 동일하고, 내부만 다릅니다. 소스에 보이는 0xA001은 반전형 CRC-16 구현임을 알려주는 믿을 만한 표식입니다.
캡처를 볼 때는 프레이밍부터 확인해야 합니다. Modbus ASCII는 별개의 프레이밍으로, 각 바이트를 16진수 문자 두 개로 실어 앞에는 3A(:)를, 뒤에는 0D 0A를 붙이며, CRC가 아니라 LRC를 씁니다. 트레이스에 01 03이 있어야 할 자리에 3A 30 31 30 33이 보인다면 ASCII 모드에 들어와 있는 것이고, 어떤 CRC 변형도 영영 맞지 않습니다. ASCII 코드표를 쓰면 그 바이트들이 곧바로 문자로 되돌아옵니다.
C로 쓴 같은 알고리즘
Modbus 펌웨어가 위의 파이썬 모델을 쓰는 일은 거의 없습니다. 반전형을 바로 씁니다. 오른쪽으로 시프트하면서 0xA001과 XOR합니다.
uint16_t crc16_modbus(const uint8_t *buf, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= buf[i];
for (int b = 0; b < 8; b++)
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
return crc;
}
프레임 01 03 00 00 00 0A을 넣으면 이 함수는 0xCDC5를 반환합니다. 파이썬 엔진이 내놓은 값과 같습니다. check 문자열 123456789에 대해서는 0x4B37이 나옵니다. 둘 다 Apple clang 21에서 실행했고 4절의 표와 비트 단위로 일치합니다.
6. refin/refout은 엔디언이 아니다
이 혼동은 특히 Modbus에서 대가가 큽니다. 비트 반전과 바이트 순서 뒤집기가 한 프레임 안에서 동시에 벌어지기 때문입니다.
엔디언은 다중 바이트 값 안에서 바이트가 어떻게 놓이는지에 대한 이야기입니다. 0xCDC5를 CD C5로 저장하고 전송하느냐, C5 CD로 하느냐입니다. 바이트 내부에서는 아무것도 움직이지 않습니다. 그 층위 이야기는 빅엔디언과 리틀엔디언에 정리돼 있습니다.
반전이 건드리는 것은 바이트 하나 안의 비트 순서입니다. refin은 레지스터가 바이트를 보기 전에 그 8비트를 뒤집습니다. 0번 비트가 7번 비트가 됩니다. 바이트 자체는 메시지 안에서 제자리를 지킵니다. 어느 쪽 설정도 다른 쪽에 영향을 주지 않습니다.
Modbus 프레임 안에서는 둘 다, 서로 독립적으로 일어나고 있습니다.
- 알고리즘 내부에서는
refin과refout이 true이므로 바이트 안의 비트가 뒤집힙니다. - 전송 단계에서는 완성된 CRC가 하위 바이트부터 덧붙습니다. 이는 프로토콜이 정한 바이트 순서 결정입니다.
바이트 순서 문제를 “고치겠다”고 반전을 꺼버리면 아예 다른 변형이 되고, 반전 불일치를 보상하겠다고 프레임 끝의 바이트를 맞바꾸면 새로운 방식으로 틀린 값이 나옵니다. 한 번에 하나씩 진단하십시오. 알고리즘만 놓고 check 값을 맞춘 다음에 프레임 배치를 손보면 됩니다.
7. CRC-32와 CRC-16, 어느 쪽을 써야 하나?
CRC-32도 CRC-16과 똑같은 문제를 안고 있습니다. 다만 덜 드러날 뿐인데, 한 변형이 워낙 압도적이라 다른 것들이 있다는 사실 자체를 모르고 지나가는 개발자가 대부분이기 때문입니다.
| 변형 | check | poly | refin/refout |
|---|---|---|---|
| CRC-32/ISO-HDLC | 0xCBF43926 | 0x04C11DB7 | true |
| CRC-32/BZIP2 | 0xFC891918 | 0x04C11DB7 | false |
| CRC-32C (Castagnoli) | 0xE3069283 | 0x1EDC6F41 | true |
ISO-HDLC가 바로 zlib.crc32가 계산하는 것이고, 웹 쪽 스택이라면 어디서든 마주칩니다. Zip은 이것을 항목별로 중앙 디렉터리에 저장하고, 모든 PNG 청크는 청크 타입과 데이터를 덮는 CRC로 끝납니다. 이더넷의 프레임 검사 시퀀스도 서버가 보내는 모든 프레임 끝에서 같은 파라미터 세트를 씁니다. BZIP2는 같은 다항식에서 반전만 꺼둔 것이고, 그 결과로 ISO-HDLC와 공통점이 전혀 없는 값이 나옵니다.
CRC-16도 같은 동네에 등장합니다. Redis Cluster는 CRC16(key) mod 16384로 키를 16,384개 슬롯 중 하나에 배정하며, 같은 해시 태그를 가진 키가 같은 노드에 모이는 이유가 여기에 있습니다.
CRC-32C가 하드웨어의 낙점을 받은 이유
CRC-32C는 다른 다항식을 쓰고, 스토리지와 네트워크 프로토콜이 주고받는 짧은 블록에서 오류 검출 성능이 더 좋습니다. 그 덕에 iSCSI와 ext4 메타데이터 체크섬, SCTP, Btrfs를 차지했습니다. 그다음에는 인텔이 이것을 실리콘에 넣었습니다. SSE4.2의 crc32 명령이 CRC-32C를 바로 계산해주므로, 요즘 x86에서는 사실상 공짜인 무결성 검사가 됩니다. 호환성 제약 없이 새 코드에 쓸 CRC를 지금 정한다면 이것을 쓰십시오.
이들 중 어느 것도 내려받은 파일을 검증하는 해시는 아닙니다. 파일이 온전히 도착했는지 확인할 체크섬이 필요하다면 MD5 해시 생성기가 익숙한 도구이고, 어떤 다이제스트를 어디까지 믿을지는 MD5와 SHA-256 비교에서 다룹니다. CRC는 “이게 손상됐나”에 답하고, 암호학적 해시는 “이게 내가 기대한 바로 그 내용인가”에 답합니다.
8. CRC는 변조를 막지 못한다
아래 두 JSON 메시지는 의미가 정반대인데 CRC-32는 같습니다.
message A = b'{"to":"alice","amount":100} H\xf6\xc0E'
message B = b'{"to":"mallory","amount":999}\xb2\xb2\xc2\xc2'
CRC-32(A) = 0x12345678
CRC-32(B) = 0x12345678
같은 둘에 대한 SHA-256입니다.
A -> a83b90f5e3f21dd32039e0e693a8b15864eda83e258c115deeee50a60c09ae23
B -> f9319ee0507d719c7260535dff33df721ddd9c0e1857690f3d8e02f92601b742
여기서 전수 탐색을 한 것은 아무것도 없습니다. 끝의 바이트들은 대수적으로 풀어낸 값입니다. CRC는 선형 함수이고, 메시지에 4바이트를 덧붙이는 연산은 CRC-32 출력 위에서 전단사로 대응합니다. 그래서 어떤 메시지와 어떤 목표값을 주어도 거기에 도달하게 하는 4바이트 접미사가 정확히 하나 존재하며, 그것을 찾는 일은 산수입니다.
풀어낸 4바이트 46 C9 6E 0B을 hello world 뒤에 붙이면 이렇게 됩니다.
CRC-32(b'hello world' + 46 C9 6E 0B) = 0xDEADBEEF target 0xDEADBEEF MATCH
따라서 페이로드를 고칠 수 있는 공격자는 아무것도 탐색하지 않고 상수 시간에 CRC까지 다시 맞춰 놓습니다. 메시지 앞에 비밀값을 붙여도 구제되지 않습니다. 길이를 유지하는 수정이라면 CRC에 가할 보정값이 바뀐 비트에만 의존하므로, 비밀값을 전혀 몰라도 계산할 수 있습니다. WEP를 무너뜨린 공격이 바로 이 형태였습니다.
CRC가 방어하는 대상은 전송 잡음이고, 잡음은 무작위이며 목적이 없습니다. 공격자는 원하는 결과를 노리고 비트를 고르므로, 그 앞에서 CRC는 아무 역할도 하지 못합니다. 무결성이 아니라 진위 확인이 필요한 상황이라면 키를 쓰는 구성이 필요합니다. HMAC 생성기는 임의의 페이로드와 키로 그것을 만들어주고, 웹훅 서명 검증이 실패하는 이유는 실제 엔드포인트에 붙일 때 사람들이 걸려 넘어지는 지점들을 짚어줍니다.
9. FAQ
CRC-16/CCITT는 CRC-16/CCITT-FALSE와 같은 것인가요?
아닙니다, CRC-16/CCITT-FALSE와 CRC-16/CCITT는 서로 다른 변형입니다. CCITT-FALSE는 CRC-16/IBM-3740입니다. init 0xFFFF, 반전 없음, check 값 0x29B1입니다. 그냥 CCITT라고 할 때 보통 가리키는 변형은 CRC-16/KERMIT으로, init 0x0000에 반전을 쓰고 check는 0x2189입니다. 둘은 다항식 0x1021만 공유할 뿐 나머지는 전부 다릅니다.
온라인 계산기 결과가 제 장비와 다른 이유는 무엇인가요?
거의 항상 파라미터가 달라서입니다. 도구는 어느 한 변형을 기본값으로 쓰고 장비는 다른 변형을 구현한 것입니다. 양쪽에 ASCII 바이트 123456789를 통과시켜 나온 check 값 두 개를 4절의 표와 대조해보면, 어긋난 파라미터는 대개 1분 안에 스스로 드러납니다.
Modbus는 왜 CRC의 하위 바이트를 먼저 보내나요?
규격이 그렇게 정해두었기 때문이고, 프레임 안에서 그렇게 동작하는 필드는 이것뿐입니다. 레지스터 주소와 개수는 빅엔디언이지만 끝에 붙는 CRC는 아닙니다. 그 두 바이트까지 포함한 프레임 전체에 CRC-16/MODBUS를 돌려서 0x0000인지 확인하십시오. 직접 순서를 바꿔가며 비교할 필요가 없습니다.
refin과 refout은 정확히 무엇을 뒤집나요?
바이트 안의 비트이고, 메시지 안의 바이트는 절대 아닙니다. refin은 처리 전에 각 입력 바이트의 8비트를 뒤집고, refout은 마지막 XOR 직전에 최종 레지스터를 뒤집습니다. 둘 다 엔디언과는 무관합니다. 엔디언은 다중 바이트 값이 전송 단계에서 어떻게 배치되는지를 정하는 개념입니다.
CRC-16과 CRC-32 중 무엇을 써야 하나요?
CRC-16은 무작위 손상을 대략 65,536번에 한 번, CRC-32는 42.9억 번에 한 번 놓칩니다. 수십 바이트 정도의 짧은 시리얼 프레임이라면 CRC-16으로 충분하고, 어차피 프로토콜이 그렇게 규정하는 경우가 대부분입니다. 파일과 네트워크 프레임, 그리고 몇 킬로바이트를 넘는 대상에는 CRC-32를 쓰십시오.
CRC를 API 서명으로 쓸 수 있나요?
아닙니다, CRC는 서명으로 쓸 수 없습니다. CRC는 선형이므로 페이로드를 바꾼 사람이 맞는 값을 다시 계산해낼 수 있고, 고른 4바이트를 덧붙이면 원하는 어떤 CRC-32 목표값에도 도달합니다. 서명에는 비밀 키와 비선형 구성이 필요합니다. SHA-256을 쓰는 HMAC을 사용하세요.
CRC 값에서 원본 데이터를 복원할 수 있나요?
아닙니다, CRC 값만으로는 원본을 되돌릴 수 없습니다. CRC-16은 어떤 입력이든 16비트로 압축하므로 값 하나마다 무한히 많은 메시지가 몰려 있습니다. 다만 반대 방향은 공격자에게 여전히 쓸모가 있습니다. 목표값이 주어지면 그 값을 내는 메시지를 만들어낼 수 있고, CRC가 아무것도 인증하지 못하는 이유가 바로 이것입니다.
벤더 문서에 “CRC-16”이라고만 적혀 있습니다. 파라미터를 어떻게 특정하나요?
캡처한 프레임 하나와 그에 대해 장비가 계산한 CRC를 요청한 다음, 그 쌍에 대해 4절의 32가지 조합 훑기를 돌리십시오. 파라미터 세트가 둘 이상 살아남으면 두 번째 프레임으로 반복해 교집합만 남기면 됩니다. 표본 둘이면 거의 언제나 결판이 납니다.