Skip to main content
Views 3

크림슨랜드 1:1 재구현과 AI 기반 리버스 엔지니어링

Summary

이 노트는 아래 출처를 AI로 재구성한 것입니다. 원문 방문을 권장합니다.

원문 출처: https://banteg.xyz/posts/crimsonland/

핵심 요약

2003년작 탑다운 슈터 게임 크림슨랜드를, 소스 코드 없이 실행 파일과 자산만으로 현대 환경에서 거의 완벽하게 재구현한 사례를 정리한 내용이다.

정적·동적 분석 도구와 커스텀 포맷 해석, 테스트 가능한 구조 재설계, 그리고 LLM 에이전트 활용이 결합될 때 오래된 게임을 "겉모습"이 아니라 "내부 동작"까지 복원할 수 있음을 보여준다.

크림슨랜드와 프로젝트의 목표

크림슨랜드는 2003년 출시된 탑다운 아레나 슈터로, 한 캐릭터가 사방에서 몰려오는 적을 쏘며 버티는 단순하지만 중독성 강한 게임이다. 당시에는 7.5MB짜리 작은 쉐어웨어였지만, 잡지 번들 CD에 실릴 만큼 컬트적인 인기를 누렸고, 이후 와이드스크린을 지원한 1.9.93 버전이 사실상 "클래식 최종판"으로 남았다.

프로젝트의 목표는 이 클래식 버전을 "영감으로 삼아 비슷하게 만드는 것"이 아니라, 원본 윈도우 실행 파일을 사실상의 명세로 삼아 동작을 최대한 똑같이 복제하는 것이다. 버그와 그래픽 깨짐까지 포함해 모든 행동을 재현하고, 구현된 모든 함수는 디컴파일이나 런타임 관찰로 근거를 가지며, 원본 런타임에 의존하지 않고 자산 파일만 재사용한다는 엄격한 원칙이 세워졌다.

이렇게 기준을 "겉으로 비슷한가?"에서 "내부적으로 같은가?"로 올리면 난이도는 크게 높아지지만, 어떤 선택을 할지 애매한 순간마다 방향을 또렷하게 잡아주는 역할을 한다.

정보가 없는 바이너리와 vtable이라는 '로제타 스톤'

대상 바이너리는 Visual C++ 7.1로 빌드된 DirectX 8.1 게임이며, 렌더링과 입력 등은 grim.dll이라는 별도 엔진 DLL이 담당한다. 문제는 이 바이너리가 심볼과 타입 정보가 거의 제거된 상태라, 함수 이름도, 구조체 정의도, 호출 규약도 제대로 노출되지 않는다는 점이다. 디컴파일하면 눈에 들어오는 것은 주소 기반의 난해한 호출뿐이다.

게임 코드가 Direct3D를 직접 호출하지 않고, 엔진 인터페이스의 vtable을 통해 BindTexture, DrawQuad 등 엔진 함수에 간접적으로 접근한다는 점이 핵심 단서가 된다. 특정 인덱스의 vtable 엔트리가 실제 실행 중 어떤 호출 패턴을 보이는지 기록하면, 이것이 텍스처 바인딩인지, 색상 설정인지, 버텍스 그리기인지 하나씩 추론할 수 있다.

이 vtable은 마치 고대 문서를 해독할 때 사용하는 '로제타 스톤'처럼, 처음에는 의미를 알 수 없었던 코드 덩어리들을 점차 사람에게 익숙한 렌더링·입력·사운드 호출로 번역해 나가는 중간 매개체 역할을 한다.

Ghidra와 Binary Ninja로 만드는 '읽을 수 있는 코드'

정적 분석에서는 Ghidra와 Binary Ninja가 서로 다른 강점을 살려 사용된다. Ghidra는 무료이며 헤드리스 모드와 스크립팅이 쉬워, 대규모 자동 디컴파일과 이름 재부여 파이프라인의 중심이 된다. 처음 디컴파일 결과는 FUN_00430af0 같은 무의미한 이름으로 넘쳐났지만, 작성자는 함수 주소와 새 이름, 시그니처, 이름을 붙인 근거를 JSON으로 체계적으로 관리하며 하나씩 의미를 부여했다.

