← 문제로

28. 존재 비트맵(presence bitmap) 역직렬화의 경계·예약 비트 검증 결함 (C#)

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

해설 — 존재 비트맵(presence bitmap) 역직렬화의 경계·예약 비트 검증 결함 (C#)

난이도: 중

요약

켜진 비트마다 고정 크기 필드를 읽는데, (1) 각 필드를 읽기 전에 버퍼 길이와 대조하지 않아 BitConverter/인덱서가 버퍼 밖을 건드리고, (2) 미정의/예약 비트(5..15)가 켜져도 거부하지 않아 오프셋이 전진하지 않고 파싱이 오염된다. 비트맵 2바이트 읽기도 길이 검사가 없다.

문제점

  • [경계 미검증 → 예외 DoS] (A) ApplyField(ref outState, i, buf, cur, sz);
    • 증상: cur + sz > buf.Length 여도 호출 → BitConverter.ToInt32/인덱서가 ArgumentOutOfRangeException/IndexOutOfRangeException 을 던진다. C++ 와 달리 메모리 누출은 없지만, 신뢰 불가 입력 한 줄로 수신 루프가 예외로 죽는 DoS. catch로 연결만 끊어도 매 패킷 예외 비용이 크다.
    • 재현: buf = { 0x1F, 0x00 }(비트 0..4) + 페이로드 0바이트 → 15바이트를 읽으려다 예외.
    • 근본원인: 매 필드 off + sz <= buf.Length 경계 검증 부재, 비트맵 buf.Length >= 2 미검증.
  • [예약/미정의 비트 미검증] (B) int sz = FieldSize[i];
    • 증상: 5..15 비트가 켜지면 sz==0, ApplyFielddefault: 는 아무것도 안 하고 cur += 0 이라 오프셋이 전진하지 않는다. 같은 바이트가 다음 켜진 필드로 중복 해석되고, 미래에 그 슬롯이 정의되면 와이어 포맷이 조용히 깨진다. 미정의 필드는 길이를 몰라 건너뛸 수도 없다.
    • 근본원인: 알려진 비트만 허용(mask & ~Known) == 0)하는 검증 부재.

수정안

길이 검증 → 미정의 비트 거부 → 매 필드 경계 검증 → 잔여 바이트 정책.

public bool Parse(byte[] buf, out EntityState outState)
{
    outState = default;
    if (buf == null || buf.Length < 2) return false;       // 비트맵 검증
    int cur = 0;
    ushort mask = (ushort)(buf[0] | (buf[1] << 8));
    cur += 2;

    const ushort Known = 0x001F;                            // 비트 0..4만 정의
    if ((mask & ~Known) != 0) return false;                 // (B) 예약 비트 거부

    for (int i = 0; i < Fields; i++)
    {
        if ((mask & (1 << i)) != 0)
        {
            int sz = FieldSize[i];
            if (sz == 0 || cur + sz > buf.Length) return false; // (A) 경계 검증
            ApplyField(ref outState, i, buf, cur, sz);
            cur += sz;
        }
    }
    return cur == buf.Length;                               // 잔여 바이트 거부(정책)
}

ReadOnlySpan<byte> + BinaryPrimitives.ReadInt32LittleEndian(span.Slice(cur, sz)) 를 쓰면 슬라이스가 경계를 강제하고 할당 없이 안전하다.

더 나은 설계

  • Span/BinaryPrimitives 로 reader 일원화: 모든 읽기를 span.Slice(cur, n) 로 통과시키면 경계 검증이 슬라이스에 내장돼 누락이 사라진다. 엔디안도 명시적.
  • TLV 로 전방 호환: 미정의 필드를 스킵하려면 필드마다 길이를 붙인다(protocol_version/problem25). 비트맵+고정크기는 콤팩트하나 확장 시 버전 협상이 필수.
  • 예외를 흐름 제어로 쓰지 않기: 파서는 불리언/Result 로 거부를 표현. 신뢰 불가 입력마다 예외를 던지면 GC·스택 언와인드 비용이 공격 표면이 된다.
  • 잔여 바이트 엄격 검증: 보안 경계에선 cur==Length 로 패딩/변조 거부.

면접 포인트

  • 관리 언어에서 OOB가 "메모리 안전하지만 예외 DoS"로 바뀌는 점 — C++ 의 UB와 대비.
  • "알 수 없는 비트 무시"가 왜 프레임 디싱크로 이어지는가(미정의 필드 길이 불명).
  • Span/BinaryPrimitives 로 경계·엔디안을 구조적으로 강제하는 패턴.