Direct3D FPS

A Direct3D game where the win condition is reached by patching the check rather than playing it.

2026.08.18 Reversing original post

문제를 살펴보자. 파일을 실행시키면 게임이 나온다. 둘러보면 뜬금없이 카라 사진과 군고구마들이 돌아다닌다. 군고구마를 가까이서 총으로 쏠 수 있고, 고구마랑 닿으면 HP 가 닳아서 리스폰 장소로 스폰된다. 코드를 한번 보자.

Figure 1

(화면에 자꾸 마우스 캡처링 현상이 일어나는데, Alt + Tab 으로 피할 수 있다.) x64 디버거로 열어주었다. 메인 함수로 이동해 문자열 찾기를 해준다.

Figure 2

조건문을 패치해서 게임 클리어 메시지 창이 뜨게 했다 (JE → JNE). 그러니까 이상한 아스키코드 창이 나왔다. 아마 정답 값이 뜨는 게 맞는데, 내가 패치를 통해 클리어 창이 뜨게 해서 암호화가 안 풀린 듯하다.

Figure 3

아까 전 004F39E2 주소가 플래그 값의 주소인 거 같다.

Figure 4

메모리 덤프로 이동했다. Ctrl + R 로 따라가 준다. 여기 코드를 분석해준다.

이 게임은 고구마(적)를 쏴서 죽이는 FPS 이고, 고구마 하나하나가 구조체다. 고구마를 “죽일 때마다” 플래그 버퍼(fps.A97028)의 특정 바이트가 XOR 로 갱신된다. 즉 플래그 = “어떤 고구마를 어떤 순서로 몇 번 죽였는가”의 결과다. 00A933FF ~ 00A9343F 는 고구마가 피격되었을 때 호출되는 처리 루틴이라는 걸 알 수 있다.

Figure 5

고구마 인덱스 계산:

call  fps.A93440
cmp   eax, FFFFFFFF
je    fps.A9343E
mov   ecx, eax
imul  ecx, ecx, 210      ; 고구마 구조체 크기 = 0x210
  • fps.A93440 → 현재 맞은 고구마의 index 반환
  • eax = -1 이면 무효
  • 유효하면 ecx = 고구마 index * 0x210 로 고구마 구조체 배열 접근

고구마 사망 시 플래그 XOR:

mov  dword ptr [ecx+A99194], 0   ; 고구마 생존 여부 = dead
mov  cl, byte ptr [ecx+A99184]   ; 고구마 고유 ID / 키
xor  byte ptr [eax+A97028], cl   ; 플래그 갱신

그럼 플래그는 언제 “완성” 되나?

  • 게임 클리어 조건 = 모든 고구마 사망
  • 모든 고구마를 죽이면 플래그 버퍼(A97028)가 최종 상태가 되고, 그 상태가 MessageBox 로 출력됨

flag[i] = 구조체[i] ^ 고구마_Key[i] 이다. 코드를 보면 모든 키가 “구조체 안에 이미 들어 있다”. 즉 구조체 전체를 다 뽑아보면 키 집합을 통째로 얻을 수 있다.

이제 그럼 고구마 구조체 전체를 어떻게 얻는가? 어셈블리어에서 구조체의 첫 복호화 주소는 4F9194 였다는 걸 기억하자. 이미지 베이스값을 생각해보면 상대 주소는 9194 다. 하지만 PE viewer 로 보면 Size of Raw Data 가 한참 작다. 즉, 고구마 구조체는 프로그램이 메모리에 올라온 후에 메모리에 기록된다.

원본 파일은 고구마 구조체에 대한 데이터가 없으니까, 고구마 구조체 데이터가 메모리에 저장된 상태에서 새 exe 를 만들어서 구조체에 대한 데이터를 볼 수 있는 파일을 만들자 (x64dbg 의 Scylla 플러그인을 사용함).

Figure 6

구조체 주소를 계산하자! 구조체의 RVA 를 RAW 로 변환했다. (RVA to RAW 공식을 기억하자: RAW = RVA - Section.VirtualAddress + Section.PointerToRawData) HxD 에서 해당 오프셋을 파일로 저장해주었다.

RVA: 0x9184 ~ 0xF8A4
RAW: 0x6F84 ~ 0xD6A4

같이 XOR 할 플래그도 찾아준다!

Figure 7

x64dbg 의 덤프창에서 주워주었다. 값은:

43 6B 66 6B 62 75 6C 69 07 25 25 29 70 17 34 39 F7 EB FA E8 B0 FD EB BC F0 A9

복호화 방식은 flag[i] = 구조체[i] ^ 고구마_Key[i] 이다. 참고로 연산을 시킬 때 중요한 부분들이 있다.

  1. 실제로는 0x210 바이트 단위 구조체들이 연속으로 있는데, 각 구조체의 첫 바이트만 키로 사용한다. 즉 고구마_key[i] = struct[i * 0x210].first_byte.
  2. 구조체 중에서 마지막 구조체는 계산에서 제외시킨다. 이유는 고구마 키 바이트 길이는 실제 flag 바이트 수인데, 구조체 길이가 하나 더 길어서 마지막 구조체는 flag 와 1:1 매칭되지 않기 때문이다.

그럼 답은:

Congratulation~ Game Clear!
Password is Thr3EDPr0m