개요
세이브 기능이 지원되지 않았던 과거 게임들은 특별하게 정의된 패스워드 입력으로 스테이지를 지정할 수 있게 하였다. 범피는 DOS 환경에서 구동되어 공을 튀겨 아이템을 먹고 목적지 골로 향하는 아주 단순한 고전 게임이다. 본 문서에서는 범피에서 스테이지 선택을 위해 제공하는 패스워드 기능 분석을 다룬다.
준비
- DOSBox-X : 도스 프로그램 구동을 위하여 사용
- 맥OS용으로 빌드된 DOSBox-X 바이너리를 사용하였다. 빌드된 바이너리에는 디버거 기능이 포함되어 있다.
- IDA Pro : 디스어셈블러
- 범피 Bumpy 게임
- UNP : 실행파일 압축 해제를 위한 유틸리티
목표 확인
게임 구동 후 PASSWORD 메뉴에서 사용되는 패스워드를 확보하고 검증 루틴을 찾고 분석한다.
분석 대상
실행파일인 BUMPY.EXE 를 분석한다.
➜ bumpy file BUMPY.EXE
BUMPY.EXE: MS-DOS executable, TinyProg compressed
해당 파일은 시그니처가 단순한 실행파일 압축 방식을 사용해서인지 macOS 의 file 커맨드에 TinyProg 실행압축이 적용된 것으로 표시되고 있다. 실행 압축 해제를 위하여 unp를 사용하였다.
만약 압축 해제가 원활히 되지 않는다면 이 과정에서 많은 시간이 소요될 수도 있는 작업이다. 하지만 대상은 unp 도구로 특별한 문제 없이 압축 해제 (unpack) 되는 것을 확인할 수 있다.
압축 해제된 실행 파일 바이너리의 문자열을 확인하면 패스워드 입력 과정의 문자열과 패스워드로 추정되는 문자열을 확인할 수 있다.
➜ bumpy strings BUMPY.EXE
<C&8
9&Lkw
nDD3
...
YOUR PASSWORD
ENTER YOUR PASSWORD
PASSWORD OK
PASSWORD ERROR
GAME OVER
Enter the platform number
ERROR
ACCESS
BUTTON
ISLAND
PRETTY
WINNER
ZOMBIE
LOVELY
SYSTEM
AAAAAA
...
문자열 확인만으로도 이미 패스워드를 확보 하였지만 확보 여부와 관계 없이 분석을 계속 진행하도록 한다.
분석
패스워드 입력 창에서 아래와 같이 임의로 패스워드 입력을 시도하였다.
Dosbox 디버거에서 메모리 덤프 기능인 MEMDUMP 명령을 사용하여 데이터 영역 메모리를 덤프하였다.
> MEMDUMP DS:0 10000
MEMDUMP.TXT 파일의 덤프된 메모리 영역에서 입력한 값을 찾아본 결과 아래와 같이 탐색에 성공하였다.
185E:2560 EB 01 EF 01 F3 01 F7 01 FB 01 A6 25 5E 18 41 43
185E:2570 43 45 53 53 00 42 55 54 54 4F 4E 00 49 53 4C 41
185E:2580 4E 44 00 50 52 45 54 54 59 00 57 49 4E 4E 45 52
185E:2590 00 5A 4F 4D 42 49 45 00 4C 4F 56 45 4C 59 00 53
185E:25A0 59 53 54 45 4D 00 44 44 44 44 44 44 00 00 00 01
185E:25B0 02 00 03 04 04 05 06 07 08 00 09 0A 0B 00 0C 0D
185E:25C0 00 00 00 00 00 0E 0E 00 0F 0F 0F 0F 10 11 00 12
185E:25D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01
185E:25E0 01 00 0B 09 09 05 06 07 09 00 09 09 01 00 01 0A
185E:25A6에 입력한 패스워드 알파벳 D에 대한 아스키 코드 값인 0x44가 저장되었음을 알 수 있다. IDA 디스어셈블러에서 확인한 정보는 아래와 같다.
dseg:256A dd aAaaaaa ; "AAAAAA"
dseg:256E aAccess db 'ACCESS',0 ; DATA XREF: dseg:135C↑o
dseg:2575 aButton db 'BUTTON',0 ; DATA XREF: dseg:1360↑o
dseg:257C aIsland db 'ISLAND',0 ; DATA XREF: dseg:1364↑o
dseg:2583 aPretty db 'PRETTY',0 ; DATA XREF: dseg:1368↑o
dseg:258A aWinner db 'WINNER',0 ; DATA XREF: dseg:136C↑o
dseg:2591 aZombie db 'ZOMBIE',0 ; DATA XREF: dseg:1370↑o
dseg:2598 aLovely db 'LOVELY',0 ; DATA XREF: dseg:1374↑o
dseg:259F aSystem db 'SYSTEM',0 ; DATA XREF: dseg:1378↑o
dseg:25A6 aAaaaaa db 'AAAAAA',0 ; DATA XREF: dseg:256A↑o
dseg:25A6에는 ‘AAAAAA’ 문자열이 위치한다. 해당 문자열은 패스워드 입력 창에 초기 값으로 저장된 값이여 사용자가 패스워드를 변경하면 메모리상의 해당 값이 변경된다. 해당 문자열은 dseg:256A에서 참조된다. 아쉽게도 dseg:256A의 참조 주소는 IDA에서 표시되지 않고있다.
입력된 문자열이 어디에서 사용되는지 확인해보기 위하여 dseg:256A 메모리에 브레이크포인트를 지정하여 값의 변경 지점을 확인하였다. Dosbox의 디버그 모드를 heavy로 빌드하면 메모리 브레이크포인트가 가능하다. 다만 Dosbox의 메모리 브레이크 포인트 기능은 Access는 지원하지 않고 Write만 가능하기 때문에 접근 여부가 아닌 메모리 상의 값 변경 지점만을 확인할 수 있다. 여기에서는 이미 빌드된 Dosbox-x의 디버거 기능을 활용하였다.
> BPM DS:256A
0823:5D6B 주소의 mov es:[bx], al 코드에서 입력 패스워드가 변경되는 것을 확인할 수 있다. bx 레지스터는 사용자 입력 패스워드가 저장되는 메모리의 주소를 가리킨다.
IDA에서 해당 코드가 위치하는 함수를 처음부터 자세히 살펴보도록 한다.
seg000:5C87 sub_15C87 proc near ; CODE XREF: sub_10F7A+1B9↑p
seg000:5C87
seg000:5C87 var_14 = word ptr -14h
seg000:5C87 var_12 = dword ptr -12h
seg000:5C87 var_E = dword ptr -0Eh
seg000:5C87 var_A = word ptr -0Ah
seg000:5C87 var_8 = word ptr -8
seg000:5C87 var_6 = byte ptr -6
seg000:5C87 var_5 = byte ptr -5
seg000:5C87 var_4 = byte ptr -4
seg000:5C87 var_3 = byte ptr -3
seg000:5C87 var_2 = byte ptr -2
seg000:5C87 var_1 = byte ptr -1
seg000:5C87 arg_0 = byte ptr 4
seg000:5C87 arg_2 = byte ptr 6
seg000:5C87
seg000:5C87 push bp
seg000:5C88 mov bp, sp
seg000:5C8A sub sp, 14h
seg000:5C8D cmp word_26EFC, sp
seg000:5C91 ja short loc_15C96
seg000:5C93 call F_OVERFLOW@
seg000:5C96 ; ---------------------------------------------------------------------------
seg000:5C96
seg000:5C96 loc_15C96: ; CODE XREF: sub_15C87+A↑j
seg000:5C96 push ss
seg000:5C97 lea ax, [bp+var_A]
seg000:5C9A push ax
seg000:5C9B push ds
seg000:5C9C mov ax, 256Ah ; dseg:256A
seg000:5C9F push ax
seg000:5CA0 mov cx, 4
seg000:5CA3 call near ptr N_SCOPY@
패스워드 입력과 관련된 함수의 시작 부분은 위와 같다. 여기에서 중요한 코드 부분은 seg000:5C9C의 mov ax, 256Ah 이다. 해당 코드는 dseg:256A를 가리키게 되는데 여기에는 입력된 패스워드의 주소 값이 위치한다.
seg000:5D5E mov al, [bp+var_3]
seg000:5D61 mov ah, 0
seg000:5D63 les bx, [bp+var_E]
seg000:5D66 add bx, ax
seg000:5D68 mov al, byte ptr [bp+var_14]
seg000:5D6B mov es:[bx], al ; save password char
seg000:5D6E cmp [bp+var_14], 2Eh ; '.'
seg000:5D72 jnz short loc_15D79
var_3는 입력된 패스워드 문자열의 위치를 나타내는 Offset Index 값이다. 패스워드는 총 6자리이기 때문에 0에서 5까지의 값을 갖게 된다. var_E는 입력 패스워드가 저장될 메모리 주소 값인 256A를 갖는다. bx 레지스터에 오프셋 256A를 저장 후 Index 값이 저장된 ax 레지스터를 더하면 입력된 패스워드가 저장될 주소가 계산된다. seg000:5D6B에서는 입력된 패스워드 문자를 es:[bx] (256A+Index) 위치에 저장한다. 분석편의를 위해 var_E는 inputPass, var_3는 index로 명칭하였다.
입력된 패스워드와 비교될 저장된 패스워드 문자열 (앞으로 패스코드 passcode로 칭하겠다)은 아래의 코드에서 가져온다.
seg000:5F5C mov al, [bp+var_2] ; passCodeIdx
seg000:5F5F mov ah, 0
seg000:5F61 shl ax, 1
seg000:5F63 shl ax, 1
seg000:5F65 mov bx, ax
seg000:5F67 mov ax, [bx+135Eh]
seg000:5F6B mov dx, [bx+135Ch] ; dseg:135C
seg000:5F6F mov word ptr [bp+var_12], dx
seg000:5F72 mov word ptr [bp+var_12+2], ax
seg000:5F75 mov al, 0
seg000:5F77 mov [bp+var_5], al
seg000:5F7A mov [bp+index], al
seg000:5F7D jmp short loc_15FA2
var_2는 passcode의 Index 값을 나타낸다. 다음번 passcode의 주소를 가리키기 위하여 쉬프트 연산으로 *4를 수행한다.
bx+135C는 dseg:135C를 가리키는데 해당 위치에는 아래와 같이 passcode 문자열의 오프셋이 저장된다.
dseg:135B align 2
dseg:135C dd aAccess ; "ACCESS"
dseg:1360 dd aButton ; "BUTTON"
dseg:1364 dd aIsland ; "ISLAND"
dseg:1368 dd aPretty ; "PRETTY"
dseg:136C dd aWinner ; "WINNER"
dseg:1370 dd aZombie ; "ZOMBIE"
dseg:1374 dd aLovely ; "LOVELY"
dseg:1378 dd aSystem ; "SYSTEM"
AX, DX 레지스터에 Passcode 주소(135C, 135E)에 Index 만큼 더한 값을 복사한다. var_12에 4바이트 Passcode 주소(세그먼트:오프셋으로 실제로는 20비트)을 복사하기 위하여 2번에 걸쳐 mov 명령을 수행한다. 분석의 편의를 위하여 var_12는 passcode로 명칭하였다.
설정된 passcode는 아래의 코드에서 입력된 패스워드와 1 Byte씩 비교 작업을 수행한다.
seg000:5F7F mov al, [bp+index]
seg000:5F82 mov ah, 0
seg000:5F84 les bx, [bp+inputPass] ; dseg:25A6
seg000:5F87 add bx, ax
seg000:5F89 mov al, es:[bx]
seg000:5F8C mov dl, [bp+index]
seg000:5F8F mov dh, 0
seg000:5F91 les bx, [bp+passcode] ; dseg:256E + PassIndex
seg000:5F94 add bx, dx
seg000:5F96 cmp al, es:[bx]
seg000:5F99 jz short loc_15F9F ; if (inputpass[idx] == passcode[idx])
seg000:5F9B mov [bp+var_5], 1 ; result (1: Error, 0: OK)
seg000:5F9F
seg000:5F9F loc_15F9F: ; CODE XREF: sub_15C87+312↑j
seg000:5F9F inc [bp+index]
seg000:5FA2
seg000:5FA2 loc_15FA2: ; CODE XREF: sub_15C87+2F6↑j
seg000:5FA2 cmp [bp+index], 6
seg000:5FA6 jnb short loc_15FB1
seg000:5FA8 mov al, [bp+var_5]
seg000:5FAB mov ah, 0
seg000:5FAD or ax, ax
seg000:5FAF jz short loc_15F7F
seg000:5F96에서 패스워드 문자를 비교하고 일치하는 경우 점프(jz) 후 Index 값을 증가시키고 계속 비교를 수행한다.
이 루틴에서 동적 디버거를 사용하여 강제로 점프 시키는 경우 아래와 같이 패스워드 인증 과정을 강제로 우회할 수 있다.
정상적인 패스워드가 입력되지 않더라도 PASSWORD OK가 출력되며 아래와 같이 다음번 스테이지로 이동되었다.
결론
bumpy 게임은 tinyprog에 의하여 실행압축되어 있지만 공개된 unp 도구를 사용하여 쉽게 unpack을 수행하였다. 입력된 패스워드와 저장된 패스워드 모두 평문으로 노출되기 때문에 수월하게 분석할 수 있었다. 이미 인터넷 상에서 쉽게 PASSWORD를 알 수 있기도 하지만 직접 프로그램을 분석 해본다는 점에 의미를 두고자 한다. 아마 bumpy 프로그램을 직접 분석해본 사람이 없지야 않겠지만 이렇게 정리한 사람 또한 있을까 싶어 대단한 작업은 아니지만 공유 해본다.