4. 게임플레이 패킷 검증 (길이/시퀀스/페이로드)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 게임플레이 패킷 검증 (길이/시퀀스/페이로드)
// ----------------------------------------------------------------------------
// 시나리오:
// - 인증을 마친 세션이 실시간 게임플레이 패킷을 주고받는다.
// - 와이어 포맷: [ushort totalLen][ushort packetId][uint seq][payload...]
// * totalLen 은 헤더(8B) + payload 를 포함한 전체 길이.
// * seq 는 송신 측이 1씩 증가시키는 시퀀스 번호(중복/재전송 탐지용).
// * 모든 정수는 little-endian 으로 합의되어 있다.
// - 예시로 C_UseItem(아이템 사용) 패킷을 처리한다:
// payload = [uint itemSlot][ushort count]
// - 클라이언트는 신뢰할 수 없다(치터/프록시/패킷 변조 가능).
//
// 요구사항:
// - 변조/재전송/길이 위조에 견뎌야 하고, 어떤 입력이 와도 서버가 죽거나
// 상태가 깨지면 안 된다.
// - 같은 행동이 두 번 처리되면(재전송 공격) 아이템 복제 등 경제 사고가 난다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용되는지 설명하고,
// 수정안과 더 나은 검증 설계를 제시하라.
// ============================================================================
using System;
public class Inventory
{
public int[] slots = new int[64]; // 슬롯별 보유 수량
public void Consume(uint slot, ushort count) { slots[slot] -= count; }
}
public class Session
{
public Inventory inv = new Inventory();
public uint lastSeq = 0; // 마지막으로 본 시퀀스
public void Disconnect() { /* 세션 종료 */ }
}
public class GameplayHandler
{
private const int HeaderSize = 8; // ushort + ushort + uint
// recvBuf[0..recvLen) : 길이 프리픽스 단계에서 이미 한 패킷 분량으로 잘라 넘겨졌다고 가정
public void OnPacket(Session s, byte[] recvBuf, int recvLen)
{
// (A) 헤더 해석
ushort totalLen = BitConverter.ToUInt16(recvBuf, 0);
ushort packetId = BitConverter.ToUInt16(recvBuf, 2);
uint seq = BitConverter.ToUInt32(recvBuf, 4);
// (B) 시퀀스 검사: 과거 시퀀스면 무시
if (seq < s.lastSeq)
return;
s.lastSeq = seq;
// payload 시작 위치와 길이
int payloadOffset = HeaderSize;
int payloadLen = totalLen - HeaderSize; // (C)
switch (packetId)
{
case 1001: // C_UseItem
HandleUseItem(s, recvBuf, payloadOffset, payloadLen);
break;
default:
break;
}
}
private void HandleUseItem(Session s, byte[] buf, int offset, int payloadLen)
{
// (D) payload 를 필드로 해석해 바로 사용
uint itemSlot = BitConverter.ToUInt32(buf, offset);
ushort count = BitConverter.ToUInt16(buf, offset + 4);
// (E) 아이템 소비
s.inv.Consume(itemSlot, count);
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 게임플레이 패킷 검증 (길이/시퀀스/페이로드)
// ----------------------------------------------------------------------------
// 시나리오:
// - 인증을 마친 세션이 실시간 게임플레이 패킷을 주고받는다.
// - 와이어 포맷: [uint16 totalLen][uint16 packetId][uint32 seq][payload...]
// * totalLen 은 헤더(8B) + payload 를 포함한 전체 길이.
// * seq 는 송신 측이 1씩 증가시키는 시퀀스 번호(중복/재전송 탐지용).
// - 예시로 C_UseItem(아이템 사용) 패킷을 처리한다:
// payload = [uint32 itemSlot][uint16 count]
// - 클라이언트는 신뢰할 수 없다(치터/프록시/패킷 변조 가능).
//
// 요구사항:
// - 변조/재전송/길이 위조에 견뎌야 하고, 어떤 입력이 와도 서버 메모리가 깨지면 안 된다.
// - 같은 행동이 두 번 처리되면(재전송 공격) 아이템 복제 등 경제 사고가 난다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용되는지 설명하고,
// 수정안과 더 나은 검증 설계를 제시하라.
// ============================================================================
#include <cstdint>
#include <cstring>
#include <vector>
#pragma pack(push, 1)
struct PacketHeader {
uint16_t totalLen; // 헤더+payload 전체 길이
uint16_t packetId;
uint32_t seq;
};
struct C_UseItem { // payload
uint32_t itemSlot;
uint16_t count;
};
#pragma pack(pop)
struct Inventory {
int slots[64] = {0}; // 슬롯별 보유 수량
void Consume(uint32_t slot, uint16_t count) { slots[slot] -= count; }
};
struct Session {
Inventory inv;
uint32_t lastSeq = 0; // 마지막으로 본 시퀀스
};
class GameplayHandler {
public:
// recvBuf[0..recvLen) : 길이 프리픽스 단계에서 이미 한 패킷 분량으로 잘라 넘겨졌다고 가정
void OnPacket(Session& s, const uint8_t* recvBuf, size_t recvLen)
{
// (A) 헤더 해석
const PacketHeader* h = reinterpret_cast<const PacketHeader*>(recvBuf);
// (B) 시퀀스 검사: 과거 시퀀스면 무시
if (h->seq < s.lastSeq)
return;
s.lastSeq = h->seq;
// payload 시작 위치와 길이
const uint8_t* payload = recvBuf + sizeof(PacketHeader);
size_t payloadLen = h->totalLen - sizeof(PacketHeader); // (C)
switch (h->packetId) {
case 1001: // C_UseItem
HandleUseItem(s, payload, payloadLen);
break;
default:
break;
}
}
private:
void HandleUseItem(Session& s, const uint8_t* payload, size_t payloadLen)
{
// (D) payload 를 구조체로 해석해 바로 사용
const C_UseItem* req = reinterpret_cast<const C_UseItem*>(payload);
// (E) 아이템 소비
s.inv.Consume(req->itemSlot, req->count);
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.