Mame Rom Hacking Reversing 롬 해킹 및 리버싱
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 디버거의 사용법은 공식 문서를 참고하고, 여기서는 따라하기 식으로 설명한다.
1. 처음 마을에 도착한 후 디버거 창에서 "ci" 입력 - 메모리 검색 초기화
2. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
3. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
4. 한 대 맞고 "cn de" 입력 - 감소한 값 찾기
피통과 관련된 메모리 주소는 두 개의 후보가 나온다.
FF831D
는 값을 바꿔도 피통이 변하지 않았고, FF8641
을 AA
로 고쳤을 때 피통이 차는 모습을 확인할 수 있었다.
우리가 찾던 변수는 FF8641
이라는 것을 알 수 있다. FF8641
변수를 항상 특정값으로 고정시키는 치트를 써도 원하는 목적을 달성할 수 있지만, 핵롬을 만들기 위해서는 변수를 고치는 것이 아니라 이 변수를 설정하는 코드를 수정해야 한다.
이제 어떤 코드에서 해당 변수를 바꾸는지 찾아야 한다. 이때 사용하는 디버거 명령어는 wp
이다.
명령어 입력창에 wp ff8641,1,w
를 입력하고 몬스터에게 맞아본다. ff8641
영역의 1바이트에 쓰기 작업이 일어나면 브레이크포인트가 걸리게 하는 명령이다.
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
이 두 개 들어간 형태다.
이제 맞아도 데미지를 받지 않는다.
Code Cave로 향하는 여정
던전 앤 드래곤의 CPU 모델
던드의 롬파일 식별자는 ddsom
이며, 자식 롬 구조는 ddsomxx
로 정해져 있다.
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 작업을 위한 도구 준비
이제 본격적인 코드 케이브 작업을 시작해보자. 이 작업에는 여러 도구가 필요하다:
- MAME 디버거: 실시간으로 메모리를 수정하고 브레이크포인트를 설정
- Ghidra: 어셈블리 코드 분석 및 옵코드 생성
- 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개까지 구매할 수 있게 된다.
코드 설명
cmp.b #0x23,D7b
: D7 레지스터의 하위 바이트가 $23(특정 아이템 ID)인지 비교beq.b LAB_0006300c
: 같다면 특정 아이템 처리 로직으로 점프cmpi.b #0x3,(A4)
: 특정 아이템의 경우 3개 제한cmpi.b #0x9,(A4)
: 일반 아이템의 경우 9개 제한bge.w LAB_00063022
: 제한을 초과하면 실패 주소로 점프jmp LAB_000afba6.l
: 성공시 원래 성공 주소로 점프jmp LAB_000afbec.l
: 실패시 원래 실패 주소로 점프
이런 식으로 코드 케이브를 이용하면 게임의 특정 로직만을 선택적으로 수정할 수 있다.
마무리
이번 실습을 통해 다음과 같은 과정을 거쳤다:
- 메모리 검색:
ci
,cn +
명령어로 아이템 구매 관련 메모리 주소 찾기 - 브레이크포인트 설정:
wp
명령어로 쓰기 포인트 설정 - 코드 추적: 브레이크포인트를 통해 아이템 구매 로직의 최초 호출 지점 찾기
- 도구 활용: MAME 디버거, Ghidra, 68000 어셈블리어를 조합한 분석
- 코드 케이브 구현: 빈 메모리 공간에 새로운 로직 삽입
이 과정은 단순한 메모리 치팅을 넘어서 게임의 내부 로직을 이해하고 수정하는 진정한 리버스 엔지니어링의 시작점이다.
고전 게임 해킹의 매력은 바로 이런 과정을 통해 게임 개발자들이 어떻게 생각하고 코드를 작성했는지 엿볼 수 있다는 점에 있다. 68000 어셈블리어라는 낯선 언어를 통해 30년 전 개발자들과 대화하는 듯한 느낌을 받을 수 있을 것이다.