← 문제로

31. 프레임 무결성 검증(체크섬/CRC) 부재

난이도 상
내 리뷰 · C#
해설 · C#

해설 — 프레임 무결성 검증(체크섬/CRC) 부재

난이도: 상

요약

TryReadFrame 은 길이 프리픽스(frameLen)와 msgType, payload 를 그대로 신뢰해 파싱한다. 프레임에 무결성 검증(체크섬/CRC)이 전혀 없어서, 전송 중 손상되거나 조작된 바이트도 "유효한 프레임"으로 통과된다. 특히 (A) 에서 frameLen 자체가 손상되면 실제 프레임 경계와 다른 위치를 "다음 프레임의 시작"으로 해석하게 되어, 이후 스트림 전체의 프레임 동기화가 깨진다(디싱크). 한 번 깨지면 이후 모든 바이트가 무의미한 msgType/payload 로 오파싱되며 스스로 복구되지 않는다.

문제점

(A) 길이/타입/페이로드 무결성 검증 부재 — 손상 프레임을 유효로 처리

  • 증상: 전송 중 몇 바이트가 뒤집히거나 중간자가 값을 바꿔도 파싱이 그대로 진행되어, 잘못된 msgType 으로 핸들러가 호출되거나 payload 필드가 오해석된 값(예: 자리수가 바뀐 골드량, 잘못된 아이템 ID)으로 처리된다.
  • 재현 조건: 정상 프레임 스트림 중 임의의 바이트 하나가 손상(전송 계층 오류, 프록시 버그, 또는 의도적 변조)됨.
  • 근본 원인: 프레임에 발신자가 계산한 체크섬/CRC 가 없고, 수신 측도 그것을 검증하는 단계가 없다. "TCP 가 체크섬을 갖고 있으니 안전하다"는 가정은 틀렸다 — TCP 체크섬은 약한 오류 검출(16비트)이며 조작에는 전혀 대응하지 못하고, 애플리케이션 레벨 조작(프록시/미들박스/공격자)은 TCP 계층 밖의 문제다.

(A)→(B) 길이 필드 손상 시 프레임 동기화 영구 붕괴

  • 증상: frameLen 이 손상되어 실제 값보다 크거나 작아지면, consumed = 4 + frameLen 만큼 버퍼를 당기는 지점이 실제 다음 프레임 시작과 어긋난다. 이후 모든 TryReadFrame 호출이 완전히 엉뚱한 바이트를 길이/타입으로 해석하며 복구되지 않는다(음수/거대 frameLen 이면 Array.Copy 인자가 잘못되어 예외/버퍼 오염까지 발생 가능).
  • 재현 조건: 길이 필드(첫 4바이트)가 포함된 손상.
  • 근본 원인: 길이 필드 자체의 신뢰성을 검증할 방법(체크섬)이 없어, 손상된 길이를 "이번 프레임의 진짜 경계"로 그대로 사용한다. 한 번 어긋나면 스트림 전체가 재동기화될 방법이 없다.

수정안 (정확한 코드)

프레임에 CRC32(또는 다른 체크섬)를 추가하고, 검증 실패 시 프레임을 버리되 바이트 단위로 다음 유효 프레임 헤더를 재탐색하는 재동기화 절차를 둔다.

using System;
using System.IO.Hashing; // Crc32 (System.IO.Hashing 패키지) — 또는 자체 CRC32 구현 사용 가능

class FrameReader
{
    private byte[] _buf = new byte[64 * 1024];
    private int _len;

    public void OnBytesReceived(byte[] data, int count)
    {
        Array.Copy(data, 0, _buf, _len, count);
        _len += count;
    }

