29. 비트 단위 스트림 리더의 경계·폭 검증 부재 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: MMORPG 델타 스냅샷을 대역폭 절약을 위해 "비트 패킹"으로 보낸다. 각 필드는
// 바이트 경계가 아니라 비트 단위 폭으로 이어 붙는다(예: HP 는 10비트, 상태 플래그 3비트).
// 서버는 클라가 보낸 비트스트림을 BitReader 로 필드 단위로 읽어 상태를 복원한다.
//
// 와이어 포맷(공격자가 조작 가능한 입력):
// [엔티티 수: 8비트] 반복 { [필드폭 w: 5비트] [값: w 비트] }
//
// 요구사항:
// - ReadBits(n): 스트림에서 n비트를 읽어 정수로 반환하고 커서를 전진한다.
// - 파서는 선언된 개수/폭만큼 읽어 필드를 복원한다.
// - 입력은 신뢰할 수 없다(길이·개수·폭 모두 조작 가능).
//
// 과제: (A)(B) 지점을 검토하라. 특정 입력에서 버퍼 밖을 읽거나 파싱이 폭주할 수 있다.
// 어떤 입력에서 그런지 밝히고 고쳐라.
using System.Collections.Generic;
public sealed class BitReader
{
private readonly byte[] _buf;
private int _bitPos; // 현재 비트 오프셋
public BitReader(byte[] buf) { _buf = buf; _bitPos = 0; }
// n비트를 MSB-first 로 읽어 정수로 반환
public uint ReadBits(int n)
{
uint v = 0;
for (int i = 0; i < n; i++)
{
int byteIdx = _bitPos >> 3;
int bitIdx = 7 - (_bitPos & 7);
int bit = (_buf[byteIdx] >> bitIdx) & 1; // (A)
v = (v << 1) | (uint)bit;
_bitPos++;
}
return v;
}
}
public struct Field { public int Width; public uint Value; }
public static class SnapshotParser
{
public static List<Field> Parse(byte[] packet)
{
var r = new BitReader(packet);
var fields = new List<Field>();
uint count = r.ReadBits(8);
for (uint i = 0; i < count; i++)
{
int w = (int)r.ReadBits(5); // 필드 폭(0..31)
uint val = r.ReadBits(w); // (B) 선언된 폭만큼 읽기
fields.Add(new Field { Width = w, Value = val });
}
return fields;
}
} 결함 코드 · C++
// 시나리오: MMORPG 델타 스냅샷을 대역폭 절약을 위해 "비트 패킹"으로 보낸다. 각 필드는
// 바이트 경계가 아니라 비트 단위 폭으로 이어 붙는다(예: HP 10비트, 상태 플래그 3비트).
// 서버는 클라가 보낸 비트스트림을 BitReader 로 필드 단위로 읽어 상태를 복원한다.
//
// 와이어 포맷(공격자가 조작 가능한 입력):
// [엔티티 수: 8비트] 반복 { [필드폭 w: 5비트] [값: w 비트] }
//
// 요구사항:
// - readBits(n): 스트림에서 n비트를 읽어 정수로 반환하고 커서를 전진한다.
// - 파서는 선언된 개수/폭만큼 읽어 필드를 복원한다.
// - 입력은 신뢰할 수 없다(길이·개수·폭 모두 조작 가능).
//
// 과제: (A)(B) 지점을 검토하라. 특정 입력에서 버퍼 밖을 읽거나 파싱이 폭주할 수 있다.
// 어떤 입력에서 그런지 밝히고 고쳐라.
#include <cstdint>
#include <cstddef>
#include <vector>
class BitReader {
public:
BitReader(const uint8_t* buf, size_t len) : buf_(buf), len_(len), bitPos_(0) {}
// n비트를 MSB-first 로 읽어 정수로 반환
uint32_t readBits(int n) {
uint32_t v = 0;
for (int i = 0; i < n; i++) {
size_t byteIdx = bitPos_ >> 3;
int bitIdx = 7 - (int)(bitPos_ & 7);
int bit = (buf_[byteIdx] >> bitIdx) & 1; // (A)
v = (v << 1) | (uint32_t)bit;
bitPos_++;
}
return v;
}
private:
const uint8_t* buf_;
size_t len_;
size_t bitPos_;
};
struct Field { int width; uint32_t value; };
std::vector<Field> parseSnapshot(const uint8_t* packet, size_t len) {
BitReader r(packet, len);
std::vector<Field> fields;
uint32_t count = r.readBits(8);
for (uint32_t i = 0; i < count; i++) {
int w = (int)r.readBits(5); // 필드 폭(0..31)
uint32_t val = r.readBits(w); // (B) 선언된 폭만큼 읽기
fields.push_back({ w, val });
}
return fields;
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.