← 문제로

22. 메시지 봉투 타입 태그 신뢰: 핸들러 인덱스/길이 미검증과 버퍼 오버리드

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

해설 — 메시지 봉투 타입 태그 신뢰: 핸들러 인덱스/길이 미검증과 버퍼 오버리드

난이도: 상

답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계

요약

수신부가 와이어에서 받은 typelen검증 없이 신뢰한다. (A) type 을 핸들러 배열의 인덱스로 그대로 사용 → type >= 3 이면 IndexOutOfRangeException(서버 스레드 다운). (B)(C)(D) 각 핸들러는 payload 가 충분한지 확인하지 않고 고정 오프셋에서 값을 읽는다 → 버퍼가 짧으면 ArgumentOutOfRangeException 또는 (Chat 의 textLen 처럼) 인접 바이트를 읽어 길이를 부풀려 OOB 디코딩. 또한 len 이 종류가 기대하는 크기와 일치하는지 전혀 안 봐서, 짧은 payload 로 보낸 패킷이 인접 데이터를 필드 값으로 오독한다. 정답 한 줄: type 의 유효성(범위/허용목록)과 payload 길이를 읽기 전에 검증하고, 모든 필드 읽기를 경계 검사가 내장된 리더로 하라.

변별: problem4(패킷 길이/시퀀스/페이로드 검증)·problem6(RPC ID↔핸들러 매핑)·problem17 (가변길이 필드 과대 길이)와 인접하지만, 본 문제의 핵심은 타입 태그를 신뢰한 디스패치 (인덱스/핸들러 선택)와 종류별 고정 레이아웃 읽기에서의 타입 혼동·경계 미검증이다.


문제점

(A) _handlers[type] — 신뢰되지 않은 인덱스 (OOB / 타입 혼동) ★간판

  • 분류 태그: untrusted index / unvalidated dispatch.
  • 증상: type 이 0..2 밖이면 배열 인덱스 초과로 예외. enum 에 정의된 값만 와도, enum 정의 순서와 배열 순서가 어긋나면 엉뚱한 핸들러로 라우팅(타입 혼동: Move 를 Trade 로 해석).
  • 재현 조건: type = 3..65535 또는 핸들러 배열/enum 의 순서 불일치.
  • 근본 원인: 와이어 값을 허용 목록과 대조하지 않고 곧장 인덱스/디스패치 키로 사용.

(B)(C)(D) 길이 미검증 고정 오프셋 읽기 — 버퍼 오버리드 ★간판

  • 분류 태그: out-of-bounds read / missing length validation.
  • 증상: len(과 실제 buf.Length)을 보지 않고 p+8, p+12 등 고정 위치를 읽는다. payload 가 짧으면 BitConverterArgumentOutOfRangeException 을 던지거나(Move/Trade), Chat 처럼 textLen버퍼에서 읽어 그 길이만큼 GetString 하면 인접 메모리/다음 패킷 바이트를 문자열로 오버리드·누출한다.
  • 재현 조건: len 을 작게(또는 0) 보내고 헤더만 채운 패킷, textLen 을 거대하게.
  • 근본 원인: "선언된 길이"와 "실제 버퍼 길이"를 읽기 전에 대조하지 않음.

(추가) len 과 종류 기대 크기의 불일치 미검증 — 타입 혼동

  • 분류 태그: type confusion.
  • 증상: Trade 는 12바이트를 기대하는데 len=4 인 패킷도 통과를 시도한다. 길이가 맞는지 보지 않으면, 짧은/긴 payload 가 다음 필드를 다른 의미로 오독한다.
  • 근본 원인: 종류별 고정 크기(또는 최소 크기) 검증 부재.

(추가) buf 최소 길이(4바이트 헤더) 미검증

  • BitConverter.ToUInt16(buf, 2) 전에 buf.Length >= 4 를 확인하지 않으면 헤더 읽기에서부터 예외가 난다.

수정안 (정확한 코드)

