DOS 크랙 연습 에올의 모험

개요

1990년대 DOS 시절 국내 초창기 게임 업체인 패밀리 프로덕션에서 제작한 에올의 모험이라는 게임의 복사 방지 로직을 분석 해본다. 패밀리 프로덕션은 국내 TV 방송사인 KBS의 게임천국에서도 나온바가 있는 피와 기티의 개발사로도 잘 알려져 있다.

인터넷 상에서 쉽게 입수되는 에올의 모험 게임은 DOSBox에서 구동하면 복제 방지가 동작되지 않는다. 같은 게임을 DOSBox-X에서 구동 하였을 때는 복제 방지가 적용되었다. 국산 게임이지만 인터넷 상에서 폭넓게 공유되고 있고 출시된지 20년 이상 지난 게임이기 때문에 분석을 통해 해당 원인을 찾아본다.

준비

  • DOSBox와 DOSBox-X : 도스 프로그램 구동과 디버깅을 위하여 사용
    • 굳이 두개의 도스 에뮬레이션 프로그램을 동시에 사용한 이유는 DOSBox와는 달리 DOSBox-x에서는 배포된 게임이 정상 실행되지 않기 때문에 비교를 위해서 사용하였다.
  • MS-DOS : 필수는 아니다. 실제 도스에서의 구동을 비교해보기 위함이지만 본문에서도 실 환경에서 직접 분석을 수행하지는 않았다. 추가로 분석을 원하는 경우 사용해 볼 수 있다.
  • IDA Pro : 디스어셈블러, 도스 프로그램의 역공학을 위하여 사용
    • 상용 프로그램이기 때문에 필수는 아니지만 사용할 수 있다면 매우 쉽게 분석이 가능하다.
    • 프로그램이 없는 경우에는 도스용 디스어셈블러인 SOURCER 또는 Ghidra로 대체할 수 있다.
  • 010 Editor : 파일 에디터
  • 에올의 모험 게임 : 정품 설치판이 아닌 인터넷 상에서 유포 중인 파일을 대상으로 분석을 진행하였다.
    • EOL_GAME.EXE
      • filesize : 411,200 bytes
      • md5 : ee29990344cbf7047f2c3cbe306dc6ee
    • HIGHSCR.EXE
      • filesize : 7,184 bytes
      • md5 : ac18161572ffe0da0aa3fb533f4ca77d

목표 확인

IDA로 실행 파일인 EOL_GAME.EXE 를 오픈하면 크랙이 적용되지 않을시에 표시되는 메세지인 Plase Don't Copy!가 표시되는 루틴이 너무나도 쉽게 보이기 때문에 크랙 자체는 매우 쉽다.

에올의 모험 프로텍트 분기 루틴

분기 구문의 단순 수정으로 가능한 크랙 제작보다는 프로텍트 기능을 조금 더 분석 해보도록 하겠다

분석

코드 예상

코드 분석을 하기 이전에 DOSBox의 로그 기능을 활용하여 파일 관련 기능과 일부 인터럽트의 호출을 파악하였다. 파악된 정보를 토대로 아래와 같이 정리를 해보았다.

해당 내용은 실행 환경에 따라 달라질 수 있으며 동작 확인을 위하여 편집된 내용으로 실제와는 일부 차이가 있을 수 있다.

DOSBox에서의 실행

에올의 모험 게임은 EOL.BAT 파일로 게임을 실행하지만 해당 파일에는 게임 오프닝 파일인 OPENING.EXE 파일을 EOL_GAME.EXE 구동 전 먼저 실행하는 기능외에는 특별한 부분이 없기 때문에 분석 과정에서는 EOL_GAME.EXE 파일을 직접 실행하고 있다. DOSBox에서 게임을 실행하면 정상적으로 게임이 구동된다.

  30700391: EXEC:Execute EOL_GAME.EXE 0
  30700391: FILES:file open command 0 file EOL_GAME.EXE
  30898267: DOSMISC:DOS:Multiplex Unhandled call 1687
  30898398: FILES:Special file open command C0 file C:\EOL\EOL_GAME.EXE
  30898531: DOSMISC:DOS:Multiplex Unhandled call 1687
DOSBox switched to max cycles, because of the setting: cycles=auto. If the game
runs too fast try a fixed cycles amount in DOSBox's options.
  30902742: MOUSE:New video is the same as the old
  31331013: MISC:DOS:0x37:Call for not supported switchchar
  31331028: EXEC:Execute Z:\COMMAND.COM 0
  31331028: FILES:file open command 0 file Z:\COMMAND.COM
  31331028: EXEC:Parsing command line:  HIGHSCR.EXE
  31331028: EXEC:Execute HIGHSCR.EXE 0
  31331028: FILES:file open command 0 file HIGHSCR.EXE
  31331031: FILES:file open command 2 file DRUG.IND
  31331031: DOSMISC:int 25 called but not as diskdetection drive 2
  31331031: FILES:file open command 2 file EOL.CFG
  31331069: FILES:file open command 2 file DRUG.IND
  31331081: INT10:Set Video Mode 13
  31331081: VGA:Blinking 0
  31331088: SBLASTER:DSP:Reset
  31331092: FILES:file open command 2 file eol.fx5
  31331103: VGA:h total 100 end 80 blank (80/98) retrace (84/96)
  31331103: VGA:v total 449 end 400 blank (407/442) retrace (412/414)
