DOS 크랙 연습 녹정기

개요

녹정기는 의천도룡기와 같은 무협물로 유명한 사조영웅문 시리즈의 작가인 김용의 마지막 작품으로 기존의 정통 무협을 벗어나 무술 실력 대신 잔머리로 살아가는 주인공이 등장하는 코믹 요소가 가미된 무협물이다. 김용 작가의 다른 소설들처럼 무협 판타지적인 요소를 제외하면 실제 역사속의 사건들과 이질감 없이 진행되기 때문에 현실감이 느껴지며 현재에도 고리타분하지 않고 속도감 있게 읽을만 하다. 게임은 김용의 녹정기 소설의 스토리를 그대로 사용하였는데 원작 자체가 매우 재미있기 때문에 게임의 스토리만 따라가더라도 재미있게 즐길 수 있다.

필자는 90년대 중반에 지관에서 출시한 녹정기 게임의 정품을 구매한 바 있으나 현재에는 게임의 소재가 불분명하여 부득히 인터넷상에 공개된 파일을 기반으로 분석을 수행하였다. 다행스럽게도 녹정기 게임의 분석이 매우 간단하였기 때문에 다른 밀려있는 어려운 샘플 대신 해당 분석 내용을 먼저 공개해본다.

문서 출처

이 문서에 대한 저작권을 굳이 주장할 의도는 없으나 글의 작성자와 출처 표기를 위하여 앞으로는 최초 공개된 문서의 주소를 함께 남겨 두려고 한다. 작성자는 과거의 기억과 새로 연습하면서 글을 쓰는 단계이기때문에 작성 글에 오류가 존재할 수 있으며 특별한 언급 없이 계속 글을 수정하고 있기 때문에 다른 경로에서 본 글을 읽었다면 최신의 글을 한번 확인해 주기 바란다.

이 문서의 최신 버전은 https://hacked.mx/431 에 게시되어 있다.

준비

  • DOSBox-x : 도스 프로그램 구동과 디버깅을 위하여 사용
    • Debugger 기능이 포함된 macOS용 바이너리를 사용하였다.
  • IDA Pro : 디스어셈블러, 도스 프로그램의 역공학을 위하여 사용
  • 010 Editor : HEX 파일 에디터
  • 녹정기 게임 (한글판)
    • USEMAP.EXE
      • filesize : 201,288 bytes
      • md5 : 9c51b8a3618e656b26f88d4f7a2f069d

목표

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에는 아래와 같이 정의되어 있는데 각각 EnterSpacebar위 화살표왼쪽 화살표오른쪽 화살표아래 화살표를 의미한다. EnterSpacebar가 눌리는 경우 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 레지스터를 변화시킨다.

가장 중요한 패스워드 검증 루틴은 EnterSpacebar가 눌린 경우에 실행되는 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 시절이 아니기 때문에 좋은 도구는 적극 활용하고 보다 생산적인 일에 집중하는 것이 옳다고 본다. 기회가 된다면 조금 더 난이도 높은 샘플을 분석하고 공유할 수 있기를 기대 해본다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다