6 minute read

MAME 기반의 게임 해킹

최근에 중국 해킹롬 “The King of Fighters 98”을 플레이하다가 문득, 고전게임의 롬파일 수정은 어떻게 하는 걸까? 라는 생각이 들었다. 고전게임도 결국 프로그래머가 작성한 프로그램일 텐데, 아무리 복잡해봐야 폰노이만 모델 아닌가? 라는 생각에 자료를 찾아보기 시작했다.

가장 기본적인 해킹 방식은 역시 메모리 치팅이다. 메모리 치팅의 진행 방식은 처음에 모든 값을 검색하고, 값의 범위를 점진적으로 좁혀나가는 작업을 반복하는 것이다.

예를 들어 생명의 개수를 조작하고 싶다면, 처음에 메모리를 초기화하고 생명의 현재 개수를 검색한다. 생명이 5개라면 5를 검색하고, 한 번 죽고 나서 4를 검색하는 과정을 반복한다.

이런 과정을 반복하다 보면 필연적으로 하나의 메모리 주소가 나오게 된다. 그 메모리 값을 99와 같은 큰 값으로 고정시키면 목숨이 무한개가 된다. 어릴 때 다들 T-search나 GameHack 같은 툴로 한 번씩은 해봤을 법한 작업이다.

이 작업은 MAME의 디버거 툴에서도 똑같이 진행할 수 있다. 간단한 것부터 해볼까?

우리의 타겟이 되는 게임은 Dungeons & Dragons: Shadow over Mystara 일명 던전 앤 드래곤이다. 줄여서 던드라고 부르겠다.

이 게임은 내 기억으로는 죽어가던 아케이드 시장에서 마지막으로 꽃을 피운 작품이다. 타임어택 방식을 제외한 일반 게임 진행 시 1시간을 넘어가는 말도 안 되는 볼륨을 보여준다. 고작 20MB 용량으로 말이다. 여러 명이 함께 즐길 수 있는 게임으로 아마 아케이드 시장에서 가장 성공한 게임이 아닐까 싶다. 이전의 타워 오브 둠에서의 단점이었던 빈약한 액션감을 보완하면서 최고의 게임으로 평가받고 있으며, 현재에도 매니아들은 이 게임을 여전히 매일매일 즐기고 있다. (액션감을 너무 강조한 나머지 난이도가 말이 안 되게 쉬워졌지만)

에뮬레이터 & 롬 설정

여러 자식 롬들이 있지만 가장 수정하기 쉬운 ddsomud로 진행한다.

MAME에 디버거를 띄우는 방법부터 알아야 한다. MAC과 WINDOWS 기준으로 설명한다.

MAC

버전 정보: sdlmame 0.174

MAC에서는 sdlmame 0.174를 다운로드하여 압축을 풀고 ./mame64 -debug 명령어로 실행한다.

WINDOWS

버전 정보: MAMEUI 0.145

UI 상에서 디버거 옵션을 체크하여 활성화한다.

윈도우즈 MAME 디버거 설정

가장 간단한 게임 조작 - 무적 만들기

지금부터 가장 기본적인 게임 조작을 해보겠다. 캐릭터가 맞아도 데미지를 받지 않게 만들어보자.

위에서 설명한 대로 설정하고 롬파일을 구동시키면 디버거 창이 나타난다.

image image

기본적인 MAME 디버거의 사용법은 공식 문서를 참고하고, 여기서는 따라하기 식으로 설명한다.

image

1. 처음 마을에 도착한 후 디버거 창에서 "ci" 입력 - 메모리 검색 초기화
2. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
3. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
4. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기

피통과 관련된 메모리 주소는 두 개의 후보가 나온다.

FF831D는 값을 바꿔도 피통이 변하지 않았고, FF8641AA로 고쳤을 때 피통이 차는 모습을 확인할 수 있었다.

우리가 찾던 변수는 FF8641이라는 것을 알 수 있다. FF8641 변수를 항상 특정값으로 고정시키는 치트를 써도 원하는 목적을 달성할 수 있지만, 핵롬을 만들기 위해서는 변수를 고치는 것이 아니라 이 변수를 설정하는 코드를 수정해야 한다.

image

이제 어떤 코드에서 해당 변수를 바꾸는지 찾아야 한다. 이때 사용하는 디버거 명령어는 wp이다.

명령어 입력창에 wp ff8641,1,w를 입력하고 몬스터에게 맞아본다. ff8641 영역의 1바이트에 쓰기 작업이 일어나면 브레이크포인트가 걸리게 하는 명령이다.

image

27566 | sub.w D0, ($62, A0) | 9168 0062
2756A | bpl $27570

2756A에서 게임이 멈추게 된다. 아마 ($62, A0) 위치에서 D0만큼 값을 빼면서 브레이크포인트가 걸린 것 같다.

27566 위치의 명령어를 아무것도 하지 않게 만들면 맞아도 데미지를 받지 않을 것이다.

한번 바꿔보자. 디버거에서 Ctrl+M을 누르고 Region ':maincpu' 영역으로 이동한 뒤 27566 위치로 이동해서

4E71 4E71로 값을 입력해보자. 참고로 4E71 4E71은 아무것도 하지 않는 명령어인 nop이 두 개 들어간 형태다.

image

이제 맞아도 데미지를 받지 않는다.

Code Cave로 향하는 여정

던전 앤 드래곤의 CPU 모델

던드의 롬파일 식별자는 ddsom이며, 자식 롬 구조는 ddsomxx로 정해져 있다.

image

MAME 소스의 cps2.cpp를 보면 던드의 CPU 모델은 68000(68k)이다.

M86K Assembler

특정 아이템 개수 제한하기 - Code Cave 실습

이번에는 더 복잡한 작업을 해보자. 던전 앤 드래곤에서 특정 아이템(LB oil)만 개수 제한을 걸어보는 코드 케이브 작업을 진행해보겠다.

실행 방법

mame64.exe -debug

타겟 롬: ddsomud

아이템 구매 관련 코드 찾기

먼저 아이템을 구매할 때 어떤 메모리 주소가 변경되는지 찾아보자.

ci

메모리 검색을 초기화한 후 아이템을 하나 사보자.

cn +

아이템을 하나 더 사보자.

cn +

아이템을 하나 더 사보자.

cn +

이 과정을 통해 FF8728이 어떤 아이템의 개수를 담고 있는 주소임을 확인할 수 있다.

Write Point 설정

이제 해당 주소에 쓰기 작업이 일어날 때 브레이크포인트를 걸어보자.

wp ff8728,1,w

브레이크포인트를 설정한 후 다시 아이템을 사보면 AFBAC에서 걸린다.

000afbaa 52 14           addq.b     #0x1,(A4)

addq.b #0x1,(A4) 명령어는 A4가 가리키는 주소의 값을 1 증가시키는 명령이다. 아이템을 구매하는 동작이 확실해 보인다.

최초 호출 지점 찾기

이제 아이템을 구매할 때 호출되는 최초의 위치를 찾아야 한다. AFBAA 위의 주소들에 브레이크포인트를 여러 개 설정해보자.

마구잡이 브레이크포인트 설정

AFB2A에서 처음 걸리는 것 같다. 여기서부터 코드를 따라가다 보면 AFB9E가 보인다.

cmpi.b #$9, (A4)

이 부분에서 9개로 제한을 두는 것 같다. 이 부분을 적절히 수정해서 원하는 아이템만 3개로 제한하면 될 것 같다.

Code Cave 작업을 위한 도구 준비

이제 본격적인 코드 케이브 작업을 시작해보자. 이 작업에는 여러 도구가 필요하다:

  1. MAME 디버거: 실시간으로 메모리를 수정하고 브레이크포인트를 설정
  2. Ghidra: 어셈블리 코드 분석 및 옵코드 생성
  3. 68000 어셈블리어: 실제 코드 작성

메모리 덤프 및 분석

먼저 디버거에서 현재 실행 중인 메모리의 덤프를 뜬다:

save v.bin,0,3fffff

이 덤프 파일을 Ghidra에서 열어서 전체 메모리 구조를 파악하고 빈 공간을 찾아보자. Ghidra에서는 인라인 어셈에 대한 옵코드를 쉽게 얻을 수 있다.

코드 케이브 위치 선택

Ghidra에서 메모리를 둘러보다 보면 63000 주소 근처가 비어있는 공간으로 보인다. 이런 빈 공간을 “코드 케이브”라고 부르며, 여기에 우리가 원하는 새로운 로직을 삽입할 수 있다.

AFB9E에서 이 63000 주소로 점프하는 명령어를 사용하여 코드 케이브 작업을 진행하자.

기존 코드 분석

0AFB9E cmpi.b $#9, (A4)    0c14 0009
0AFBA2 bge $afbec          6c00 0048

