C&C 제너럴의 귀환, 그리고 20년 묵은 웜(Worm)의 공포: 레거시 코드의 보안 부채 분석
안녕하세요. 기술 블로그를 운영하는 Principal Engineer입니다.
오늘은 우리 세대 엔지니어들에게는 이름만 들어도 가슴이 웅장해지는 게임, Command & Conquer: Generals (이하 C&C:G)에 대한 이야기를 해보려 합니다. “AK-47s for everyone!”이라는 대사가 아직도 귀에 선한데, 최근 이 고전 명작이 보안 커뮤니티에서 아주 뜨거운 감자가 되었습니다.
최근 Atredis Partners의 연구원들이 이 게임의 네트워크 아키텍처를 탈탈 털어서 RCE(Remote Code Execution) 취약점과 이를 이용한 웜(Worm) 구현 가능성까지 증명했기 때문입니다. EA가 2025년에 소스코드를 공개한 것이 시발점이 되었는데, 코드를 뜯어보니 그야말로 ‘야생의 2000년대 C++’ 그 자체였습니다.
단순한 게임 해킹 이슈로 치부하기엔, 레거시 시스템의 취약점과 P2P 네트워크의 위험성을 너무나 교과서적으로 보여주는 사례라 기술적으로 깊게 파볼 가치가 충분합니다.
1. 타겟: 2003년의 유산
연구원들은 ‘Junkyard’라는 EoL(End-of-Life) 제품 해킹 대회에 참가하기 위해 C&C:G를 타겟으로 삼았습니다. 이 게임은 Peer-to-Peer(P2P) 방식을 사용합니다. 즉, 게임에 참여한 모든 클라이언트가 서로 0.0.0.0:8086(로비), 8088(인게임) 포트를 열고 통신해야 합니다.
이 구조가 보안 관점에서는 재앙의 시작입니다. 중앙 서버의 검증 없이 클라이언트끼리 데이터를 주고받는데, 패킷 구조는 XOR로 대충 암호화된 헤더와 데이터를 담고 있습니다. 패킷 파싱 로직은 거대한 if/else 블록으로 이루어져 있고요.
2. 취약점: 너무나 고전적인 스택 오버플로우
발견된 취약점 중 가장 치명적인 것은 NetPacket::readFileMessage 함수에 있었습니다. 코드를 한번 보시죠.
NetCommandMsg * NetPacket::readFileMessage(UnsignedByte *data, Int &i) {
NetFileCommandMsg *msg = newInstance(NetFileCommandMsg);
char filename[_MAX_PATH];
char *c = filename;
while (data[i] != 0) {
*c = data[i];
++c;
++i;
}
*c = 0;
// ... 후략
}
보자마자 한숨이 나오지 않나요? data[i]가 NULL을 만날 때까지 filename 버퍼에 복사합니다. _MAX_PATH라는 고정된 크기의 스택 버퍼를 두고, 입력 데이터의 길이를 전혀 검증하지 않습니다. 전형적인 Stack Buffer Overflow 입니다.
왜 이것이 치명적인가?
2025년인 지금, 최신 OS와 컴파일러는 ASLR(주소 공간 배치 난수화)이나 DEP(데이터 실행 방지) 같은 보호 기법을 기본적으로 적용합니다. 하지만 C&C:G는 2003년 게임입니다. 32비트 모드로 실행되고, 많은 라이브러리가 ASLR이 적용되지 않은 상태 로 로드됩니다.
연구원들은 이를 이용해 아주 손쉽게 ROP(Return-Oriented Programming) 체인을 구성했습니다. NULL 바이트만 피하면 되는 조건이라, VirtualAlloc을 호출해 메모리를 할당하고 쉘코드를 실행하는 것이 “식은 죽 먹기”였던 것이죠. 솔직히 말해, 요즘 보안 환경에 익숙한 엔지니어들에게는 향수를 불러일으키는(?) 수준의 난이도였을 겁니다.
3. 웜(Worm)의 탄생: 게임 안의 기생충
단순 RCE에서 끝났다면 이 글을 쓰지도 않았을 겁니다. 연구원들은 P2P 구조를 악용해 자가 복제 웜 을 만들었습니다. 감염 시나리오는 다음과 같습니다.
- 배포 (Delivery): 취약점을 이용해 피해자의 PC에 악성 DLL을 드랍합니다. 게임이 재실행될 때
dbghelp.dll같은 시스템 DLL을 가장해 로드되도록 합니다. - 후킹 (Trigger): 로드된 웜은
WSOCK32.dll의recvfrom함수에 IAT Hooking 을 겁니다. 이제 게임이 받는 모든 패킷은 웜이 먼저 검사합니다. - 전파 (Spread): 웜은 게임 내의 ‘Magic Packet’을 감지합니다. 새로운 플레이어가 게임에 들어오면(JOIN 패킷), 웜은 그 플레이어의 IP를 따내고 다시 악성 패킷을 보내 감염시킵니다.
이 과정에서 인상적인 부분은 Stealth 기술입니다. 웜끼리 통신하는 패킷 헤더에 특정 매직 바이트(0xdead4ead)를 심어두고, 후킹된 recvfrom에서 이를 먼저 가로챕니다. 일반적인 게임 로직으로 넘어가기 전에 처리해버리기 때문에, 감염되지 않은 클라이언트는 이 패킷을 무시하고, 감염된 클라이언트는 조용히 명령을 수행합니다.
4. 페이로드: “모든 것을 팔아라”
연구원들은 단순히 계산기를 띄우는 것(calc.exe)을 넘어, 게임 엔진 내부의 ScriptEngine을 직접 호출하는 기능까지 구현했습니다. ASLR이 없으니 함수 주소가 고정되어 있다는 점을 이용해, 원격에서 피해자의 게임 내 모든 건물을 팔아버리는(SellEverything) 스크립트를 실행시킬 수 있었습니다.
상상해 보세요. 멀티플레이 게임을 하는데 갑자기 내 기지가 전부 팔리고 게임이 터집니다. 그런데 상대방은 아무런 조작도 하지 않은 것처럼 보입니다. 이것이 P2P 게임 보안의 악몽입니다.
5. 결론 및 생각
EA의 반응은 예상대로였습니다. “지원 종료된 게임이니 패치는 없다.” 하지만 다행히 오픈소스 커뮤니티( TheSuperHackers/GeneralsGameCode)가 빠르게 반응하여 해당 취약점들을 패치했습니다.
이 사례가 우리에게 시사하는 바는 명확합니다.
- 레거시 코드는 시한폭탄이다: 소스코드가 공개된다는 것은 양날의 검입니다. 투명성을 제공하지만, 수십 년 묵은 취약점이 만천하에 드러나는 순간이기도 합니다.
- 메모리 안전성(Memory Safety)의 중요성: Rust나 Go 같은 현대 언어가 왜 중요한지, C/C++을 쓸 때
memcpy나 포인터 연산에 왜 강박적으로 집착해야 하는지 다시 한번 깨닫게 해줍니다. - 보안은 경계(Perimeter)에만 있지 않다: 방화벽 안쪽이라도, 신뢰하는 피어(Peer)라도 검증 없는 데이터는 언제나 위험합니다.
개인적으로는 추억의 게임이 이렇게 난도질당하는 모습을 보니 씁쓸하면서도, 20년 전의 코딩 관행과 현재의 보안 기준이 얼마나 달라졌는지 체감할 수 있는 흥미로운 사례였습니다. 혹시라도 향수에 젖어 구버전 C&C:G를 설치하시려는 분들이 있다면, 반드시 커뮤니티 패치가 적용된 버전을 사용하시길 권장합니다.
참고 자료: