35. 길이 필드 부호 혼동(signed/unsigned)으로 인한 음수 길이 처리 (C#)
난이도 하해설 — 길이 필드 부호 혼동(signed/unsigned)으로 인한 음수 길이 처리 (C#)
난이도: 하
요약
프로토콜은 길이를 "부호 없는 16비트"로 정의했지만, 코드는 (A)에서 short(부호 있는 16비트)로 읽는다.
0x8000 이상의 길이(32768~65535)는 음수 short로 해석되고, 이 음수가 (B)의 검증과 배열 할당으로
흘러 들어가 검증을 우회하거나 예외를 던진다.
문제점
- [역직렬화/부호 혼동] 부호 없는 길이를 signed로 해석.
(short)((buf[off]<<8)|buf[off+1])는 상위 비트가 1이면 음수가 된다. 예: 바이트0xFF 0xFF→ 의도한 값 65535가 아니라-1. - [경계 검증 우회] 음수 길이가 검증을 통과. (B)의
off + len > buf.Length에서len이 음수면 좌변이 작아져 검증을 통과한다. 이어new byte[len]은 음수 크기라OverflowException(또는ArgumentOutOfRangeException)을 던진다. 신뢰 못 할 입력 한 방에 서버 스레드가 예외로 떨어진다(DoS). 최소한 "정상 거부"여야 할 입력이 예외 경로로 샌다. - [경계 검증 부재] fieldCount·off 사전 검증 부족.
buf[off++],buf[off],buf[off+1]접근 전에 남은 바이트 수를 확인하지 않아, 짧은 패킷이면IndexOutOfRangeException이 난다.
근본 원인
와이어 상의 "부호 없는" 필드를 언어의 부호 있는 타입으로 담으면서 명시적 마스킹을 하지 않았다. 바이트 → 정수 변환에서 부호 확장(sign extension)이 조용히 일어난다.
수정안
길이를 부호 없는 값으로 읽고(& 0xFFFF 또는 ushort), 모든 인덱스 접근 전에 경계를 검사한다.
검증 실패는 예외가 아니라 "잘못된 패킷" 거부로 처리한다.
public static bool TryParseFields(byte[] buf, int off, out List<byte[]> fields)
{
fields = new List<byte[]>();
if (off < 0 || off >= buf.Length) return false;
int fieldCount = buf[off++];
for (int i = 0; i < fieldCount; i++)
{
if (off + 2 > buf.Length) return false; // 길이 필드 경계
int len = ((buf[off] << 8) | buf[off + 1]) & 0xFFFF; // 부호 없는 16비트로 마스킹
off += 2;
if (len > buf.Length - off) return false; // 오버플로 없는 비교
var data = new byte[len];
Buffer.BlockCopy(buf, off, data, 0, len);
off += len;
fields.Add(data);
}
return true;
}
& 0xFFFF로 부호 확장을 차단한다.off + len > buf.Length대신len > buf.Length - off처럼 좌변을 키우지 않는 비교를 쓰면 정수 오버플로도 함께 피한다.- 실패를
bool반환(또는 결과 코드)으로 바꿔 신뢰 못 할 입력이 예외로 번지지 않게 한다.
더 나은 설계
BinaryPrimitives.ReadUInt16BigEndian(span)같은 표준 API로 엔디안·부호를 명확히 한다.Span<byte>기반 리더로 오프셋·남은 길이를 캡슐화하면 매 접근마다 경계 검사를 강제할 수 있다.- 필드 총개수·총 크기에 상한을 두어 메모리 증폭(수천 개 필드)도 방어한다.
면접 포인트
- 부호 확장이 언제 일어나는가 — 좁은 정수에서 넓은 signed로 변환할 때 최상위 비트가 부호로 해석된다.
- 경계 비교에서
a + b > limit가 왜 위험한가(오버플로) —a > limit - b로 재작성. - 신뢰 경계(untrusted input)에서 "예외로 죽기" vs "정상 거부"의 차이와 DoS 관점.
해설 — 길이 필드 부호 혼동(signed/unsigned)과 정수 변환 함정 (C++)
난이도: 하
요약
길이를 (A)에서 char로 담는다. 이는 이중으로 틀렸다. 첫째 char는 보통 8비트라 상위 바이트가 잘려
16비트 길이를 표현조차 못 한다. 둘째 (플랫폼에 따라) char가 signed면 0x80 이상 바이트가 음수가 된다.
음수·잘린 길이가 (B) 검증과 memcpy로 흘러 들어가 경계 검증을 우회하거나 거대한 복사를 유발한다.
문제점
- [역직렬화/타입 폭 부족]
char에 16비트 길이 저장.(buf[off]<<8)|buf[off+1]은 최대 65535지만char(8비트)로 캐스팅하면 상위 8비트가 통째로 버려진다. 길이 300(0x012C)이0x2C=44로 둔갑한다. - [부호 혼동/부호 확장] signed char → 음수.
char가 signed인 구현에서 0x80~0xFF 하위바이트는 음수가 된다. 이 음수가 (B)에서off + fieldLen > len비교로 간다. - [경계 검증 우회 + 부호 없는 산술의 함정]
off + fieldLen.off는size_t(부호 없음)다. 음수fieldLen이 이 식에서size_t로 암시적 변환되며 거대한 양수로 바뀐다(예: -1 → 0xFFFF...). 그 결과 비교가 참이 되어 여기서는 예외로 떨어지지만, 코드 형태가 조금만 달랐다면(예:off - fieldLen, 또는int로 비교) 검증을 우회해memcpy가 힙을 넘어 읽어 버퍼 오버리드/UAF로 이어진다. - [경계 검증 부재] 선행 접근.
buf[off++],buf[off],buf[off+1]앞에서len대비 남은 바이트를 확인하지 않아 OOB 읽기가 가능하다.
근본 원인
와이어의 "부호 없는 16비트"를 표현할 수 없는/부호가 다른 타입에 담았고, 부호 있는 값과 size_t를
섞어 비교하면서 암시적 정수 변환의 함정에 빠졌다.
수정안
정확한 폭의 부호 없는 타입(std::uint16_t)으로 읽고, 오프셋은 size_t로 통일하며, 비교는 좌변을
키우지 않는 형태로 쓴다. 매 접근 전에 경계를 검사하고 실패는 예외 대신 결과 코드로 반환한다.
#include <optional>
std::optional<std::vector<std::vector<std::uint8_t>>>
ParseFields(const std::uint8_t* buf, std::size_t len, std::size_t off)
{
std::vector<std::vector<std::uint8_t>> fields;
if (off >= len) return std::nullopt;
unsigned fieldCount = buf[off++];
for (unsigned i = 0; i < fieldCount; ++i) {
if (off + 2 > len) return std::nullopt; // 길이 필드 경계
std::uint16_t fieldLen =
static_cast<std::uint16_t>((buf[off] << 8) | buf[off + 1]); // 폭·부호 정확
off += 2;
if (fieldLen > len - off) return std::nullopt; // 오버플로 없는 비교
fields.emplace_back(buf + off, buf + off + fieldLen);
off += fieldLen;
}
return fields;
}
std::uint16_t가 폭과 부호를 모두 고정한다.len - off는 이미off <= len을 보장했으므로 안전.off + fieldLen처럼 좌변을 키우는 비교는 오버플로 위험이 있어fieldLen > len - off로 바꾼다.
더 나은 설계
- 경계 검사를 강제하는
ByteReader(포인터+남은 길이)로 캡슐화해 raw 인덱싱을 없앤다. - 필드 개수·누적 크기에 상한을 둬 메모리 증폭을 막는다.
-Wconversion/-Wsign-conversion경고를 켜서 이런 암시적 변환을 컴파일 타임에 잡는다.
면접 포인트
- signed와
size_t를 섞어 비교할 때의 암시적 변환(음수 → 거대한 양수)과 그 위험. - 부호 확장 vs 폭 잘림(truncation)의 차이를 구체적 바이트로 설명.
- 신뢰 경계에서 "죽지 않기": 예외 대신 결과 코드, 매 접근 전 경계 검사, 컴파일러 경고 활용.