예를 들어 문자열 이름으로 항목을 찾고, 없으면 새로 할당하고, 문자열과 float 값을 저장하며, 특정 콘솔 변수 등록 함수들에서만 호출되는 함수라면 "콘솔용 cvar 등록 함수"라고 이름 붙인다. 이런 방식으로 함수·타입·구조체 레이아웃 정보가 조금씩 전파되면서, 난해했던 코드가 점점 player_fire_weapon, player_state_table처럼 사람 눈에 익숙한 게임 로직으로 변해간다.

Binary Ninja는 상위 버전에서만 헤드리스가 가능해 자동화에는 덜 맞지만, 인터랙티브 탐색에 탁월하다. "이 함수는 어디서 호출되는가", "이 주소를 참조하는 모든 위치", "이 패턴과 유사한 함수"를 빠르게 찾아볼 수 있어, LLM 에이전트가 맥락을 잡고 가설을 세우는 데 큰 도움을 준다.

WinDbg와 Frida로 '추측'을 검증하는 실행 분석

정적 분석만으로는 항상 추측이 섞일 수밖에 없다. 따라서 실제 게임을 실행하면서 동작을 관찰하는 동적 분석이 필수다. 작성자는 가상 머신 대신 부트캠프 환경에 윈도우를 설치해, 네이티브 환경에서 WinDbg/cdb로 디버깅을 진행했다.

LLM 에이전트가 디버거와 직접 긴 세션을 유지하기 어렵기 때문에, 서버/클라이언트 모드와 로그 파일, tail 스크립트를 조합해 간접적인 실시간 인터페이스를 구성했다. 에이전트는 특정 함수 진입 시점의 레지스터와 메모리 상태를 반복적으로 확인하며 "이 함수가 정말 내가 생각한 그 기능을 하는가?"를 끈질기게 검증한다.

Frida는 자바스크립트를 주입해 함수 후킹과 호출 추적, 인자·리턴값 변조를 가능하게 해 준다. 예를 들어 DirectX8과 OpenGL 간 좌표계 차이로 지형 렌더링이 미묘하게 어긋날 때, 원본 게임의 프레임버퍼를 캡처해 새 구현과 픽셀 단위로 비교하는 데 쓰인다. 또한 치트 기능 구현이나 로그 수집에도 활용되어, 특정 함수가 실제 플레이 중 언제, 어떤 인자로 호출되는지를 정확하게 기록할 수 있다.

자산 포맷 해독: PAQ 아카이브와 JAZ 텍스처

원본 게임의 자산은 PAQ라는 커스텀 아카이브 포맷으로 묶여 있다. 매직 문자열 이후 파일명(널 종료 문자열), 32비트 리틀엔디언 크기, 원시 바이트가 연속된 단순한 구조이며, 경로는 윈도우 스타일 백슬래시를 사용한다. 이 구조를 파악하면 게임에 포함된 모든 파일을 안정적으로 추출할 수 있다.

텍스처는 JAZ라는 별도 포맷을 사용하는데, 처음에는 "zlib 압축된 JPEG + 정체불명 데이터"로 알려져 있었다. 분석 결과, 기본 JPEG 이미지에 알파 채널을 RLE로 붙이고, 이 전체를 zlib으로 다시 압축한 형태라는 것이 드러났다. 알파 RLE를 풀면 항상 width * height 바이트 배열이 나와야 하는데, 일부 파일은 한 픽셀만 부족한 등 예외도 존재한다.

이 모든 이진 포맷은 Python의 construct 라이브러리로 선언적 파서를 만들어 다룬다. 이렇게 정의된 파서는 JAZ를 PNG로 변환해 디버그용 텍스처나 테스트 픽스처로 활용할 수 있게 하고, 나중에 다른 엔진이나 도구에서도 재사용 가능한 "포맷 명세" 역할을 한다.