읽기 전에 헤더 길이 → type 허용성 → payload 길이(종류 기대치) 순으로 검증하고, 경계 검사가 내장된 리더로 필드를 읽는다.

public bool Dispatch(byte[] buf)
{
    if (buf == null || buf.Length < 4) return false;            // 헤더 보장
    ushort type = BitConverter.ToUInt16(buf, 0);
    ushort len  = BitConverter.ToUInt16(buf, 2);

    // payload 가 실제 버퍼 안에 있는지(선언 길이 ≤ 남은 바이트)
    if (4 + len > buf.Length) return false;
    var payload = new ReadOnlySpan<byte>(buf, 4, len);

    switch ((MsgType)type)                                      // 명시적 허용 목록(switch)
    {
        case MsgType.Move:  return HandleMove(payload);
        case MsgType.Trade: return HandleTrade(payload);
        case MsgType.Chat:  return HandleChat(payload);
        default:            return false;                       // 알 수 없는 type 거부
    }
}

private bool HandleMove(ReadOnlySpan<byte> p)
{
    if (p.Length != 12) return false;                           // 종류 기대 크기 검증
    float x = BinaryPrimitives.ReadSingleLittleEndian(p);
    float y = BinaryPrimitives.ReadSingleLittleEndian(p.Slice(4));
    float z = BinaryPrimitives.ReadSingleLittleEndian(p.Slice(8));
    if (!IsFinite(x, y, z)) return false;                       // 값 정상성(NaN/Inf 방어)
    ApplyMove(x, y, z);
    return true;
}

private bool HandleChat(ReadOnlySpan<byte> p)
{
    if (p.Length < 2) return false;
    ushort textLen = BinaryPrimitives.ReadUInt16LittleEndian(p);
    if (2 + textLen != p.Length) return false;                 // 선언 길이 == 실제 남은 길이
    string text = System.Text.Encoding.UTF8.GetString(p.Slice(2, textLen));
    ApplyChat(text);
    return true;
}

핵심은 ① type 을 인덱스가 아니라 명시적 허용 목록(switch)으로 매핑(default 거부), ② 읽기 전에 길이 검증(헤더 4바이트 → 4+len ≤ buf.Length → 종류별 기대 크기), ③ 모든 읽기를 Span + BinaryPrimitives 처럼 경계·엔디안이 명시된 API로.

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

  • 선언적 스키마/코드젠: 메시지 레이아웃을 IDL(FlatBuffers/Protobuf 등)로 정의하고 파서를 생성하면 수작업 오프셋 산술과 경계 실수를 제거한다. 트레이드오프: 빌드 파이프라인· 의존성 추가.
  • 핸들러 등록을 딕셔너리로: Dictionary<MsgType, Handler> 로 두면 인덱스 OOB 가 구조적으로 불가능하고, 미등록 type 은 자연히 거부된다.
  • type↔기대 크기 표: 종류별 (최소)크기를 테이블로 관리해 디스패치 직전 한 곳에서 검증.
  • 방어적 카운팅/레이트리밋: 잘못된 패킷 비율이 높은 연결은 끊거나 감속(악성 트래픽 방어).
  • 값 정상성 검증: 좌표 NaN/Inf, 음수 길이/수량 등 도메인 불변식을 파싱 직후 검사 (protocol_version/problem14 와 연결).

면접 포인트

  1. "와이어에서 온 값으로 배열을 인덱싱/캐스팅하지 말라" — 모든 외부 입력은 검증 후 사용. type 은 허용 목록, 길이는 버퍼 경계와 종류 기대치 둘 다 대조.
  2. 타입 혼동(type confusion)이란 — 같은 바이트열을 다른 타입으로 해석하게 만드는 공격. "선언 길이 == 종류 기대 길이" 검증이 1차 방어선.
  3. 수작업 오프셋 파싱의 위험과, Span/BinaryPrimitives·스키마 코드젠으로 경계·엔디안을 구조적으로 강제하는 방법.