    // 프레임 포맷: [4B frameLen][2B msgType][payload][4B crc32(msgType+payload)]
    public bool TryReadFrame(out ushort msgType, out ArraySegment<byte> payload)
    {
        msgType = 0;
        payload = default;

        while (true)
        {
            if (_len < 6) return false;

            int frameLen = BitConverter.ToInt32(_buf, 0);

            // 길이 필드 자체의 상식적 상한/하한 검증 (조작·손상 방어)
            const int MaxFrameLen = 32 * 1024;
            if (frameLen < 6 || frameLen > MaxFrameLen)
            {
                // 손상된 길이 — 이 프레임을 신뢰할 수 없으므로 헤더 시작을 1바이트 밀어
                // 다음 유효한 프레임 경계를 재탐색(resync)한다.
                ResyncOneByte();
                continue;
            }

            int total = 4 + frameLen; // frameLen 안에 crc32(4B) 포함
            if (_len < total) return false; // 아직 다 안 옴

            ushort type = BitConverter.ToUInt16(_buf, 4);
            int payloadLen = frameLen - 2 - 4; // msgType(2) + crc(4) 제외
            if (payloadLen < 0)
            {
                ResyncOneByte();
                continue;
            }

            uint expectedCrc = BitConverter.ToUInt32(_buf, 4 + 2 + payloadLen);
            uint actualCrc = Crc32.HashToUInt32(new ReadOnlySpan<byte>(_buf, 4, 2 + payloadLen));

            if (actualCrc != expectedCrc)
            {
                // 무결성 검증 실패 — 프레임 폐기 후 재동기화(다음 프레임 탐색)
                ResyncOneByte();
                continue;
            }

            msgType = type;
            payload = new ArraySegment<byte>(_buf, 6, payloadLen);

            Array.Copy(_buf, total, _buf, 0, _len - total);
            _len -= total;
            return true;
        }
    }

    // 손상 감지 시 헤더 후보 위치를 1바이트씩 밀며 재동기화를 시도한다.
    private void ResyncOneByte()
    {
        Array.Copy(_buf, 1, _buf, 0, _len - 1);
        _len -= 1;
    }
}

핵심: ① CRC 로 손상/조작을 검증해 실패 시 프레임을 버린다. ② 길이 필드 자체에 상한/하한을 둬 명백히 조작된 값으로 인한 버퍼 오염을 막는다. ③ 검증 실패 시 스트림을 끊는 대신 1바이트씩 밀며 재동기화해, 이후 정상 프레임은 계속 처리될 수 있게 한다(완전한 재동기화 보장은 아니지만 스트림 전체 마비는 막는다).

더 나은 설계 (+ 트레이드오프)

  • 매직 넘버 + CRC 조합: 프레임 시작에 고정 매직 바이트(예: 0xAB 0xCD)를 두면 재동기화 탐색이 "CRC 통과"뿐 아니라 "매직 일치"까지 요구해 오탐(우연히 CRC 가 맞는 손상 프레임)을 크게 줄인다. 대신 프레임당 2바이트 오버헤드.
  • 전송 계층에서 TLS 사용: TLS 는 AEAD 로 무결성+기밀성을 함께 보장하므로 애플리케이션 레벨 CRC 없이도 변조를 탐지(실패 시 연결 종료)할 수 있다. 다만 손상 시 "프레임 단위 폐기+재동기화"가 아니라 "연결 종료"가 기본 동작이라 스트림 복구 전략이 달라진다.
  • 끊고 재연결(fail-fast): 무결성 검증 실패를 심각한 이상 신호로 보고 연결을 즉시 끊는 전략도 있다(특히 조작이 의심되는 경우). 재동기화보다 안전하지만 일시적 전송 오류에도 재연결 비용이 발생.
  • 트레이드오프: CRC 는 계산 비용이 적고 우발적 손상에 강하지만 암호학적 위조에는 취약(공격자가 CRC 도 함께 위조 가능) — 진짜 위협 모델이 "악의적 변조"라면 HMAC/TLS 가 필요하다.

면접 포인트

  1. TCP 체크섬은 우발적 오류의 약한 검출일 뿐, 애플리케이션 레벨 손상/조작을 막지 못한다 — 프레임 자체에 무결성 검증이 필요한 이유.
  2. 길이 프리픽스 프레이밍에서 길이 필드가 손상되면 스트림 전체 동기화가 깨진다는 것과, 재동기화(resync) 전략의 필요성.
  3. CRC(무결성) vs HMAC/TLS(무결성+인증) 의 위협 모델 차이 — 우발적 손상만 막을지, 의도적 위조까지 막을지.