텍스트·지형·스프라이트: 2D 렌더링 구조와 Raylib 선택

새 구현에서는 초반에 텍스트 렌더링을 먼저 복원한다. 텍스트가 되면 디버그 정보, UI, 상태 표시를 빠르게 그려볼 수 있어 전체 리버스 작업의 속도가 크게 올라간다. 원본 게임은 제목, 메뉴, UI, 레벨 이름 등에 서로 다른 폰트를 사용했는데, 당시 무료 폰트 사이트에서 가져온 것으로 추정되는 파일들을 다시 구해 원본과 최대한 비슷한 느낌을 재현한다.

렌더링 구조는 단순하지만 특징적이다. 먼저 지형만을 위한 별도 프레임버퍼를 만들고, 시드를 바탕으로 배경 텍스처를 생성한다. 이후 시체, 피, 탄피, 탄흔 등 "영구 흔적"에 해당하는 요소들을 이 프레임버퍼에 계속 덧그려, 전장의 변화가 배경에 그대로 남게 한다. 적, 투사체, 이펙트는 그 위에 별도의 스프라이트로 그려지고, 일부 투사체는 잔상을 남기는 식으로 표현된다.

새 엔진으로는 Unity나 Unreal 같은 고수준 엔진 대신 Raylib이 선택된다. Raylib은 윈도우 생성, 텍스처 그리기, 사운드 재생, 입력 처리 같은 최소한의 기능만 제공하는 비교적 저수준 라이브러리로, DirectX 8 시절의 직관적인 2D 렌더링 모델을 그대로 재현하기에 적합하다. grim.dll이 담당하던 엔진 레이어를 Raylib으로 교체하고, 그 위에 게임 로직을 원본과 비슷한 구조로 쌓아 올리는 방식으로, "현대 API 위에 옛날 게임의 뇌"를 이식하는 셈이다.

하드코딩 로직을 테스트 가능한 구조로 바꾸기

원본 크림슨랜드의 게임 로직은 대부분 실행 파일 안에 하드코딩되어 있으며, 레벨별 적 스폰 시나리오도 4천 줄에 달하는 거대한 switch 문으로 구현되어 있다. 이런 구조는 재현이 목적이라면 그대로 베껴 올 수도 있지만, 이후 이해와 수정, 테스트에는 치명적으로 불편하다.

재구현에서는 동작은 그대로 유지하되 구조를 쪼개고 테스트 가능하게 바꾸는 전략을 택한다. 예를 들어 레벨 빌더를 함수로 분리해, 랜덤 시드와 맵 크기를 입력으로 받아 "언제, 어디에, 어떤 적을 몇 마리 스폰할지"를 기술한 SpawnEntry 목록을 반환하게 만든다. 그러면 원본 실행 파일에서 캡처한 스폰 결과를 정답으로 삼아, 새 구현이 같은 SpawnEntry 시퀀스를 만드는지 단위 테스트로 비교할 수 있다.

같은 시드를 사용해 생성한 지형 텍스처를 픽셀 단위로 비교하는 테스트도 가능해지며, 이 과정에서 "음수 히트박스로 애니메이션 속도를 제어하는" 등 기묘하지만 효과적인 설계도 그대로 문서화된다. 결과적으로, 처음에는 난해했던 디컴파일 코드는 점차 "테스트 가능한 작은 조각들로 구성된 읽을 수 있는 게임 엔진"으로 변모한다.

AI 에이전트가 바꾸는 개발 루틴

작성자는 이 프로젝트를 통해, GPT‑5.2 기반 에이전트와 Codex 조합이 리버스 엔지니어링 같은 작업에 특히 잘 맞는다고 평가한다. 이 모델들은 주어진 규칙을 엄격히 따르고, 추측보다는 근거를 중시하는 성향이 강해 "코드에 근거 없는 상상"을 줄이는 데 도움을 준다.

