← 문제로

23. 배치 메시지 디코딩과 길이/개수 검증 (OOB / 메모리 증폭 / 부분 프레임)

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

해설 — 배치 메시지 디코딩과 길이/개수 검증 (OOB / 메모리 증폭 / 부분 프레임)

난이도: 중상

요약

디코더는 와이어에서 읽은 count(A)·len(C)·읽기 오프셋을 남은 바이트와 한 번도 대조하지 않는다. ReadU16(F)도 인덱스 경계를 확인하지 않는다. 따라서 (1) 큰 count 나 (2) 남은 바이트보다 큰 len 이 오면 b[o]/Array.Copy 가 버퍼 밖을 읽어 IndexOutOfRangeException/ArgumentException 으로 죽거나(요청 한 건으로 처리 스레드 다운 = DoS), (3) 공격자 제어 len 으로 매 항목 new byte[len] 을 선할당해 메모리를 증폭시킨다. 또 (4) 프레임이 덜 도착한 부분 수신을 완전한 프레임으로 오인해 마지막 항목을 잘린 채 복사한다.

문제점

1. (입력 검증) count 신뢰 — (A)

  • 증상: count=0xFFFF 인데 실제 항목은 2개뿐이면, 3번째 반복부터 버퍼 밖을 읽어 예외/크래시.
  • 근본원인: 선언된 개수를 실제 데이터 양과 대조하지 않음.

2. (입력 검증) len 신뢰 — (C)(D)(E)

  • 증상: off + len > buf.Length 인데 Array.Copy(buf, off, payload, 0, len) 가 범위를 벗어나 던진다. 버퍼 오버리드(인접 데이터 노출)나 크래시.
  • 근본원인: 길이 프리픽스를 "남은 바이트"와 비교하지 않음.

3. (메모리 증폭/DoS) 선할당 — (D)

  • 항목마다 new byte[len] 을 검증 없이 만든다. 작은 입력으로 다수의 큰 할당을 유도(할당 폭증).

4. (경계 검사 부재) ReadU16 — (F)

  • b[o], b[o+1] 접근 전에 o+2 <= buf.Length 를 확인하지 않는다. 모든 헤더 읽기가 OOB 후보.

5. (프레이밍) 부분 프레임/잔여 바이트 미처리

  • buf 가 한 프레임 전체라는 보장이 없다. 덜 오면 잘린 항목을 읽고, 더 길면(여러 프레임이 붙어 옴) 남는 바이트를 조용히 무시한다. 끝에서 off == buf.Length 인지 확인하지 않음.

수정안

모든 읽기 전에 남은 바이트를 검사하고, count·len·총량에 상한을 두며, 끝에서 정확히 소비됐는지 확인한다. 불완전하면 "더 받아야 함"으로, 위반이면 "프레임 거부"로 분리한다.

const int MaxCount = 1024;
const int MaxLen   = 16 * 1024;

public bool TryDecode(ReadOnlySpan<byte> buf, out List<Msg> result)
{
    result = null;
    int off = 0;
    if (!TryU16(buf, ref off, out ushort count)) return false; // 헤더도 다 안 옴
    if (count > MaxCount) throw new ProtocolException("count");  // 명백한 위반은 연결 차단

    var list = new List<Msg>(Math.Min(count, 64));              // 신뢰 가능한 만큼만 예약
    for (int i = 0; i < count; i++)
    {
        if (!TryU16(buf, ref off, out ushort type)) return false;
        if (!TryU16(buf, ref off, out ushort len))  return false;
        if (len > MaxLen) throw new ProtocolException("len");
        if (off + len > buf.Length) return false;               // 남은 바이트 부족 → 부분 프레임
        var payload = buf.Slice(off, len).ToArray();
        off += len;
        list.Add(new Msg { Type = type, Payload = payload });
    }
    if (off != buf.Length) throw new ProtocolException("trailing"); // 정확히 소비됐는지
    result = list;
    return true;
}

static bool TryU16(ReadOnlySpan<byte> b, ref int o, out ushort v)
{
    if (o + 2 > b.Length) { v = 0; return false; }
    v = (ushort)(b[o] | (b[o + 1] << 8));
    o += 2;
    return true;
}
  • 부분 프레임(return false)프로토콜 위반(throw → 연결 종료) 을 구분한다. 전자는 더 받으면 되고, 후자는 신뢰 불가 상대다.
  • 외곽에 프레임 길이 프리픽스([u32 frameLen][batch...])를 둬 한 프레임 경계를 먼저 확정하면 "부분 vs 완전"이 명확해진다.

더 나은 설계

  • "선언된 길이는 절대 믿지 말고 항상 남은 바이트로 검증"을 디코더의 불변식으로. 모든 읽기는 TryRead 류로 경계를 반환.
  • count·len·총 페이로드 합에 상한, 선할당은 상한 이하에서만. 큰 페이로드는 별도 청크 프로토콜로.
  • 길이 프리픽스 프레이밍 + 누적 버퍼로 부분 수신을 명시적으로 다룬다(이 저장소 protocol/problem1, problem20 과 연계).

면접 포인트

  1. 신뢰 불가 입력의 count/length 는 반드시 남은 바이트와 대조 — OOB·DoS의 단골 원인.
  2. "부분 수신(더 받기)"과 "프로토콜 위반(끊기)"은 다른 처리 — 섞으면 정상 트래픽을 끊거나 공격을 흘려보낸다.
  3. 끝에서 정확히 소비 검증으로 trailing/under-read 를 잡는다.