본 내용은 게임 클라이언트의 네트워크 통신 구조를 역분석하고, 이를 바탕으로 사용자 정의 채팅 메시지를 서버에 전송하는 기능을 C++로 구현하는 과정을 다룹니다. 분석 대상은 특정 온라인 게임의 클라이언트이며, 주요 목표는 채팅 패킷의 형식을 해독하고, 올바른 구조의 데이터를 생성하여 서버와 정상적으로 통신하는 것입니다.
패킷 분석을 위해 디버거를 사용해 게임 내에서 채팅을 입력할 때 발생하는 네트워크 요청을 포착했습니다. 여러 번의 테스트를 통해 다음과 같은 공통적인 패킷 구조를 추출할 수 있었습니다:
- 첫 번째 바이트: 채널 타입 식별자 (예: 0x01 = 일반 채팅, 0x05 = 길드 채팅)
- 두 번째 바이트: 메시지 시퀀스 번호 (전송 시마다 증가)
- 세 번째와 네 번째 바이트: 리틀엔디안 방식의 16비트 길이 필드
- 다음 바이트부터 실제 문자열 데이터
예를 들어, "123456789"라는 메시지를 보낼 경우, 패킷은 01 01 00 09 00 31 32 33 34 35 36 37 38 39 형태로 나타났으며, 여기서 0x09(=9)는 이후의 문자열 길이와 일치함을 확인했습니다. 이로부터 데이터 길이 필드가 유동적임을 알 수 있었고, 인코딩은 ASCII 기반임을 추론할 수 있었습니다.
또한, 시퀀스 번호(두 번째 바이트)는 클라이언트 측에서 각 메시지에 부여하는 고유 번호로, 서버는 해당 번호를 이용해 어떤 메시지가 성공적으로 처리되었는지 응답합니다. 이를 통해 클라이언트는 로컬에 저장된 메시지를 재출력하며, 불필요한 중복 전송을 방지합니다. 이는 네트워크 트래픽 최적화를 위한 설계로 볼 수 있습니다.
특정 채널(예: 길드 채팅)에서의 전송 테스트 결과, 첫 번째 바이트 값이 0x05로 변경되는 것을 확인했으며, 이는 채널 구분을 위한 주요 필드임을 입증합니다. 반면, 개인 메시지(PM)는 완전히 다른 구조를 가지며, 수신자 이름 길이, 수신자 닉네임, 본문 길이, 본문 데이터 순으로 구성되어 추가적인 파싱 로직이 필요합니다.
C++ 코드 구현 단계에서는 기존 게임 엔진의 SRO_Net 모듈을 활용해 패킷을 조합하고 전송했습니다. 핵심 구조체는 다음과 같이 정의되었습니다:
struct ChatPacket {
uint8_t channel; // 채널 타입
uint8_t sequence; // 시퀀스 번호
uint16_t textLength; // 텍스트 길이 (리틀엔디언)
char message[80]; // 메시지 본문
};
전송 함수에서는 이 구조체를 기반으로 CreateNetData를 호출하고, 정확한 오프셋과 크기를 지정하여 데이터를 직렬화했습니다. 초기 구현에서 게임 충돌이 발생했으나, 디버깅을 위해 MessageBoxA를 삽입하고 x96dbg에서 반환 주소를 추적함으로써 오류 위치를 식별했습니다. 문제는 잘못된 메모리 접근과 불일치하는 구조체 크기에서 비롯되었으며, extern_all.cpp 내의 유틸리티 함수를 아래와 같이 수정해 해결했습니다:
void InitClassProc(void* target, unsigned value) {
*reinterpret_cast<unsigned*>(target) = value;
}
void InitClassProc(void* target, unsigned* vtable, size_t index) {
InitClassProc(target, vtable[index]);
}
최종적으로 UI 버튼 클릭 시 커스텀 메시지를 생성하고, 올바른 채널과 길이 정보를 포함해 서버로 전송하는 기능이 정상 작동함을 확인했습니다. 수신 측에서는 메시지가 예상대로 표시되며, 시퀀스 번호 기반의 로컬 메시지 동기화도 정상적으로 이루어졌습니다.