에이전트는 Ghidra, Binary Ninja, WinDbg, Frida 등 다양한 도구와 연동되어 "함수 이름 추론 → 근거 기록 → 디컴파일 재생성 → 런타임 검증"이라는 루프를 지치지 않고 반복한다. 사람에게는 지루하고 실수하기 쉬운 작업을 대량으로 처리해 주고, 사람은 전략 수립, 설계 선택, 최종 판단에 집중한다.

작성자는 한 달 동안 이 프로젝트를 포함해 두 개의 큰 프로젝트를 완수하면서, "사람 + 에이전트 도구" 조합이 이미 상당한 생산성을 제공한다는 확신을 얻는다. 중요한 것은 한 번에 멋진 결과를 뽑는 것이 아니라, 수백·수천 번의 반복에서도 꾸준히 앞으로 나아가는 흐름을 유지하는 것이라는 통찰을 강조한다.

보존과 확장, 그리고 커뮤니티의 역할

이 재구현의 궁극적인 가치는 단순히 "옛 게임을 현대 환경에서 돌릴 수 있게 만드는 것"을 넘어선다. 2040년에 누군가 다시 크림슨랜드를 다른 플랫폼으로 옮기려 할 때, 더 이상 난해한 바이너리에서 시작할 필요 없이 정리된 소스 코드와 상세한 지식 베이스를 출발점으로 삼을 수 있게 되는 것이다.

작성자는 의도적으로 시각적 개선을 거의 하지 않았다. 자신의 기억 속 크림슨랜드를 그대로 되살리는 것이 우선 목표였기 때문이다. 대신 선택적 옵션으로 "야간 모드와 고품질 조명" 정도를 상상하며, 서명 거리 함수(SDF)를 이용한 레이마칭 소프트 섀도우 프로토타입 정도만 구현해 두었다.

프로젝트는 GitHub 코드, 지식 베이스, 텔레그램 그룹 형태로 공개되었고, 원작에 익숙한 플레이어들이 실제로 플레이하며 "거미가 서로에게 주던 특수한 피해 판정" 같은 미세한 차이들을 지적하는 것이 마지막 퍼즐 조각을 맞추는 데 큰 도움을 준다. 작성자는 다른 이들도 자신이 사랑했던 옛 소프트웨어를 비슷한 방식으로 보존해 보라고 권하며, 그 과정 자체가 훌륭한 학습이자 즐거움이 될 수 있다고 말한다.

인사이트

이 사례는 소스 코드를 잃어버린 소프트웨어도, 충분한 의지와 적절한 도구, 그리고 AI 에이전트의 도움만 있다면 상당한 정밀도로 재구현할 수 있음을 보여준다. 특히 게임처럼 상태 머신과 그래픽·사운드 로직이 복잡하게 얽힌 대상조차, 정적·동적 분석을 체계적으로 결합하고 테스트 가능한 설계로 재구성하면 "겉모습"을 넘어 "동작"까지 보존할 수 있다.

실천적인 관점에서, 오래된 프로젝트를 복원하거나 포팅하고 싶은 사람은 먼저 "겉으로 비슷하게 만드는 것"보다 "내부 동작을 이해하고 기록하는 것"을 우선 목표로 삼는 것이 좋다. 디컴파일 결과를 그대로 믿지 말고 함수 이름, 타입, 구조체를 꾸준히 개선해 읽을 수 있는 코드로 다듬고, 원본 실행 파일에서 캡처한 런타임 데이터를 테스트 픽스처로 적극 활용하면, 인간의 기억이 아니라 측정 가능한 기준으로 재현 성능을 평가할 수 있다.

마지막으로, AI 도구는 창의적인 설계를 대신해 주는 마법사가 아니라, 귀찮고 반복적인 정리·탐색·검증 작업을 대신해 주는 "성실한 팀원"에 가깝다고 이해하는 것이 좋다. 이 관점을 유지하면, 복잡한 옛 소프트웨어를 복원하거나 대규모 리팩터링을 진행할 때 현재의 LLM 에이전트를 실질적인 전력으로 활용하는 새로운 개발 방식이 열린다.

크림슨랜드 1:1 재구현과 AI 기반 리버스 엔지니어링

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.