9/0.03051): VGA:h total 0.03178 (31.47kHz) blank(0.02542/0.03114) retrace(0.0266
9235/13.15591)A:v total 14.26806 (70.09Hz) blank(12.93347/14.04568) retrace(13.0
  31331103: VGA:Width 320, Height 200, fps 70.086303
  31331103: VGA:double width, double height aspect 1.200000
  31331109: FILES:file open command 2 file bullet.fx5
  31331126: FILES:file open command 2 file font.fx5
  31331140: FILES:file open command 2 file 00.eft

DOSBox-X에서의 게임 실행 과정 분석

DOSBox-X에서 게임을 실행하면 Please Don't Copy 메시지를 출력하고 게임을 종료한다. 상세한 로그 확인을 위하여 -debug -log-int21 -log-con 옵션을 추가하여 실행한 결과는 아래와 같다. 편의를 위하여 일부 불필요한 내용은 편집하였다.

LOG:   53612288 DEBUG EXEC:Parsing command line: EOL_GAME.EXE
LOG:   53612291 DEBUG DOSMISC:Executing interrupt 21, ah=4b, al=0
LOG:   53612291       EXEC:Execute EOL_GAME.EXE 0
LOG:   53612291       FILES:file open command 0 file EOL_GAME.EXE
LOG:   53612291 DEBUG DOSMISC:DOS_AllocateMemory(blocks=0x0048) = 0x07ca-0x0811
LOG:   53612291 DEBUG DOSMISC:DOS_AllocateMemory(blocks=0x97ec) = 0x0813-0x9ffe
LOG:   53811375 DEBUG DOSMISC:DOS:INT 2F Unhandled call AX=1687
LOG:   53811546 DEBUG DOSMISC:Executing interrupt 21, ah=3d, al=c0
LOG:   53811546       FILES:Special file open command C0 file C:\EOL\EOL_GAME.EXE
LOG:   53811587 DEBUG DOSMISC:Executing interrupt 21, ah=3f, al=5
LOG:  123260202       EXEC:Execute Z:\COMMAND.COM 0
LOG:  123260202       FILES:file open command 0 file Z:\COMMAND.COM
LOG:  123260202 DEBUG DOSMISC:DOS_AllocateMemory(blocks=0x0048) = 0x6051-0x6098
LOG:  123260202 DEBUG DOSMISC:DOS_AllocateMemory(blocks=0x3f65) = 0x609a-0x9ffe
LOG:  123260299 DEBUG DOSMISC:Executing interrupt 21, ah=4a, al=0
LOG:  123260299 DEBUG DOSMISC:DOS_ResizeMemory(seg=0x609a) blocks=0x0040
LOG:  123260363 DEBUG EXEC:Parsing command line:  HIGHSCR.EXE
LOG:  123260428 DEBUG DOSMISC:Executing interrupt 21, ah=4b, al=0
LOG:  123260428       EXEC:Execute HIGHSCR.EXE 0
LOG:  123260428       FILES:file open command 0 file HIGHSCR.EXE
LOG:  123260428 DEBUG DOSMISC:DOS_AllocateMemory(blocks=0x0048) = 0x60db-0x6122
LOG:  123260428 DEBUG DOSMISC:DOS_AllocateMemory(blocks=0x3edb) = 0x6124-0x9ffe
LOG:  123260525 DEBUG DOSMISC:Executing interrupt 21, ah=30, al=0
LOG:  123267390 DEBUG DOSMISC:Executing interrupt 21, ah=3d, al=2
LOG:  123267390       FILES:file open command 2 file DRUG.IND
LOG:  123277951 DEBUG DOSMISC:Executing interrupt 21, ah=44, al=0
LOG:  123278464 DEBUG DOSMISC:Executing interrupt 21, ah=19, al=d2
LOG:  123278817       DOSMISC:int 25 called but not as disk detection drive 2
LOG:  123279522 DEBUG DOSMISC:Executing interrupt 21, ah=40, al=3
LOG:  123279907 DEBUG DOSMISC:Executing interrupt 21, ah=3e, al=2
LOG:  123287948 DEBUG DOSMISC:Executing interrupt 21, ah=4c, al=0
LOG:  123287954 DEBUG DOSMISC:Executing interrupt 21, ah=4c, al=0
LOG:  123289045 DEBUG DOSMISC:Executing interrupt 21, ah=49, al=1
LOG:  123290624 DEBUG DOSMISC:Executing interrupt 21, ah=4d, al=0
LOG:  123293321 DEBUG DOSMISC:Executing interrupt 21, ah=3d, al=2
LOG:  123293321       FILES:file open command 2 file DRUG.IND
LOG:  123295283 DEBUG DOSMISC:Executing interrupt 21, ah=44, al=0
LOG:  123296349 DEBUG DOSMISC:Executing interrupt 21, ah=44, al=0
LOG:  123297159 DEBUG DOSMISC:Executing interrupt 21, ah=44, al=0
LOG:  123298449 DEBUG DOSMISC:Executing interrupt 21, ah=42, al=1
LOG:  123299323 DEBUG DOSMISC:Executing interrupt 21, ah=42, al=2
LOG:  123300229 DEBUG DOSMISC:Executing interrupt 21, ah=42, al=0
LOG:  123301614 DEBUG DOSMISC:Executing interrupt 21, ah=3f, al=0
LOG:  123302872 DEBUG DOSMISC:Executing interrupt 21, ah=3e, al=5
LOG:  123304930 DEBUG DOSMISC:Executing interrupt 21, ah=44, al=0
LOG:  123308398 DEBUG DOSMISC:Executing interrupt 21, ah=44, al=0
LOG:  123309753 DEBUG DOSMISC:Executing interrupt 21, ah=40, al=14
LOG: DOS CON: Please Don`t Copy!
LOG:  123315575 DEBUG DOSMISC:Executing interrupt 21, ah=42, al=1
LOG:  123316545 DEBUG DOSMISC:Executing interrupt 21, ah=42, al=0
LOG:  123324657 DEBUG DOSMISC:Executing interrupt 21, ah=42, al=0
LOG:  123327355 DEBUG DOSMISC:Executing interrupt 21, ah=4c, al=0

EOL_GAME을 실행하면

  1. HIGHSCR.EXE를 실행하고 (int 21h, ah=4bh)
  2. 디스크 읽기 작업과 관련된 int 25h를 호출하고 (drive 2: ‘C’)
  3. DRUG.IND 파일을 오픈(int 21h, ah=3dh)하고 무언가 쓰기 작업(int 21h, ah=40h)을 수행하고 파일 핸들을 닫는다.(int 21h, ah=3eh)
  4. HIGHSCR.EXE가 종료된 후 EOL_GAME.EXE는 DRUG.IND를 읽고 (int 21h, ah=3fh)
  5. Please Don't Copy 메시지를 출력하고 종료 (int 21h, ah=4ch) 한다.

처음 DOSBox의 실행 로그와 비교해보면 DRUG.IND를 읽고 쓰는 과정 사이에서 EOL.CFG를 읽는 과정이 빠져있다. 확실하지는 않지만 해당 부분과 프로텍트의 동작이 매우 밀접하게 보여진다.

상세한 프로텍트의 동작 분석을 위하여 EOL_GAME.EXE의 코드를 분석해본다.

EOL_GAME.EXE 코드 분석

EOL_GAME.EXE 파일을 macOS에서 file 명령을 사용하여 확인한 결과는 아래와 같다.

➜  eol file EOL_GAME.EXE
EOL_GAME.EXE: MS-DOS executable, LE executable for MS-DOS, PMODE/W DOS extender

MS-DOS 실행파일 LE 포맷으로 PMODE/W extender가 사용된다. IDA에서 파일 포맷을 잘 읽어 들이기 때문에 큰 문제는 없지만, DOSBox 디버거에서 보호모드 실행 파일이 잘 분석되지 않고 있기 때문에 런타임 동적 분석을 생략하고 정적 분석만을 수행하였다.

IDA를 이용하면 메인코드가 위치하는 main 함수를 알아서 찾아주기 때문에 실제 프로그래밍된 코드 부분을 바로 분석할 수 있지만 게임의 코드 부분 앞에는 보호모드(DPMI)와 관련된 코드가 포함되어 있다.

cseg01:00014380                                   public start
cseg01:00014380                   start           proc near
cseg01:00014380 EB 76                             jmp     short loc_143F8
cseg01:00014380                   ; ---------------------------------------------------------------------------
cseg01:00014382 57 41 54 43 4F 4D+aWatcomCC32RunT db 'WATCOM C/C++32 Run-Time system. (c) Copyright by WATCOM Internati'
cseg01:00014382 20 43 2F 43 2B 2B+                db 'onal Corp. 1988-1994. All rights reserved.'
cseg01:000143ED 8D                                db  8Dh
cseg01:000143EE 40                                db  40h ; @
cseg01:000143EF 00                                db    0
cseg01:000143F0 03 00 01 00                       dd offset ___begtext
cseg01:000143F4 63 6F 6E 00       aCon            db 'con',0              ; DATA XREF: start+293↓o
cseg01:000143F8                   ; ---------------------------------------------------------------------------
cseg01:000143F8
cseg01:000143F8                   loc_143F8:                              ; CODE XREF: start↑j
cseg01:000143F8 FB                                sti
cseg01:000143F9 83 E4 FC                          and     esp, 0FFFFFFFCh
cseg01:000143FC 89 E3                             mov     ebx, esp
cseg01:000143FE 89 1D E0 15 05 00                 mov     dword_515E0, ebx
cseg01:00014404 89 1D CC 15 05 00                 mov     dword_515CC, ebx
cseg01:0001440A 66 B8 24 00                       mov     ax, 24h
cseg01:0001440E 66 A3 D8 15 05 00                 mov     word ptr dword_515D8, ax
cseg01:00014414 BB 52 41 48 50                    mov     ebx, 50484152h
cseg01:00014419 29 C0                             sub     eax, eax
cseg01:0001441B B4 30                             mov     ah, 30h
cseg01:0001441D CD 21                             int     21h             ; DOS - GET DOS VERSION
cseg01:0001441D                                                           ; Return: AL = major version number (00h for DOS 1.x)
...
cseg01:000144D1                   loc_144D1:                              ; CODE XREF: start+149↑j
cseg01:000144D1 66 B8 06 00                       mov     ax, 6
cseg01:000144D5 66 8C DB                          mov     bx, ds
cseg01:000144D8 CD 31                             int     31h             ; DPMI Services   ax=func xxxxh
cseg01:000144D8                                                           ; GET SEGMENT BASE ADDRESS
cseg01:000144D8                                                           ; BX = selector
cseg01:000144D8                                                           ; Return: CF set on error
cseg01:000144D8                                                           ; CF clear if successful, CX:DX = linear base address of segment
...
cseg01:00014603 E8 D8 9D 01 00                    call    __InitRtns
cseg01:00014608 29 ED                             sub     ebp, ebp
cseg01:0001460A E8 82 9D 01 00                    call    __CMain

cseg01:0002E391                   __CMain         proc near               ; CODE XREF: start+28A↑p
cseg01:0002E391 52                                push    edx
cseg01:0002E392 55                                push    ebp
cseg01:0002E393 89 E5                             mov     ebp, esp
cseg01:0002E395 8B 15 E4 15 05 00                 mov     edx, dword_515E4
cseg01:0002E39B 83 C2 03                          add     edx, 3
cseg01:0002E39E 80 E2 FC                          and     dl, 0FCh
cseg01:0002E3A1 E8 A5 63 01 00                    call    sub_4474B
cseg01:0002E3A6 39 C2                             cmp     edx, eax
cseg01:0002E3A8 73 08                             jnb     short loc_2E3B2
cseg01:0002E3AA 89 D0                             mov     eax, edx
cseg01:0002E3AC 29 C4                             sub     esp, eax
cseg01:0002E3AE 89 E0                             mov     eax, esp
cseg01:0002E3B0 EB 02                             jmp     short loc_2E3B4
cseg01:0002E3B2                   ; ---------------------------------------------------------------------------
cseg01:0002E3B2
cseg01:0002E3B2                   loc_2E3B2:                              ; CODE XREF: __CMain+17↑j
cseg01:0002E3B2 31 C0                             xor     eax, eax
cseg01:0002E3B4
cseg01:0002E3B4                   loc_2E3B4:                              ; CODE XREF: __CMain+1F↑j
cseg01:0002E3B4 8B 15 E4 15 05 00                 mov     edx, dword_515E4
cseg01:0002E3BA 01 D0                             add     eax, edx
cseg01:0002E3BC A3 E8 15 05 00                    mov     dword_515E8, eax
cseg01:0002E3C1 E8 8E 63 01 00                    call    sub_44754
cseg01:0002E3C6 8B 15 A0 BA 05 00                 mov     edx, dword_5BAA0
cseg01:0002E3CC A1 9C BA 05 00                    mov     eax, dword_5BA9C
cseg01:0002E3D1 E8 3A 1C FE FF                    call    main_
cseg01:0002E3D1                   __CMain         endp

하지만 게임의 로직과는 크게 상관이 없기 때문에 메인 함수 이전의 부분은 분석하지 않고 아래와 같이 코드 메인을 바로 살펴보도록 한다.

cseg01:00010010                   main_           proc near               ; CODE XREF: __CMain+40↓p
cseg01:00010010 68 10 00 00 00                    push    10h
cseg01:00010015 E8 1E 01 00 00                    call    __CHK
cseg01:0001001A 53                                push    ebx
cseg01:0001001B 56                                push    esi
cseg01:0001001C 57                                push    edi
cseg01:0001001D 31 D2                             xor     edx, edx
cseg01:0001001F 89 15 B0 4A 05 00                 mov     dword_54AB0, edx
cseg01:00010025 E8 56 0A 00 00                    call    sub_10A80
cseg01:0001002A 83 F8 01                          cmp     eax, 1
cseg01:0001002D 74 11                             jz      short loc_10040
cseg01:0001002F B8 04 00 05 00                    mov     eax, offset aPleaseDonTCopy ; "Please Don`t Copy!"
cseg01:00010034 E8 AF 0A 00 00                    call    puts_
cseg01:00010039 31 C0                             xor     eax, eax
cseg01:0001003B E8 D6 0A 00 00                    call    exit_
cseg01:00010040                   ; ---------------------------------------------------------------------------
cseg01:00010040
cseg01:00010040                   loc_10040:                              ; CODE XREF: main_+1D↑j
cseg01:00010040 E8 3B 01 00 00                    call    sub_10180
cseg01:00010045 E8 A6 01 00 00                    call    sub_101F0
cseg01:0001004A E8 6F 1B 00 00                    call    sub_11BBE
cseg01:0001004F 85 C0                             test    eax, eax
cseg01:00010051 75 07                             jnz     short loc_1005A
cseg01:00010053 31 D2                             xor     edx, edx
cseg01:00010055 E8 06 02 00 00                    call    sub_10260

“Plase Don’t Copy!” 메시지를 출력하고 종료하기 이전에 호출되는 함수는 sub_10A80이다. 해당 함수의 리턴 값이 1인 경우 0001002D 주소에서 점프(jz)하여 게임을 정상적으로 수행하고 이외의 값인 경우에는 메시지 출력 후 게임을 종료한다.

sub_10A80 함수는 아래와 같은 간단한 코드로 구성된다.

cseg01:00010A80                   sub_10A80       proc near               ; CODE XREF: main_+15↑p
cseg01:00010A80
cseg01:00010A80                   var_10          = dword ptr -10h
cseg01:00010A80                   anonymous_0     = dword ptr -8
cseg01:00010A80
cseg01:00010A80 68 14 00 00 00                    push    14h
cseg01:00010A85 E8 AE F6 FF FF                    call    __CHK
cseg01:00010A8A 53                                push    ebx
cseg01:00010A8B 51                                push    ecx
cseg01:00010A8C 52                                push    edx
cseg01:00010A8D 83 EC 04                          sub     esp, 4
cseg01:00010A90 B8 EC 00 05 00                    mov     eax, offset aHighscrExe ; "HIGHSCR.EXE"
cseg01:00010A95 E8 89 AD 01 00                    call    system_
cseg01:00010A9A 31 D2                             xor     edx, edx
cseg01:00010A9C B8 F8 00 05 00                    mov     eax, offset aDrugInd ; "DRUG.IND"
cseg01:00010AA1 E8 1E A0 01 00                    call    sub_2AAC4
cseg01:00010AA6 89 C1                             mov     ecx, eax
cseg01:00010AA8 85 C0                             test    eax, eax
cseg01:00010AAA 75 09                             jnz     short loc_10AB5
cseg01:00010AAC EB 33                             jmp     short loc_10AE1
cseg01:00010AAE                   ; ---------------------------------------------------------------------------
cseg01:00010AAE
cseg01:00010AAE                   loc_10AAE:                              ; CODE XREF: sub_10A80+57↓j
cseg01:00010AAE                                                           ; DATA XREF: cseg01:off_10A70↑o
cseg01:00010AAE B8 01 00 00 00                    mov     eax, 1          ; jumptable 00010AD7 case 1
cseg01:00010AB3 EB 2C                             jmp     short loc_10AE1
cseg01:00010AB5                   ; ---------------------------------------------------------------------------
cseg01:00010AB5
cseg01:00010AB5                   loc_10AB5:                              ; CODE XREF: sub_10A80+2A↑j
cseg01:00010AB5 BB 02 00 00 00                    mov     ebx, 2
cseg01:00010ABA 89 E2                             mov     edx, esp
cseg01:00010ABC E8 F3 A0 01 00                    call    sub_2ABB4
cseg01:00010AC1 89 C8                             mov     eax, ecx
cseg01:00010AC3 E8 C4 A0 01 00                    call    sub_2AB8C
cseg01:00010AC8 8B 04 24                          mov     eax, [esp+10h+var_10]
cseg01:00010ACB 05 02 00 00 00                    add     eax, 2          ; switch 4 cases
cseg01:00010AD0 66 3D 03 00                       cmp     ax, 3
cseg01:00010AD4 77 09                             ja      short loc_10ADF ; jumptable 00010AD7 default case
cseg01:00010AD6 98                                cwde
cseg01:00010AD7 2E FF 24 85 70 0A+                jmp     cs:off_10A70[eax*4] ; switch jump
cseg01:00010ADF                   ; ---------------------------------------------------------------------------
cseg01:00010ADF
cseg01:00010ADF                   loc_10ADF:                              ; CODE XREF: sub_10A80+54↑j
cseg01:00010ADF                                                           ; sub_10A80+57↑j
cseg01:00010ADF                                                           ; DATA XREF: ...
cseg01:00010ADF 31 C0                             xor     eax, eax        ; jumptable 00010AD7 default case
cseg01:00010AE1
cseg01:00010AE1                   loc_10AE1:                              ; CODE XREF: sub_10A80+2C↑j
cseg01:00010AE1                                                           ; sub_10A80+33↑j
cseg01:00010AE1 83 C4 04                          add     esp, 4
cseg01:00010AE4 5A                                pop     edx
cseg01:00010AE5 59                                pop     ecx
cseg01:00010AE6 5B                                pop     ebx
cseg01:00010AE7 C3                                retn
cseg01:00010AE7                   sub_10A80       endp

sub_10A80 함수에서 1을 리턴해야만 정상적으로 게임이 구동된다. 1이 리턴되는 로직은 아래와 같이 Graph 형태에서 쉽게 확인할 수 있다.

sub_10A80의 로직은 아래와 같다.

  1. HIGHSCR.EXE 를 실행한다
  2. DRUG.IND를 인자로 sub_2AAC4 함수 호출
    1. 이전 실행 로그 분석을 통해 DRUG.IND 파일 열람과 관련될 것으로 추정 가능
  3. sub_2AAC4 함수(open 함수)의 결과(eax)가 0이면
    1. sub_10A80 함수는 0을 반환하고 종료
  4. sub_2ABB4 (read 함수) 호출
  5. sub_2AB8C (close 함수) 호출
  6. 결과 값에 따라 분기
    1. loc_10AAE 분기인 경우 eax에 1을 저장 후 함수 종료 (게임 정상 수행)
    2. 이 외 분기인 경우 eax에 0을 저장 후 함수 종료 (메시지 출력 후 게임 종료)

sub_2AAC4 함수는 eax 레지스터를 사용하여 “DRUG.IND” 파일명의 주소를 인자로 전달 받는다. sub_2AAC4 함수의 주요 코드 부분은 아래와 같다.

cseg01:0002AAC4                   ; int __fastcall sub_2AAC4(char *a1, int a2, int a3, int a4)
cseg01:0002AAC4                   sub_2AAC4       proc near               ; CODE XREF: cseg01:000109FA↑p
cseg01:0002AAC4                                                           ; sub_10A80+21↑p ...
cseg01:0002AAC4 68 14 00 00 00                    push    14h
cseg01:0002AAC9 E8 6A 56 FE FF                    call    __CHK
cseg01:0002AACE 53                                push    ebx
cseg01:0002AACF 51                                push    ecx
cseg01:0002AAD0 89 C1                             mov     ecx, eax
cseg01:0002AAD2 B8 11 00 00 00                    mov     eax, 11h
cseg01:0002AAD7 E8 BF 3C 00 00                    call    _nmalloc_
cseg01:0002AADC 89 C3                             mov     ebx, eax
...
cseg01:0002AB3B                   loc_2AB3B:                              ; CODE XREF: sub_2AAC4+31↑j
cseg01:0002AB3B 68 02 02 00 00                    push    202h
cseg01:0002AB40 51                                push    ecx
cseg01:0002AB41 E8 64 83 01 00                    call    open_
cseg01:0002AB46 83 C4 08                          add     esp, 8
cseg01:0002AB49 83 F8 FF                          cmp     eax, 0FFFFFFFFh
cseg01:0002AB4C 75 1C                             jnz     short loc_2AB6A
...
cseg01:0002AB6A                   loc_2AB6A:                              ; CODE XREF: sub_2AAC4+66↑j
cseg01:0002AB6A                                                           ; sub_2AAC4+88↑j
cseg01:0002AB6A C6 03 01                          mov     byte ptr [ebx], 1
cseg01:0002AB6D 89 43 01                          mov     [ebx+1], eax
cseg01:0002AB70 E8 83 85 01 00                    call    filelength_
cseg01:0002AB75 89 43 05                          mov     [ebx+5], eax
cseg01:0002AB78 C7 43 09 00 00 00+                mov     dword ptr [ebx+9], 0
cseg01:0002AB7F C7 43 0D 00 00 00+                mov     dword ptr [ebx+0Dh], 0
cseg01:0002AB86 89 D8                             mov     eax, ebx
cseg01:0002AB88
cseg01:0002AB88                   loc_2AB88:                              ; CODE XREF: sub_2AAC4+1C↑j
cseg01:0002AB88                                                           ; sub_2AAC4+47↑j
cseg01:0002AB88 59                                pop     ecx
cseg01:0002AB89 5B                                pop     ebx
cseg01:0002AB8A C3                                retn
cseg01:0002AB8A                   sub_2AAC4       endp

sub_2AAC4 함수는

  1. nmalloc 를 호출하여 0x11 크기 만큼 메모리를 할당한 주소를 ebx 레지스터에 저장하고
  2. DRUG.IND 파일을 오픈하고
  3. filelength를 호출하여
  4. 할당된 메모리 주소가 저장된 ebx 레지스터에 결과 값을 저장한다.
    1. [ebx] = 1
    2. [ebx+1] = DRUG.IND file handle
    3. [ebx+5] = DRUG.IND file size
  5. 할당된 메모리 주소를 eax에 저장 후 반환한다.

sub_2AAC4 함수 내부에서 오류가 발생되면 0을 반환하며 성공시에는 할당한 메모리 주소를 반환한다.

sub_10A80의 00010AA8 85 C0 test eax, eax 구문에서 sub_2AAC4 함수의 반환 값을 검사하고 파일 오픈이 실패하지 않은 경우에는 cseg01:00010AAA 75 09 jnz short loc_10AB5 구문을 실행하여 loc_10AB5로 점프한다. 10AB5 주소의 코드를 다시 살펴보면 아래와 같다.

cseg01:00010AB5                   loc_10AB5:                              ; CODE XREF: sub_10A80+2A↑j
cseg01:00010AB5 BB 02 00 00 00                    mov     ebx, 2
cseg01:00010ABA 89 E2                             mov     edx, esp
cseg01:00010ABC E8 F3 A0 01 00                    call    sub_2ABB4

EOL_GAME.EXE 안에서 호출되는 함수들이 Calling Convention이 뒤죽 박죽이라 햇갈릴 수 있는데 Watcom C로 개발된 함수의 Calling Convetion은 EAX, EDX, EBX, ECX 레지스터 순서로 인자를 전달하고 있음을 찾을 수 있다.

X86 Calling Conventions Watcom register (Wikipedia)

Watcom does not support the __fastcall keyword except to alias it to null. The register calling convention may be selected by command line switch. (However, IDA uses __fastcall anyway for uniformity.)
Up to 4 registers are assigned to arguments in the order EAX, EDX, EBX, ECX. Arguments are assigned to registers from left to right. If any argument cannot be assigned to a register (say it is too large) it, and all subsequent arguments, are assigned to the stack. Arguments assigned to the stack are pushed from right to left. Names are mangled by adding a suffixed underscore.

Calling Convetion을 유의하여 레지스터 값을 잘 기억하고 있어야만 분석이 용이하다. sub_2ABB4 함수는 3개의 인자를 갖는다.

  1. eax = sub_2AAC4 함수에서 리턴된 파일 정보 관련 구조체 메모리 주소
  2. edx = esp (데이터 저장에 사용될 스택 주소)
  3. ebx = 2 (read 과정에서 사용될 데이터 크기 값)

sub_2ABB4 함수의 코드는 아래와 같다.

cseg01:0002ABB4                   sub_2ABB4       proc near               ; CODE XREF: sub_109E0+3C↑p
cseg01:0002ABB4                                                           ; sub_109E0+61↑p ...
cseg01:0002ABB4 68 08 00 00 00                    push    8
cseg01:0002ABB9 E8 7A 55 FE FF                    call    __CHK
cseg01:0002ABBE 51                                push    ecx
cseg01:0002ABBF 31 C9                             xor     ecx, ecx
cseg01:0002ABC1 8A 08                             mov     cl, [eax]
cseg01:0002ABC3 83 F9 02                          cmp     ecx, 2
cseg01:0002ABC6 74 1B                             jz      short loc_2ABE3
cseg01:0002ABC8 8B 40 01                          mov     eax, [eax+1]
cseg01:0002ABCB E8 6A 85 01 00                    call    read_
cseg01:0002ABD0 83 F8 FF                          cmp     eax, 0FFFFFFFFh
cseg01:0002ABD3 75 0E                             jnz     short loc_2ABE3
cseg01:0002ABD5 B8 03 00 00 00                    mov     eax, 3
cseg01:0002ABDA E8 D9 A9 FE FF                    call    sub_155B8
cseg01:0002ABDF 31 C0                             xor     eax, eax
cseg01:0002ABE1 59                                pop     ecx
cseg01:0002ABE2 C3                                retn
cseg01:0002ABE3                   ; ---------------------------------------------------------------------------
cseg01:0002ABE3
cseg01:0002ABE3                   loc_2ABE3:                              ; CODE XREF: sub_2ABB4+12↑j
cseg01:0002ABE3                                                           ; sub_2ABB4+1F↑j
cseg01:0002ABE3 B8 01 00 00 00                    mov     eax, 1
cseg01:0002ABE8 59                                pop     ecx
cseg01:0002ABE9 C3                                retn
cseg01:0002ABE9                   sub_2ABB4       endp

sub_2ABB4 함수는 파일에서 데이터를 읽는 read 함수를 호출하고 오류 발생시 sub_155B8 함수를 호출한 이후 0을 반환한다. 정상적으로 수행되는 경우에는 1을 반환한다. sub_2ABB4 함수 호출 과정에서 사용된 3개의 인자가 read 함수 호출에 사용된다. 이번 호출에는 DRUG.IND 파일에서 2바이트 만큼 읽은 후 스택 메모리(esp)에 저장한다.

cseg01:00010AC1 89 C8                             mov     eax, ecx
cseg01:00010AC3 E8 C4 A0 01 00                    call    sub_2AB8C
cseg01:00010AC8 8B 04 24                          mov     eax, [esp]
cseg01:00010ACB 05 02 00 00 00                    add     eax, 2          ; switch 4 cases
cseg01:00010AD0 66 3D 03 00                       cmp     ax, 3
cseg01:00010AD4 77 09                             ja      short loc_10ADF ; jumptable 00010AD7 default case
cseg01:00010AD6 98                                cwde
cseg01:00010AD7 2E FF 24 85 70 0A+                jmp     cs:off_10A70[eax*4] ; switch jump

sub_2ABB4 함수가 리턴된 이후 호출되는 sub_2AB8C 함수는 close를 호출하여 DRUG.IND 파일을 닫는다. 이후 읽어들인 데이터 값을 eax로 옮긴 후 비교 후 점프를 수행한다. 읽어들인 값이 1인 경우 (정상적으로 게임이 수행될 당시의 DRUG.IND의 데이터 값은 00 01이 사용됨)에는 eax 값이 3이되며 loc_10AAE로 점프된다. 이 외의 경우에는 loc_10ADF로 점프한다.

cseg01:00010A67 00 00 00 00 00 00+                align 10h
cseg01:00010A70 DF 0A 01 00 DF 0A+off_10A70       dd offset loc_10ADF     ; DATA XREF: sub_10A80+57↓r
cseg01:00010A70 01 00 DF 0A 01 00+                dd offset loc_10ADF     ; jump table for switch statement
cseg01:00010A70 AE 0A 01 00                       dd offset loc_10ADF
cseg01:00010A70                                   dd offset loc_10AAE

loc_10AAE로 점프하는 경우에는 아래와 같이 함수의 반환 값으로 사용될 eax 레지스터에 1을 저장하며 다른 경우에는 0이 반환된다. 여기에서 1을 반환해야만 main 함수에서 “Plase Don’t Copy!” 메시지를 출력하지 않고 게임을 진행할 수 있다.

HIGHSCR.EXE 코드 분석

앞에서 EOL_GAME.EXE를 분석하여 DRUG.IND의 파일 내용에 따라 게임 구동 여부가 결정되는 것을 확인할 수 있었다. 그런데 EOL_GAME.EXE를 분석하는 과정에서 DRUG.IND 파일에 내용을 쓰는 부분이 분석되지 않았다. DRUG.IND 파일의 내용은 어디에서 쓰여질까? EOL_GAME.EXE에서 DRUG.IND 파일을 읽기 전 HIGHSCR.EXE를 실행하는 루틴이 있음을 주목한다. 때문에 HIGHSCR.EXE 파일을 분석해본다.

HIGHSCR.EXE 는 MS DOS 실행 파일로 식별된다. IDA를 사용하여 파일을 읽어오면 16비트 코드로 작성되어 있음을 확인할 수 있다.

➜  eol file HIGHSCR.EXE
HIGHSCR.EXE: MS-DOS executable

HIGHSCR.EXE의 main 함수의 코드는 아래와 같다.

seg001:004C ; int __cdecl main(int argc, const char **argv, const char **envp)
seg001:004C _main           proc far                ; CODE XREF: start+14C↑P
seg001:004C
seg001:004C var_22          = byte ptr -22h
seg001:004C var_16          = byte ptr -16h
seg001:004C var_C           = word ptr -0Ch
seg001:004C buf             = word ptr -0Ah
seg001:004C var_8           = word ptr -8
seg001:004C var_6           = word ptr -6
seg001:004C var_4           = word ptr -4
seg001:004C var_2           = word ptr -2
seg001:004C argc            = word ptr  6
seg001:004C argv            = dword ptr  8
seg001:004C envp            = dword ptr  0Ch
seg001:004C
seg001:004C                 enter   22h, 0
seg001:0050                 push    si
seg001:0051                 push    di
seg001:0052                 cmp     word_11A02, sp
seg001:0056                 ja      short loc_115ED
seg001:0058                 call    F_OVERFLOW@
seg001:005D ; ---------------------------------------------------------------------------
seg001:005D
seg001:005D loc_115ED:                              ; CODE XREF: _main+A↑j
seg001:005D                 lea     ax, [bp+var_22]
seg001:0060                 push    ss
seg001:0061                 push    ax
seg001:0062                 push    ds
seg001:0063                 push    offset unk_117B4
seg001:0066                 mov     cx, 0Ch
seg001:0069                 call    SCOPY@
seg001:006E                 lea     ax, [bp+var_16]
seg001:0071                 push    ss
seg001:0072                 push    ax
seg001:0073                 push    ds
seg001:0074                 push    offset unk_117C0
seg001:0077                 mov     cx, 9
seg001:007A                 call    SCOPY@
seg001:007F                 push    8004h           ; access
seg001:0082                 push    ds
seg001:0083                 push    offset path     ; "DRUG.IND"
seg001:0086                 call    _open
seg001:008B                 add     sp, 6
seg001:008E                 mov     di, ax
seg001:0090                 mov     word_11A12, 0
seg001:0096                 mov     word_11A10, 0
seg001:009C                 mov     word_11A14, 1
seg001:00A2                 mov     word_11A18, ds
seg001:00A6                 mov     word_11A16, 2FAh
seg001:00AC                 call    _getdisk
seg001:00B1                 mov     [bp+var_C], ax
seg001:00B4                 push    ax
seg001:00B5                 push    cs
seg001:00B6                 call    near ptr sub_1159F
seg001:00B9                 pop     cx
seg001:00BA                 mov     [bp+var_2], ax
seg001:00BD                 cmp     [bp+var_2], 0
seg001:00C1                 jz      short loc_1165B
seg001:00C3                 mov     [bp+var_2], 0FFFFh
seg001:00C8                 jmp     _quit
seg001:00CB ; ---------------------------------------------------------------------------
seg001:00CB
seg001:00CB loc_1165B:                              ; CODE XREF: _main+75↑j
seg001:00CB                 xor     si, si
seg001:00CD                 jmp     short loc_11667
seg001:00CF ; ---------------------------------------------------------------------------
seg001:00CF
seg001:00CF loc_1165F:                              ; CODE XREF: _main+8E↓j
seg001:00CF                 mov     al, [si+325h]
seg001:00D3                 mov     [bp+si+var_22], al
seg001:00D6                 inc     si
seg001:00D7
seg001:00D7 loc_11667:                              ; CODE XREF: _main+81↑j
seg001:00D7                 cmp     si, 0Bh
seg001:00DA                 jl      short loc_1165F
seg001:00DC                 xor     si, si
seg001:00DE                 jmp     short loc_11678
seg001:00E0 ; ---------------------------------------------------------------------------
seg001:00E0
seg001:00E0 loc_11670:                              ; CODE XREF: _main+9F↓j
seg001:00E0                 mov     al, [si+330h]
seg001:00E4                 mov     [bp+si+var_16], al
seg001:00E7                 inc     si
seg001:00E8
seg001:00E8 loc_11678:                              ; CODE XREF: _main+92↑j
seg001:00E8                 cmp     si, 8
seg001:00EB                 jl      short loc_11670
seg001:00ED                 mov     ax, word_11A43
seg001:00F0                 cwd
seg001:00F1                 and     ax, 0FFFFh
seg001:00F4                 mov     [bp+var_4], ax
seg001:00F7                 mov     ax, word_11A41
seg001:00FA                 mov     [bp+var_6], ax
seg001:00FD                 push    8004h           ; access
seg001:0100                 push    ds
seg001:0101                 push    offset aEolCfg  ; "EOL.CFG"
seg001:0104                 call    _open
seg001:0109                 add     sp, 6
seg001:010C                 mov     si, ax
seg001:010E                 cmp     ax, 0FFFFh
seg001:0111                 jnz     short loc_116AA
seg001:0113                 mov     [bp+var_2], 0FFFEh
seg001:0118                 jmp     short _quit
seg001:011A ; ---------------------------------------------------------------------------
seg001:011A
seg001:011A loc_116AA:                              ; CODE XREF: _main+C5↑j
seg001:011A                 push    0               ; fromwhere
seg001:011C                 push    0
seg001:011E                 push    40h ; '@'       ; offset
seg001:0120                 push    si              ; handle
seg001:0121                 call    _lseek
seg001:0126                 add     sp, 8
seg001:0129                 push    2               ; len
seg001:012B                 push    ss
seg001:012C                 lea     ax, [bp+buf]
seg001:012F                 push    ax              ; buf
seg001:0130                 push    si              ; handle
seg001:0131                 call    j____read
seg001:0136                 add     sp, 8
seg001:0139                 push    2               ; len
seg001:013B                 push    ss
seg001:013C                 lea     ax, [bp+var_8]
seg001:013F                 push    ax              ; buf
seg001:0140                 push    si              ; handle
seg001:0141                 call    j____read
seg001:0146                 add     sp, 8
seg001:0149                 push    si              ; handle
seg001:014A                 call    _close
seg001:014F                 pop     cx
seg001:0150                 mov     ax, [bp+var_8]
seg001:0153                 not     ax
seg001:0155                 cmp     ax, [bp+var_4]
seg001:0158                 jnz     short loc_116F4
seg001:015A                 mov     ax, [bp+buf]
seg001:015D                 not     ax
seg001:015F                 cmp     ax, [bp+var_6]
seg001:0162                 jz      short loc_116FB
seg001:0164
seg001:0164 loc_116F4:                              ; CODE XREF: _main+10C↑j
seg001:0164                 mov     [bp+var_2], 1
seg001:0169                 jmp     short _quit
seg001:016B ; ---------------------------------------------------------------------------
seg001:016B
seg001:016B loc_116FB:                              ; CODE XREF: _main+116↑j
seg001:016B                 mov     [bp+var_2], 1
seg001:0170
seg001:0170 _quit:                                  ; CODE XREF: _main+7C↑j
seg001:0170                                         ; _main+CC↑j ...
seg001:0170                 push    2               ; len
seg001:0172                 push    ss
seg001:0173                 lea     ax, [bp+var_2]
seg001:0176                 push    ax              ; buf
seg001:0177                 push    di              ; handle
seg001:0178                 call    j____write
seg001:017D                 add     sp, 8
seg001:0180                 push    di              ; handle
seg001:0181                 call    _close
seg001:0186                 pop     cx
seg001:0187                 pop     di
seg001:0188                 pop     si
seg001:0189                 leave
seg001:018A                 retf
seg001:018A _main           endp

HIGHSCR.EXE의 주 기능은 main 함수에 다 구현이 되어있다. 이 중 중요한 로직은 아래와 같다.

  1. DRUG.IND 파일 오픈
  2. _getdisk를 호출하여 현재 드라이브 정보 획득 (‘C’ : 2)
  3. sub_1159F 함수 호출 후 결과 값(ax)를 [bp+var_2]에 저장
    1. 결과 값이 0이 아닌 경우 _quit (0170)으로 점프 (_quit는 분석 편의를 위하여 사용)
  4. si 레지스터를 초기화하고 [si+325h]를 [bp+si+var_22]에 si가 Bh가 될 때까지 반복하여 저장
  5. si 레지스터를 초기화하고 [si+330h]를 [bp+si+var_16]에 si가 8h가 될 때까지 반복하여 저장
  6. EOL.CFG 파일 오픈
    1. 40h로 파일 포인터 위치를 이동
    2. 2바이트를 읽어 [bp+buf]에 저장
    3. 다음 2바이트를 읽어 [bp+var_8]에 저장 (이름이 혼동 되지만 Stack 메모리 상에서 [bp+buf] 다음에 위치한다)
    4. EOL.CFG 파일을 닫는다.
  7. 위에서 읽어 들인 [bp+var_8]의 값을 NOT 연산한 후 [bp_var_4]와 비교 후 다른 경우 loc_116F4로 점프
    1. 점프 시 [bp+var_2]에 1을 저장 후 _quit 구문 실행
  8. [bp+buf]의 값을 NOT 연산한 후 [bp_var_6]과 비교 후 같은 경우 loc_116FB로 점프
    1. 점프 시 [bp+var_2]에 1을 저장 후 _quit 구문 실행
    2. 점프하지 않는 경우 loc_116F4 구문을 실행하며 [bp+var_2]에 1을 저장 후 _quit 구문 실행
  9. [bp+var_2]의 값을 DRUG.IND에 저장 후 파일을 닫고 종료

중간 과정에서 오류가 발생시 [bp+var_2]에 1을 저장하지 않고 편의상 이름 붙인 _quit 구문으로 바로 점프하며 DRUG.IND 파일에 1 이외의 값이 저장되어 오류 메시지 출력 후 게임이 종료되게 된다.

7번과 8번에서 흥미로운 점이 있다. 8번에서는 비교 결과와 상관 없이 무조건 [bp+var_2]에는 1을 저장한다. 결과적으로 DRUG.IND에는 무조건 1의 값이 저장된다. 7번의 경우에는 비교 결과가 다를 경우에 loc_116F4로 점프하게 되더라도 [bp+var_2]에 1이 저장되고 있다. 아마도 원본 게임의 경우에는 비교 결과가 다른 경우 loc_116F4에서는 [bp+var_2]에 0을 저장하도록 되어 있을걸로 추정된다. 인터넷 상에 배포중인 게임은 해당 부분을 패치하여 EOL.CFG에 기록된 값과 디스크에서 읽어온 값이 다르더라도 DRUG.IND에 1을 저장하여 게임이 구동되도록 패치한 것으로 추정된다.

EOL.CFG의 값과 비교되는 [bp+var_4], [bp+var_6]은 어디에서 저장되는 것일까? 아직 분석을 수행하지 않은 sub_1159F 함수에 답이 있다.

sub_1159F 함수의 코드는 아래와 같다.

seg001:000F sub_1159F       proc far                ; CODE XREF: _main+6A↓p
seg001:000F
seg001:000F inregs          = REGS ptr -18h
seg001:000F segregs         = SREGS ptr -8
seg001:000F arg_0           = byte ptr  6
seg001:000F
seg001:000F                 enter   18h, 0
seg001:0013                 cmp     word_11A02, sp
seg001:0017                 ja      short loc_115AE
seg001:0019                 call    F_OVERFLOW@
seg001:001E ; ---------------------------------------------------------------------------
seg001:001E
seg001:001E loc_115AE:                              ; CODE XREF: sub_1159F+8↑j
seg001:001E                 mov     al, [bp+arg_0]
seg001:0021                 mov     byte ptr [bp+inregs], al
seg001:0024                 mov     word ptr [bp+inregs+4], 0FFFFh
seg001:0029                 mov     word ptr [bp+inregs+2], 2F0h
seg001:002E                 mov     [bp+segregs._ds], ds
seg001:0031                 push    ss
seg001:0032                 lea     ax, [bp+segregs]
seg001:0035                 push    ax              ; segregs
seg001:0036                 push    ss
seg001:0037                 lea     ax, [bp+inregs]
seg001:003A                 push    ax              ; outregs
seg001:003B                 push    ss
seg001:003C                 push    ax              ; inregs
seg001:003D                 push    25h ; '%'       ; intno
seg001:003F                 call    _int86x
seg001:0044                 add     sp, 0Eh
seg001:0047                 mov     ax, word ptr [bp+inregs+0Ch]
seg001:004A                 leave
seg001:004B                 retf
seg001:004B sub_1159F       endp

sub_1159F 함수의 주요 기능은 _int86x 함수를 호출하여 int 25h를 실행하는 것이다. 중간 과정을 생략하고 DOSBox 디버거를 사용하여 int 25h가 호출되는 장면을 확인한 부분은 아래와 같다.

아래의 자료를 참고하여 INT 25h와 관련된 내용을 알 수 있다.

디버거 상에서 확인한 AL 레지스터는 02h로 C 드라이브를 가리키고 있다. CX 레지스터는 FFFFh이며 DS:BX는 0995:02F0이다. 모두 sub_1159F 함수에서 셋팅되고 있다.

DS:BX에 저장된 값은 위의 그림에서 설명되는 AbsDiskIORec 구조체로 0번 섹터(부트 섹터) 데이터를 2FAh 번지에 저장하는 것을 의미한다. 검증에 사용되는 데이터는 아래의 주소에 위치한다. 해당 주소가 어떻게 사용되는지는 코드를 살펴보면 알 수 있다.

dseg:0321 word_11A41      dw ?                    ; DATA XREF: _main+AB↑r
dseg:0323 word_11A43      dw ?                    ; DATA XREF: _main+A1↑r

0321h를 부트섹터가 저장되기 시작하는 2FAh를 빼면 Offset 27h임을 알 수 있다. 27h에는 4바이트의 Volume Serial Number가 위치한다. 검증과정에서 게임이 실행되는 디스크 부트섹터의 시리얼 넘버가 사용된 것을 알 수 있다.

DOSBox-X에서의 크랙 미적용 원인 분석

위의 분석 과정에서 디스크의 시리얼 넘버를 이용하여 복제 여부를 파악하고 있음을 확인하였다. 원본 파일이 없어 정확한 내용을 확인할 수는 없으나 인스톨 과정에서 디스크의 시리얼 넘버를 EOL.CFG에 저장 후 게임을 구동할 때마다 비교하는 방식이 사용된 것으로 판단된다.

HIGHSCR.EXE 분석 과정에서 디스크의 시리얼 넘버를 비교하는 단계 (7번)에서 시리얼이 일치되지 않을때도 정상 값을 리턴하도록 패치된 것을 확인할 수 있었다. 해당 패치는 DOSBox에서 잘 적용되었으나 DOSBox-X에서는 동작되지 않았다. DOSBox-X는 INT 25h가 사용되는 sub_1159F 함수에서 오류 1을 리턴하여 시리얼 넘버 비교 과정 없이 DRUG.IND에 0xFFFFFFFF 값을 저장하여 프로텍트 검증이 실패된다. DOSBox-X에서 사용되는 INT 25h의 구현은 아래와 같다.

static Bitu DOS_25Handler_Actual(bool fat32) {
	if (reg_al >= DOS_DRIVES || !Drives[reg_al] || Drives[reg_al]->isRemovable()) {
		reg_ax = 0x8002;
		SETFLAGBIT(CF,true);
	} else {
		DOS_Drive *drv = Drives[reg_al];
		/* assume drv != NULL */
		uint32_t sector_size = drv->GetSectorSize();
		uint32_t sector_count = drv->GetSectorCount();
		PhysPt ptr = PhysMake(SegValue(ds),reg_bx);
		uint32_t req_count = reg_cx;
		uint32_t sector_num = reg_dx;

DOSBox-X는 DOSBox에 비하여 INT 25h의 기능이 보다 더 충실하게 구현한 것처럼 보이지만 시험한 버전에서는 오류 값이 리턴된 것으로 보여진다. (INT 25h는 오류 발생시에 Carry Flag를 Set하고 ax에 오류 코드를 반환한다) 때문에 INT 25h 호출에서 오류가 발생되어 시리얼 검증을 수행하지 못하는 것으로 판단되어 게임이 구동되지 않는다.

DOSBox의 경우 아래와 같이 INT 25h가 제대로 구현되어 있지 않았다. 하지만 HIGHSCR.EXE에 이미 크랙이 적용 되어있어 시리얼 비교 과정이 우회되어 게임이 정상 실행되고 있다.

static Bitu DOS_25Handler(void) {
    if(Drives[reg_al]==0){
        reg_ax=0x8002;
        SETFLAGBIT(CF,true);
    }else{
        SETFLAGBIT(CF,false);
        if((reg_cx != 1) ||(reg_dx != 1))
            LOG(LOG_DOSMISC,LOG_NORMAL)("int 25 called but not as diskdetection drive %X",reg_al);

       reg_ax=0;
    }
    return CBRET_NONE;
}

결론

고전게임인 에올의 모험이 DOSBox-X에서만 정상 실행되지 않는 것을 보고 관심을 갖고 분석을 해보았다. DOSBox에 해당하는 유니크한 값이 사용된것이 원인으로 예상하였는데 비슷하면서도 조금은 다른 결과를 얻게된듯 하다. 항상 그렇듯이 작업을 끝내고 나면 별거 아니지만 정리하는데는 많은 시간과 노력이 드는듯 하다. 고전 작업에 대하여 찾는 사람이 얼마나 될지 알수 없지만 개인적인 작업에 대한 정리가 그래도 누군가에게는 도움이 되길 바라는 바이다.

크랙의 방법은 한 가지가 아니다. 다소 억지스럽더라도 본인의 방법을 찾아서 해보는 것이 더 의미 있을것이다.

댓글 달기

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