이 코드는 A4가 가리키는 값(아이템 개수)이 9개 이상인지 비교하고, 9개 이상이면 afbec로 점프하는 코드다.

새로운 로직 설계

$63000 번지에 작성할 새로운 규칙을 의사코드로 설계해보자:

// 코드 케이브 시작 ($63000)

if (현재 아이템의 ID(D7 레지스터) == 특정 아이템($23)) {
    // 특정 아이템의 개수 제한 로직
    if (현재 아이템의 개수(A4 포인터가 가리키는 ) >= 3) {
        // 3개 이상이면 실패
        goto 실패주소($afbec);
    }
} else {
    // 일반 아이템의 개수 제한 로직
    if (현재 아이템의 개수 >= 9) {
        // 9개 이상이면 실패
        goto 실패주소($afbec);
    }
}

// 성공시 아이템 획득
goto 성공주소($afba6);

실제 어셈블리 코드 구현

위 의사코드를 실제 68000 어셈블리어로 변환하면:

                             LAB_000afb9e                                    XREF[1]:     000afb5e(j)  
        000afb9e 4e f9 00        jmp        LAB_00062ffe+2.l
                 06 30 00
        000afba4 4e 71           nop
                             LAB_00063000                                    XREF[1]:     000afb9e(j)  
        00063000 4e 71           nop
        00063002 be 3c 00 23     cmp.b      #0x23,D7b
        00063006 67 04           beq.b      LAB_0006300c
        00063008 60 00 00 0a     bra.w      LAB_00063014
                             LAB_0006300c                                    XREF[1]:     00063006(j)  
        0006300c 0c 14 00 03     cmpi.b     #0x3,(A4)
        00063010 60 00 00 06     bra.w      LAB_00063018
                             LAB_00063014                                    XREF[1]:     00063008(j)  
        00063014 0c 14 00 09     cmpi.b     #0x9,(A4)
                             LAB_00063018                                    XREF[1]:     00063010(j)  
        00063018 6c 00 00 08     bge.w      LAB_00063022
        0006301c 4e f9 00        jmp        LAB_000afba6.l
                 0a fb a6
                             LAB_00063022                                    XREF[1]:     00063018(j)  
        00063022 4e f9 00        jmp        LAB_000afbec.l
                 0a fb ec

코드 적용

위 코드를 MAME 디버거의 region ':maincpu'에 hex 값으로 입력하면 실제 동작이 적용된다.

성공 장면

이렇게 하면 특정 아이템(LB oil, ID: $23)은 3개까지만 구매할 수 있고, 다른 아이템들은 기존처럼 9개까지 구매할 수 있게 된다.

코드 설명

  1. cmp.b #0x23,D7b: D7 레지스터의 하위 바이트가 $23(특정 아이템 ID)인지 비교
  2. beq.b LAB_0006300c: 같다면 특정 아이템 처리 로직으로 점프
  3. cmpi.b #0x3,(A4): 특정 아이템의 경우 3개 제한
  4. cmpi.b #0x9,(A4): 일반 아이템의 경우 9개 제한
  5. bge.w LAB_00063022: 제한을 초과하면 실패 주소로 점프
  6. jmp LAB_000afba6.l: 성공시 원래 성공 주소로 점프
  7. jmp LAB_000afbec.l: 실패시 원래 실패 주소로 점프

이런 식으로 코드 케이브를 이용하면 게임의 특정 로직만을 선택적으로 수정할 수 있다.

마무리

이번 실습을 통해 다음과 같은 과정을 거쳤다:

  1. 메모리 검색: ci, cn + 명령어로 아이템 구매 관련 메모리 주소 찾기
  2. 브레이크포인트 설정: wp 명령어로 쓰기 포인트 설정
  3. 코드 추적: 브레이크포인트를 통해 아이템 구매 로직의 최초 호출 지점 찾기
  4. 도구 활용: MAME 디버거, Ghidra, 68000 어셈블리어를 조합한 분석
  5. 코드 케이브 구현: 빈 메모리 공간에 새로운 로직 삽입

이 과정은 단순한 메모리 치팅을 넘어서 게임의 내부 로직을 이해하고 수정하는 진정한 리버스 엔지니어링의 시작점이다.

고전 게임 해킹의 매력은 바로 이런 과정을 통해 게임 개발자들이 어떻게 생각하고 코드를 작성했는지 엿볼 수 있다는 점에 있다. 68000 어셈블리어라는 낯선 언어를 통해 30년 전 개발자들과 대화하는 듯한 느낌을 받을 수 있을 것이다.