개요
녹정기는 의천도룡기와 같은 무협물로 유명한 사조영웅문 시리즈의 작가인 김용의 마지막 작품으로 기존의 정통 무협을 벗어나 무술 실력 대신 잔머리로 살아가는 주인공이 등장하는 코믹 요소가 가미된 무협물이다. 김용 작가의 다른 소설들처럼 무협 판타지적인 요소를 제외하면 실제 역사속의 사건들과 이질감 없이 진행되기 때문에 현실감이 느껴지며 현재에도 고리타분하지 않고 속도감 있게 읽을만 하다. 게임은 김용의 녹정기 소설의 스토리를 그대로 사용하였는데 원작 자체가 매우 재미있기 때문에 게임의 스토리만 따라가더라도 재미있게 즐길 수 있다.
필자는 90년대 중반에 지관에서 출시한 녹정기 게임의 정품을 구매한 바 있으나 현재에는 게임의 소재가 불분명하여 부득히 인터넷상에 공개된 파일을 기반으로 분석을 수행하였다. 다행스럽게도 녹정기 게임의 분석이 매우 간단하였기 때문에 다른 밀려있는 어려운 샘플 대신 해당 분석 내용을 먼저 공개해본다.
문서 출처
이 문서에 대한 저작권을 굳이 주장할 의도는 없으나 글의 작성자와 출처 표기를 위하여 앞으로는 최초 공개된 문서의 주소를 함께 남겨 두려고 한다. 작성자는 과거의 기억과 새로 연습하면서 글을 쓰는 단계이기때문에 작성 글에 오류가 존재할 수 있으며 특별한 언급 없이 계속 글을 수정하고 있기 때문에 다른 경로에서 본 글을 읽었다면 최신의 글을 한번 확인해 주기 바란다.
이 문서의 최신 버전은 https://hacked.mx/431 에 게시되어 있다.
준비
- DOSBox-x : 도스 프로그램 구동과 디버깅을 위하여 사용
- Debugger 기능이 포함된 macOS용 바이너리를 사용하였다.
- IDA Pro : 디스어셈블러, 도스 프로그램의 역공학을 위하여 사용
- 010 Editor : HEX 파일 에디터
- 녹정기 게임 (한글판)
- USEMAP.EXE
- filesize : 201,288 bytes
- md5 : 9c51b8a3618e656b26f88d4f7a2f069d
- USEMAP.EXE
목표
PLAY.BAT 파일로 게임을 구동하면 오프닝 애니메이션 영상이 끝나면 패스워드 입력 화면이 표시된다. 배치 파일에서 처음 실행되는 CARTONV.EXE 파일이 오프닝 영상을 플레이하며 실제 게임은 USEMAP.EXE에서 실행된다. 때문에 CARTONV.EXE 파일은 분석에서 제외하고 USEMAP.EXE 파일에 대해서만 분석을 수행한다.
분석
실행 로그 분석
dosbox-x의 인자에 -debug
를 사용하여 패스워드 화면이 표시되기 까지의 주요 로그를 확인한 결과는 아래와 같다.
LOG: 38704927 EXEC:Execute USEMAP.EXE 0 LOG: 38704927 FILES:file open command 0 file USEMAP.EXE ... LOG: 38720709 FILES:file open command 0 file arrow.img LOG: 38721140 DEBUG DOSMISC:DOS_ResizeMemory(seg=0x0813) blocks=0x38c0 LOG: 38724949 FILES:file open command 0 file .\setup.ini ... LOG: 40006348 FILES:file open command 0 file color.tab LOG: 40006802 FILES:file open command 0 file box.ico LOG: 40007570 FILES:file open command 0 file gameasc.fnt LOG: 40008199 FILES:file open command 0 file kfont.15 LOG: 40008425 FILES:file open command 0 file gr16.fnt LOG: 40027234 FILES:file open command 0 file human.att ... LOG: 40058308 FILES:file open command 0 file box.img LOG: 40058702 DEBUG DOSMISC:DOS_ResizeMemory(seg=0x0813) blocks=0x5380 LOG: 40060461 VGAMISC:Lines left: 92 LOG: 40067554 FILES:file open command 0 file man\pass.ico LOG: 41492809 DEBUG MISC:Silencing PC speaker output (output disabled)
패스워드 입력 이전에 box.img 파일과 man\pass.ico 파일의 오픈이 있음을 주목한다. pass.ico 파일명은 패스워드와 연관되어 있음을 기대할 수 있다.
패스워드가 3번 틀리게 되면 프로그램이 종료된다. 종료 과정에서는 특별히 주목할만한 로그는 남지 않았다.
사전 조사
실제 코드 분석에 앞서 패스워드 입력 과정을 시험하면서 아래와 같은 내용을 확인할 수 있다. 실제 코드 분석 이전에 수행을 통해 알아낸 내용을 정리하고 코드 구현을 예상해 볼 수 있다.
- 패스워드의 갯수(쪽)은 총 35쪽 (23h)이다.
- 패스워드는 총 4자리가 사용된다.
- 각 자리에는 총 10명의 캐릭터 그림이 사용된다. (0 ~ 9)
- 방향키 좌우로 패스워드 자리를 옮길 수 있으며 상하로 위치한 패스워드 캐릭터를 변경할 수 있다.
- 스페이스바와 엔터 키를 사용하여 패스워드 입력을 종료한다.
- 잘못된 패스워드를 입력하더라도 재실행 전까지 처음 결정된 패스워드 쪽은 변경되지 않는다
- 패스워드 입력이 3번 틀리면 프로그램이 종료된다.
패스워드 크랙
IDA로 USEMAP.EXE 파일을 오픈하면 _main 함수를 자동으로 찾아 seg012:08A2 위치를 가리킨다.
_main 함수의 앞 부분에서 아래와 같은 코드를 확인할 수 있다.
seg012:08F6 50 push ax ; seed seg012:08F7 9A 39 0A 00 10 call _srand ; Call Procedure seg012:08FC 59 pop cx seg012:08FD C7 06 46 27 00 80 mov word_3DCD6, 8000h seg012:0903 0E push cs seg012:0904 E8 7A FE call near ptr sub_34641 ; Call Procedure seg012:0907 68 01 80 push 8001h ; access seg012:090A 1E push ds seg012:090B 68 98 24 push offset aColorTab ; "color.tab" seg012:090E 9A 2C 2F 00 10 call _open ; Call Procedure seg012:0913 83 C4 06 add sp, 6 ; Add seg012:0916 8B F8 mov di, ax seg012:0918 68 00 04 push 400h ; len seg012:091B 1E push ds seg012:091C 68 4F 5C push offset unk_411DF ; buf seg012:091F 57 push di ; handle seg012:0920 9A 6F 36 00 10 call j____read ; Call Procedure seg012:0925 83 C4 08 add sp, 8 ; Add seg012:0928 57 push di ; handle seg012:0929 9A E5 27 00 10 call _close ; Call Procedure seg012:092E 59 pop cx seg012:092F 68 01 80 push 8001h ; access seg012:0932 1E push ds seg012:0933 68 A2 24 push offset aBoxIco ; "box.ico" seg012:0936 9A 2C 2F 00 10 call _open ; Call Procedure
_main 함수는 srand를 호출하여 랜덤 값을 위한 seed 값을 생성하고 로그에서 확인된 것처럼 파일을 오픈한다. _main 함수의 코드 아래 부분을 그래프 형태로 표시하여 계속 분석하면 아래와 같은 분기 구문을 찾을 수 있다.
seg012:0A7C E8 A8 F5 call near ptr sub_33EE7 seg012:0A7F 0B C0 or ax, ax seg012:0A81 75 03 jnz short loc_34946 seg012:0A83 E9 CE 01 jmp loc_34B14
코드 부분 중 call near ptr sub_33EE7
호출 이후 분기 구문에서 jnz short loc_36946
명령에 의하여 분기되는 루틴에는 기존에 패스워드 입력 실패시에는 확인되지 않은 파일명이 표시된다. 해당 분기 구문에서 점프되지 않고 빨간색 루트(loc_34B14)를 타는 경우에는 텍스트 환경으로 그래픽 모드를 변경하고 종료된다. 패스워드 검증 실패시에 게임 종료 지점으로 분기되는 코드인 jnz short loc_36946
인 코드 75 03
을 NOP
명령인 90 90
으로 변경한다면 패스워드 검증 성공 여부와 관계 없이 loc_34B14
루틴으로 분기하여 게임이 실행될 것이다.
상세 분석
패스워드 크랙은 쉽게 완료 되었지만 패스워드 루틴을 조금 더 분석 해보겠다. 앞에서 확인한 패스워드 검증 함수인 sub_33EE7는 아래와 같은 코드로 시작한다.
seg012:0027 sub_33EE7 proc far ; CODE XREF: _main+1DA↓p seg012:0027 seg012:0027 buffer = byte ptr -58h seg012:0027 var_3A = word ptr -3Ah seg012:0027 var_38 = word ptr -38h seg012:0027 var_36 = word ptr -36h seg012:0027 var_34 = word ptr -34h seg012:0027 var_32 = word ptr -32h seg012:0027 var_30 = word ptr -30h seg012:0027 handle = word ptr -2Eh seg012:0027 var_2C = word ptr -2Ch seg012:0027 var_2A = byte ptr -2Ah seg012:0027 var_29 = byte ptr -29h seg012:0027 var_28 = word ptr -28h seg012:0027 var_1C = word ptr -1Ch seg012:0027 buf = byte ptr -0Ah seg012:0027 seg012:0027 enter 58h, 0 seg012:002B push si seg012:002C push di seg012:002D mov ax, seg seg012 seg012:0030 mov bx, 19h seg012:0033 call sub_116FD seg012:0038 mov [bp+var_29], 0FFh seg012:003C mov [bp+var_2A], 0FFh seg012:0040 call _rand seg012:0045 cwde seg012:0047 imul eax, 23h seg012:004B mov ebx, 8000h seg012:0051 cdq seg012:0053 idiv ebx seg012:0056 mov [bp+var_2C], ax
_rand 함수를 호출하고 결과 값에 23h 만큼 곱한다. 23h라는 값이 의미있어 보이는데 23h는 10진수로 35이며 패스워드를 질문하는 최대 횟수 (쪽)에 해당한다. 곱한 값에 8000h를 나눈 값을 var_2C에 저장한다. var_2C는 패스워드 쪽에 해당하는 값으로 passNumber로 정의할 수 있다. _rand 함수는 0부터 최대 7FFFh 값을 반환하는데 이를 23h (35) 만큼 곱하고 8000h 만큼 나누면 var_2C는 0에서 34사이의 랜덤 값을 갖는다. 랜덤 식이 혼동된다면 계산기 프로그램에서 비트 값을 보며 연산하면 이해할 수 있다.
이후의 코드에서는 box.img와 pass.ico 파일을 오픈하고 메모리에 읽어온다.
seg012:0076 push 0 ; int seg012:0078 push ds seg012:0079 push offset aBoxImg ; "box.img" seg012:007C push ss seg012:007D lea ax, [bp+buf] seg012:0080 push ax ; buf seg012:0081 call sub_25E0B seg012:0086 add sp, 0Ah seg012:0089 inc dword ptr ss:10h seg012:008F push 3E80h seg012:0092 push large 0 seg012:0095 push word ptr dword_44710+2 seg012:0099 push word ptr dword_44710 seg012:009D call sub_3190A seg012:00A2 add sp, 0Ah seg012:00A5 push 3E80h seg012:00A8 push large 0 seg012:00AB push word ptr dword_3EA48+2 seg012:00AF push word ptr dword_3EA48 seg012:00B3 call sub_3190A seg012:00B8 add sp, 0Ah seg012:00BB push 0 seg012:00BD call sub_3255D seg012:00C2 pop cx seg012:00C3 mov eax, dword_46634 seg012:00C7 mov dword_3FC92, eax seg012:00CB push 8001h ; access seg012:00CE push ds seg012:00CF push offset aManPassIco ; "man\\pass.ico" seg012:00D2 call _open seg012:00D7 add sp, 6 seg012:00DA mov [bp+handle], ax seg012:00DD push 1680h ; len seg012:00E0 push word ptr dword_3FC92+2 seg012:00E4 push word ptr dword_3FC92 ; buf seg012:00E8 push [bp+handle] ; handle seg012:00EB call j____read seg012:00F0 add sp, 8 seg012:00F3 push [bp+handle] ; handle seg012:00F6 call _close
파일 처리 코드는 패스워드 검증 루틴과 직접 연관이 없기 때문에 해당 부분의 분석은 생략한다.
seg012:00FC loc_33FBC: ; CODE XREF: sub_33EE7+310↓j seg012:00FC mov ax, [bp+var_2C] seg012:00FF inc ax seg012:0100 push ax seg012:0101 push ds seg012:0102 push offset aA_1 ; "Á" seg012:0105 push ss seg012:0106 lea ax, [bp+buffer] seg012:0109 push ax ; buffer seg012:010A call _sprintf seg012:010F add sp, 0Ah
loc_33FBC 지점의 코드에서 sprintf 함수를 사용하여 문자열을 복사한다. IDA에는 소스 문자열을 정확하게 표시하지 않고 있다.
필자는 오랫동안 IDA의 라이센스를 계속 유지하고 있지만 분석에 사용한 Macbook에 설치된 구버전의 사용자 라이센스 IDA 7.0에는 IDA가 강제 종료되는 한글 입력과 관련된 버그가 존재하여 분석 과정이 다소 거슬렸다. 이후의 IDA에는 한글 입력과 관련된 버그가 수정되고 유니코드 다국어 문자열도 확인할 수 있으나 게임에는 UTF-8 대신 과거의 완성형 한글 코드가 사용되었기 때문에 IDA에서는 한글 메시지가 표시되지 않는다.
완성형 코드 표기가 지원되는 에디터를 사용하여 해당 위치를 살펴본 결과는 아래와 같다.
seg012:0102 코드에서 참조되는 문자열은 제 %2d 쪽 비밀번호입력
이다. sprintf 인자에 사용되는 var_2C 값은 앞에서 _rand 함수로 구해진 패스워드 쪽이다. _rand 함수의 결과 값은 0부터 시작하기 때문에 출력 전에 +1을 먼저하였다.
seg012:0112 seg012:0112 loc_33FD2: ; CODE XREF: sub_33EE7+26F↓j seg012:0112 cmp [bp+var_2A], 0 seg012:0116 jz short loc_33FE4 seg012:0118 mov al, [bp+var_2A] seg012:011B mov [bp+var_29], al seg012:011E mov [bp+var_2A], 0 seg012:0122 jmp short loc_33FEA seg012:0124 ; --------------------------------------------------------------------------- seg012:0124 seg012:0124 loc_33FE4: ; CODE XREF: sub_33EE7+EF↑j seg012:0124 mov al, byte_44718 seg012:0127 mov [bp+var_29], al seg012:012A seg012:012A loc_33FEA: ; CODE XREF: sub_33EE7+FB↑j seg012:012A mov al, [bp+var_29] seg012:012D mov ah, 0 seg012:012F cmp ax, 80h ; '€' seg012:0132 jge loc_34091 seg012:0136 mov al, [bp+var_29] seg012:0139 mov ah, 0 seg012:013B mov [bp+var_3A], ax seg012:013E mov cx, 6 ; switch 6 cases seg012:0141 mov bx, offset word_34257 seg012:0144 seg012:0144 loc_34004: ; CODE XREF: sub_33EE7+128↓j seg012:0144 mov ax, cs:[bx] seg012:0147 cmp ax, [bp+var_3A] seg012:014A jz short loc_34013 seg012:014C add bx, 2 seg012:014F loop loc_34004 seg012:0151 jmp short loc_3408C ; jumptable 00034013 default case
패스워드 쪽 문자열을 완성 후 이후의 코드에서는 키 입력 여부를 검사하고 입력된 키를 처리한다. var_2A, var_29에는 키가 입력되지 않았다면 최초 초기 값인 FFh가 사용되기 때문에 seg012:0132 지점에서 loc_34091로 점프한다. 현재까지는 키가 입력되지 않은 것으로 가정하기 때문에 키 입력 부분은 이후에 다시 살펴보기로 한다.
seg012:01CC loc_3408C: ; CODE XREF: sub_33EE7+12A↑j seg012:01CC ; sub_33EE7:loc_34042↑j ... seg012:01CC call sub_278C6 ; jumptable 00034013 default case seg012:01D1 seg012:01D1 loc_34091: ; CODE XREF: sub_33EE7+10B↑j seg012:01D1 mov eax, dword_3EA48 seg012:01D5 mov dword_44710, eax seg012:01D9 push 3E80h seg012:01DC push large 0 seg012:01DF push word ptr dword_3EA48+2 seg012:01E3 push word ptr dword_3EA48 seg012:01E7 call sub_3190A seg012:01EC add sp, 0Ah seg012:01EF push 0 seg012:01F1 push 0Fh seg012:01F3 push ds seg012:01F4 push offset aACOcaICEcoAU ; "Á¤Ç° ¼ÒÇÁÆ®¿þ¾î¸¦ »ç¿ëÇսô٣®" seg012:01F7 push 0Ah seg012:01F9 push 0Ah seg012:01FB call sub_31F4F seg012:0200 add sp, 0Ch seg012:0203 push 0 seg012:0205 push 0Fh seg012:0207 push ss seg012:0208 lea ax, [bp+buffer] seg012:020B push ax seg012:020C push 1Eh seg012:020E push 0Ah seg012:0210 call sub_31F4F seg012:0215 add sp, 0Ch seg012:0218 push 0 seg012:021A call sub_3255D seg012:021F pop cx seg012:0220 push 57h ; 'W' seg012:0222 mov ax, si seg012:0224 imul ax, 1Eh seg012:0227 add ax, 0Dh seg012:022A push ax seg012:022B push ss seg012:022C lea ax, [bp+buf] seg012:022F push ax seg012:0230 call sub_2613A seg012:0235 add sp, 8 seg012:0238 xor di, di seg012:023A jmp short loc_3412D
loc_34091에서는 패스워드 입력 화면을 출력한다. sub_31F4F 함수는 문자열을 화면에 출력하는 TextOut 기능의 함수로서 X, Y 위치 좌표와 출력 문자열 주소를 인자로 사용한다. seg012:01FB의 출력 문자열은 '정품 소프트웨어를 사용합시다'
이며 seg012:0210은 입력 패스워드 쪽과 관련된 문자열이다.
seg012:023C loc_340FC: ; CODE XREF: sub_33EE7+249↓j seg012:023C mov bx, di seg012:023E add bx, bx seg012:0240 lea ax, [bp+var_38] seg012:0243 add bx, ax seg012:0245 push word ptr ss:[bx] seg012:0248 mov ax, di seg012:024A imul ax, 1Eh seg012:024D mov dx, word ptr dword_3EA48 seg012:0251 add dx, ax seg012:0253 add dx, 6E0Eh seg012:0257 push word ptr dword_3EA48+2 seg012:025B push dx seg012:025C push word ptr dword_3FC92+2 seg012:0260 push word ptr dword_3FC92 seg012:0264 call sub_18BAA seg012:0269 add sp, 0Ah seg012:026C inc di seg012:026D seg012:026D loc_3412D: ; CODE XREF: sub_33EE7+213↑j seg012:026D cmp di, 4 seg012:0270 jl short loc_340FC seg012:0272 mov dword_44710, 0A0000000h seg012:027B push 23A0h seg012:027E push word ptr dword_3EA48+2 seg012:0282 push word ptr dword_3EA48 seg012:0286 push word ptr dword_44710+2 seg012:028A push word ptr dword_44710 seg012:028E call sub_318F3 seg012:0293 add sp, 0Ah seg012:0296 jmp loc_33FD2
di 값을 index로 사용하는 var_38 스택 변수는 입력 패스워드가 저장되는 스택 영역이다. 해당 부분은 신경 쓰지 않고 seg012:0296의 jmp loc_33FD2를 따라간다.
loc_33FD2은 이미 위에서 확인한 키 입력 처리 루틴 부분이다. var_2A는 앞에서 0으로 초기화 되었기 때문에 seg012:0116
의 점프 루틴에서 loc_33FE4로 점프한다. byte_44718
에는 입력 키의 Scan Code가 저장된다. 키보드가 눌린 시점에는 80h 보다 작은 값을 같기 때문에 seg012:0132
에서 loc_34091로 점프하지 않고 계속 실행한다. loc_34091은 이전에 분석한 바와 같이 패스워드 입력과 관련된 문자열이 출력되는 코드이다.
입력된 키는 offset word_34257
에 정의된 6개의 키와 비교를 수행하는데 해당되지 않는 경우에는 loc_3408C로 점프 호출되는 sub_278C6 함수에서 키 입력을 대기한다.
seg003:0046 sub_278C6 proc far ; CODE XREF: sub_1C5E4+8↑P seg003:0046 ; sub_1E03F+1BD↑P ... seg003:0046 push bp seg003:0047 mov bp, sp seg003:0049 jmp short $+2 seg003:004B ; --------------------------------------------------------------------------- seg003:004B seg003:004B loc_278CB: ; CODE XREF: sub_278C6+3↑j seg003:004B ; sub_278C6+D↓j seg003:004B mov al, byte_44718 seg003:004E mov ah, 0 seg003:0050 cmp ax, 80h ; '€' seg003:0053 jl short loc_278CB seg003:0055 pop bp seg003:0056 retf seg003:0056 sub_278C6 endp
offset word_34257
에는 아래와 같이 정의되어 있는데 각각 Enter
, Spacebar
, 위 화살표
, 왼쪽 화살표
, 오른쪽 화살표
, 아래 화살표
를 의미한다. Enter
, Spacebar
가 눌리는 경우 loc_34089
가 실행되며 화살표 키 순서에 따라 정의된 코드가 실행된다.
seg012:0397 word_34257 dw 1Ch, 39h, 48h, 4Bh seg012:0397 ; DATA XREF: sub_33EE7+11A↑o seg012:0397 dw 4Dh, 50h ; value table for switch statement seg012:03A3 dw offset loc_34089 ; jump table for switch statement seg012:03A3 dw offset loc_34089 seg012:03A3 dw offset loc_34017 seg012:03A3 dw offset loc_34071 seg012:03A3 dw offset loc_3407D seg012:03A3 dw offset loc_34044
위 화살표가 눌리면 코드 loc_34017이 실행된다.
seg012:0157 loc_34017: ; CODE XREF: sub_33EE7:loc_34013↑j seg012:0157 ; DATA XREF: seg012:03A3↓o seg012:0157 mov bx, si ; jumptable 00034013 case 72 seg012:0159 add bx, bx seg012:015B lea ax, [bp+var_38] seg012:015E add bx, ax seg012:0160 cmp word ptr ss:[bx], 0 seg012:0164 jnz short loc_34036 seg012:0166 mov bx, si seg012:0168 add bx, bx seg012:016A lea ax, [bp+var_38] seg012:016D add bx, ax seg012:016F mov word ptr ss:[bx], 9 seg012:0174 jmp short loc_34042 seg012:0176 ; --------------------------------------------------------------------------- seg012:0176 seg012:0176 loc_34036: ; CODE XREF: sub_33EE7+13D↑j seg012:0176 mov bx, si seg012:0178 add bx, bx seg012:017A lea ax, [bp+var_38] seg012:017D add bx, ax seg012:017F dec word ptr ss:[bx] seg012:0182 seg012:0182 loc_34042: ; CODE XREF: sub_33EE7+14D↑j seg012:0182 jmp short loc_3408C ; jumptable 00034013 default case
si 레지스터는 4자리 패스워드의 자리 위치를 나타낸다. WORD 단위로 메모리를 이동하기 위하여 bx 레지스터 연산을 수행하고 [bp+var_38]에서 패스워드 첫 자리 주소를 ax 레지스터에 저장한다. 자리 위치만큼 이동 후 현재 값이 0보다 크다면 1만큼 값을 감소 시키고 0인 경우에는 9를 설정한다. 마지막으로 loc_3408C로 점프하여 키 입력 이벤트(키 떼어짐)를 대기한다. 아래 화살표의 경우에는 위 화살표와는 반대로 값을 감소하는 대신 증가 시킨다.
좌우 화살표의 경우 패스워드의 자리 위치 값인 si 레지스터를 변화시킨다.
가장 중요한 패스워드 검증 루틴은 Enter
, Spacebar
가 눌린 경우에 실행되는 loc_34089
루틴이다.
seg012:01C9 loc_34089: ; CODE XREF: sub_33EE7:loc_34013↑j seg012:01C9 ; DATA XREF: seg012:03A3↓o seg012:01C9 jmp loc_34159 ; jumptable 00034013 cases 28,57
loc_34089는 단순히 loc_34159로의 점프를 수행한다.
seg012:0299 loc_34159: ; CODE XREF: sub_33EE7:loc_34089↑j seg012:0299 mov ax, [bp+var_38] seg012:029C imul ax, 3E8h seg012:02A0 mov dx, [bp+var_36] seg012:02A3 imul dx, 64h seg012:02A6 add ax, dx seg012:02A8 mov dx, [bp+var_34] seg012:02AB imul dx, 0Ah seg012:02AE add ax, dx seg012:02B0 add ax, [bp+var_32] seg012:02B3 mov di, ax seg012:02B5 mov bx, [bp+var_2C] seg012:02B8 add bx, bx seg012:02BA cmp [bx+235Eh], di seg012:02BE jz loc_3424E seg012:02C2 cmp [bp+var_30], 1 seg012:02C6 jle short loc_341FA seg012:02C8 dec [bp+var_30] seg012:02CB push 1900h seg012:02CE push large 0 seg012:02D1 mov ax, word ptr dword_44710 seg012:02D4 add ax, 8E80h seg012:02D7 push word ptr dword_44710+2 seg012:02DB push ax seg012:02DC call sub_3190A seg012:02E1 add sp, 0Ah seg012:02E4 push 0 seg012:02E6 push 0Fh seg012:02E8 push ds seg012:02E9 push offset aAOucUAIU ; "À߸øµÇ¾ú½À´Ï´Ù£¡" seg012:02EC push 8Ch ; 'Œ' seg012:02EF push 0Ah seg012:02F1 call sub_31F4F seg012:02F6 add sp, 0Ch seg012:02F9 push [bp+var_30] seg012:02FC push ds seg012:02FD push offset aAuAe2dE ; "ÀÜ¿©±âȸ %2d ȸ£¡" seg012:0300 push ss seg012:0301 lea ax, [bp+buffer] seg012:0304 push ax ; buffer seg012:0305 call _sprintf seg012:030A add sp, 0Ah seg012:030D push 0 seg012:030F push 0Fh seg012:0311 push ss seg012:0312 lea ax, [bp+buffer] seg012:0315 push ax seg012:0316 push 0A0h ; ' ' seg012:0319 push 0Ah seg012:031B call sub_31F4F seg012:0320 add sp, 0Ch seg012:0323 push 12Ch ; milliseconds seg012:0326 call _delay seg012:032B pop cx seg012:032C mov al, 0FFh seg012:032E mov byte_44718, al seg012:0331 mov [bp+var_2A], al seg012:0334 mov [bp+var_29], al seg012:0337 jmp loc_33FBC
var_38, var_36, var_34, var_32는 각각 첫 번째부터 네 번째 자리의 입력된 패스워드를 가리킨다. var_2C는 이전에 이미 분석된 바와 같이 _rand 함수에 의하여 생성된 패스워드 쪽이다. 문서에는 변수명을 따로 바꾸지 않은 코드를 예시로 사용하였지만 실제 분석 과정에는 미리 변수명을 설정해두면 분석이 훨씬 수월하다. 입력된 패스워드는 자리수에 따라 각각 3E8h(1000), 64h(100), 0Ah(10)를 곱하고 합하여 최종적으로 4자리 수로 완성된다.
완성된 입력 패스워드는 [bx+235Eh]의 값과 비교한다. bx 레지스터는 var_2C 변수의 패스워드 쪽 값이며 출력된 수와는 달리 랜덤 값인 0부터 시작한다. bx 레지스터 값에 add 작업을 한번 수행하여 WORD 단위의 패스워드 오프셋 값을 가리킨다.
아래의 그림의 패스워드 검증 루틴에서 패스워드를 스택에 09 00 08 00 07 00 06 00
의 형태로 입력 후 비교하는 장면을 설명한다. 패스워드 입력 화면의 문자열은 재미를 위하여 바꾸어 보았다.
패스워드 검증에 실패하는 경우에는 패스워드 입력 잔여 회수를 나타내는 var_30 값을 비교하여 횟수가 남은 경우에는 값을 감소시키고 문자열을 출력하는 함수인 sub_31F4F를 호출하여 잘못되었습니다!
, 잔여기회 2회!
와 같은 오류 메시지를 출력한다.
[bx+235Eh]에는 패스워드 값이 존재하는데 그 값은 아래와 같다.
dseg:235E word_3D8EE dw 4D2h ; DATA XREF: sub_33EE7+293↑r dseg:2360 dw 5C0h dseg:2362 dw 14DEh dseg:2364 dw 929h dseg:2366 dw 9AEh dseg:2368 dw 1F10h
dseg:235E에서부터 WORD 단위로 패스워드 값이 기록되는데 1쪽 패스워드인 0번째 오프셋의 값은 4D2h로 1234이다. 게임 구동시 최초 패스워드 상태는 0000이기 때문에 1쪽의 패스워드는 첫 번째 캐릭터에서 아래 화살표 한번, 두 번째 캐릭터는 두번, 세 번째는 세번, 네 번째는 네 번을 입력하면 1234의 값이 되어 검증을 통과할 수 있다. 이와 같은 방법으로 모든 패스워드의 정답을 알아낼 수 있다.
참고로 패스워드 입력 첫 화면의 캐릭터는 주인공인 위소보로 0 값을 갖는데 0이 사용되는 패스워드는 없기 때문에 패스워드 중 위소보가 사용되는 사례는 없다. 그러나 패스워드 영역을 모두 0으로 수정한다면 처음 상태에서 엔터를 치는것만으로도 검증을 우회할 수 있다. 우아한 방법은 아니지만 여러가지 크랙의 방법을 재미있게 생각할 수 있다.
검증에 성공하는 경우에는 seg012:02BE에서 loc_3424E 루틴으로 점프하여 ax 레지스터에 1을 셋팅한 후 함수를 종료한다.
seg012:038E loc_3424E: ; CODE XREF: sub_33EE7+297↑j seg012:038E mov ax, 1 seg012:0391 jmp short loc_34232 seg012:0393 ; --------------------------------------------------------------------------- seg012:0393 seg012:0393 loc_34253: ; CODE XREF: sub_33EE7+363↑j seg012:0393 ; sub_33EE7+365↑j seg012:0393 pop di seg012:0394 pop si seg012:0395 leave seg012:0396 retf seg012:0396 sub_33EE7 endp
본 분석 과정에서는 패스워드 크랙과 무관한 루틴인 그래픽 출력과 키 입력에 대해서는 상세 분석을 수행하지 않았기 때문에 문서에서는 해당 내용을 자세히 다루지 않았다. 참고로 패스워드 체크 루틴인 sub_33EE7 함수 호출 이전에 불려지는 sub_27934 함수는 키보드 인터럽트(Interrupt)를 seg003:00F9로 후킹한다. 게임상에서의 키 입력에 대해 더 분석하고자 한다면 해당 함수와 기타 관련된 코드들을 살펴볼 수 있다. 키보드 입력을 대기하기 위해서는 seg003:0046 함수를 호출한다. 해당 함수는 인터럽트 후킹 함수 sub_33EE7에서 키 입력이 들어올때까지 대기한다. 키가 입력되면 해당 키를 비교해서 80h 값보다 큰 경우에(눌린 키가 떼어지는 이벤트) 값을 리턴한다.
결론
도스 게임 크랙을 위하여 블로그를 개설한것은 아니나 필자의 주 관심사 중 하나인 보안 분야는 워낙에 쟁쟁한 사람들이 많고 좋은 글들을 많이 올리고 있기 때문에 요근래 레트로에 관심을 갖고 있는 필자는 도스 크랙과 관련된 이야기를 몇개 더 써볼 생각에 있다. 30여년전의 클랙식 게임들이라고 해서 만만하게 보기엔 최근의 안티 리버싱 기법과 유사한 기술들이 그 당시에도 이미 적용되어 있어 생각보다 어려움에 부딪혀 당황스러운 점이 있다.
이번 분석 샘플은 매우 간단하게 해결 하였지만 현대의 분석 도구인 IDA Pro가 너무 막강하기 때문에 손쉬운 점도 있다. 그러나 과거의 DOS 시절이 아니기 때문에 좋은 도구는 적극 활용하고 보다 생산적인 일에 집중하는 것이 옳다고 본다. 기회가 된다면 조금 더 난이도 높은 샘플을 분석하고 공유할 수 있기를 기대 해본다.