개요
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
- EOL_GAME.EXE
목표 확인
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을 실행하면
- HIGHSCR.EXE를 실행하고 (int 21h, ah=4bh)
- 디스크 읽기 작업과 관련된 int 25h를 호출하고 (drive 2: ‘C’)
- DRUG.IND 파일을 오픈(int 21h, ah=3dh)하고 무언가 쓰기 작업(int 21h, ah=40h)을 수행하고 파일 핸들을 닫는다.(int 21h, ah=3eh)
- HIGHSCR.EXE가 종료된 후 EOL_GAME.EXE는 DRUG.IND를 읽고 (int 21h, ah=3fh)
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의 로직은 아래와 같다.
- HIGHSCR.EXE 를 실행한다
- DRUG.IND를 인자로 sub_2AAC4 함수 호출
- 이전 실행 로그 분석을 통해 DRUG.IND 파일 열람과 관련될 것으로 추정 가능
- sub_2AAC4 함수(open 함수)의 결과(eax)가 0이면
- sub_10A80 함수는 0을 반환하고 종료
- sub_2ABB4 (read 함수) 호출
- sub_2AB8C (close 함수) 호출
- 결과 값에 따라 분기
- loc_10AAE 분기인 경우 eax에 1을 저장 후 함수 종료 (게임 정상 수행)
- 이 외 분기인 경우 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 함수는
- nmalloc 를 호출하여 0x11 크기 만큼 메모리를 할당한 주소를 ebx 레지스터에 저장하고
- DRUG.IND 파일을 오픈하고
- filelength를 호출하여
- 할당된 메모리 주소가 저장된 ebx 레지스터에 결과 값을 저장한다.
- [ebx] = 1
- [ebx+1] = DRUG.IND file handle
- [ebx+5] = DRUG.IND file size
- 할당된 메모리 주소를 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개의 인자를 갖는다.
- eax = sub_2AAC4 함수에서 리턴된 파일 정보 관련 구조체 메모리 주소
- edx = esp (데이터 저장에 사용될 스택 주소)
- 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 함수에 다 구현이 되어있다. 이 중 중요한 로직은 아래와 같다.
- DRUG.IND 파일 오픈
- _getdisk를 호출하여 현재 드라이브 정보 획득 (‘C’ : 2)
- sub_1159F 함수 호출 후 결과 값(ax)를 [bp+var_2]에 저장
- 결과 값이 0이 아닌 경우 _quit (0170)으로 점프 (_quit는 분석 편의를 위하여 사용)
- si 레지스터를 초기화하고 [si+325h]를 [bp+si+var_22]에 si가 Bh가 될 때까지 반복하여 저장
- si 레지스터를 초기화하고 [si+330h]를 [bp+si+var_16]에 si가 8h가 될 때까지 반복하여 저장
- EOL.CFG 파일 오픈
- 40h로 파일 포인터 위치를 이동
- 2바이트를 읽어 [bp+buf]에 저장
- 다음 2바이트를 읽어 [bp+var_8]에 저장 (이름이 혼동 되지만 Stack 메모리 상에서 [bp+buf] 다음에 위치한다)
- EOL.CFG 파일을 닫는다.
- 위에서 읽어 들인 [bp+var_8]의 값을 NOT 연산한 후 [bp_var_4]와 비교 후 다른 경우 loc_116F4로 점프
- 점프 시 [bp+var_2]에 1을 저장 후 _quit 구문 실행
- [bp+buf]의 값을 NOT 연산한 후 [bp_var_6]과 비교 후 같은 경우 loc_116FB로 점프
- 점프 시 [bp+var_2]에 1을 저장 후 _quit 구문 실행
- 점프하지 않는 경우 loc_116F4 구문을 실행하며 [bp+var_2]에 1을 저장 후 _quit 구문 실행
- [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에 해당하는 유니크한 값이 사용된것이 원인으로 예상하였는데 비슷하면서도 조금은 다른 결과를 얻게된듯 하다. 항상 그렇듯이 작업을 끝내고 나면 별거 아니지만 정리하는데는 많은 시간과 노력이 드는듯 하다. 고전 작업에 대하여 찾는 사람이 얼마나 될지 알수 없지만 개인적인 작업에 대한 정리가 그래도 누군가에게는 도움이 되길 바라는 바이다.
크랙의 방법은 한 가지가 아니다. 다소 억지스럽더라도 본인의 방법을 찾아서 해보는 것이 더 의미 있을것이다.