개요
최근에 남는 라즈베리 파이를 가지고 에뮬 게임기를 만들어보니 과거 도스 (DOS) 게임들이 생각나서 찾아보게 되었다.
복잡하게 환경 구성할 필요 없이 DOSBox를 사용하면 예전의 고전 도스게임을 쉽게 할 수 있으며 고전게임을 인터넷에서 검색하니 이미 DOSBox를 포함하여 배포하고 있다. 나는 맥OS에서 DOSBox를 사용해보았지만 현재까지는 큰 문제 없이 잘 동작하고 있다.
과거의 도스 유물들을 찾아보다가 문득 고전 프로그램을 분석해볼까 생각이 들었고 재미를 위하여 게임을 선정해보았다.
선정을 위한 기준으로는
- 대중적으로 유명하여 현재도 인터넷 상에서 쉽게 파일을 찾을 수 있어야 하며
- 크랙이 적용되지 않은 원본 프로그램이 존재하고
- 옵션으로 크랙이 함께 배포되고 있다면 크랙 파일을 분석해 볼 수도 있겠다.
고전 프로그램이지만 저작권은 언제나 민감한 이슈 사항이기 때문에 관련된 파일은 따로 공개하지 않고, 여기에는 방법론만 작성하도록 하겠다.
준비
- DOSBox : 도스 프로그램 구동을 위해 사용
- 디버거 기능이 포함되지 않은 일반 버전을 사용하였다.
- IDA Pro : 디스어셈블러, 도스 프로그램의 역공학 분석을 위한 도구
- 상용 버전을 사용하였는데, 없는 경우 유사 기능의 다른 프로그램을 이용해도 무방할 듯 하다
- 010 Editor : 파일 에디터
- 손에 익은 프로그램을 사용한 것으로, 이 역시 비슷한 기능의 프로그램을 이용하면 된다.
- 코에이 대항해시대2 (한글판) 게임 (KOEI Uncharted Waters 2)
정적 분석만을 하였기 때문에 디버깅 프로그램은 따로 사용하지 않았다.
목표 확인
게임을 구동하면 암호 입력 화면이 표시된다. 암호를 세 번 틀리게 되면 프로그램이 종료되어 게임을 진행할 수 없게된다.
목표 : 패스워드 (CODE) 검사를 무력화 하여 게임을 구동한다.
분석 대상
코에이 대항해시대2 게임은 KOEI.COM 실행 파일로 시작된다. 확장자가 COM인 만큼 아주 작은 프로그램으로 초기화 작업 후 별도의 실행 파일을 구동하는 역할을 수행한다. KOEI.COM은 FMDRV.COM, OPEN.EXE, MAIN.EXE, END.EXE 순서로 프로그램을 수행한다.
- FMDRV.COM : 포함된 문자열을 보면 사운드와 관련된 드라이버로 추정된다.
!SOUND-DRIVER-AD-LIBOPLDRV
OPL-II Sound Driver Version 3.12
Copyright (C) 1991-93 KOEI CO.,LTD. JAPAN
- OPEN.EXE : 게임 오프닝, 무조건 실행됨
- MAIN.EXE : 일반적인 경우라면 항상 실행됨 (오프닝 과정에 오류가 발생되지 않는다면)
- END.EXE : MAIN.EXE의 결과 값에 따라 실행됨 (엔딩 크레딧을 보여주기 위하여)
게임 구동 후 암호 코드 부분을 확인하기 위해서 MAIN.EXE를 분석 대상으로 선정한다.
코드 분석
검증 로직 분석
IDA로 MAIN.EXE를 열면 자동으로 _main (main) 함수를 분석 후 가리킨다.
(사실 main 함수는 엔트리포인트 주소인 0x32F4의 start 함수에서 호출되지만 보통 프로그램의 코드는 main에서 부터 시작한다고 보기 때문에 여기에서는 start 함수는 신경 쓰지 않는다)
seg001:5F77 ; int __cdecl main(int argc, const char **argv, const char **envp)
seg001:5F77 _main proc near ; CODE XREF: start+B8↑P
seg001:5F77
seg001:5F77 var_1 = byte ptr -1
seg001:5F77 argc = word ptr 4
seg001:5F77 argv = dword ptr 6
seg001:5F77 envp = dword ptr 0Ah
seg001:5F77
seg001:5F77 push bp
seg001:5F78 mov bp, sp
seg001:5F7A sub sp, 2
seg001:5F7D call sub_454EE
seg001:5F82 or ax, ax
seg001:5F84 jz short loc_25B24
seg001:5F86 call sub_107F7
seg001:5F8B mov ax, 3
seg001:5F8E call sub_1949C
seg001:5F93 mov ax, 32h ; '2'
seg001:5F96 call sub_15958
seg001:5F9B sub ax, ax
seg001:5F9D push ax ; int
seg001:5F9E call _exit
seg001:5FA3 ; ------------------------------------------------------
seg001:5FA3 pop bx
seg001:5FA4
seg001:5FA4 loc_25B24: ; CODE XREF: _main+D↑j
seg001:5FA4 push cs
seg001:5FA5 call near ptr sub_25A44
seg001:5FA8 nop
seg001:5FA9 push cs
seg001:5FAA call near ptr sub_26F20
seg001:5FAD jmp loc_25BB4
처음 호출되는 sub_454EE가 패스워드를 검증하는 함수이다. 함수 안을 들어가 분석을 수행한다.
seg003:7ADE sub_454EE proc far ; CODE XREF: _main+6↑P
seg003:7ADE
seg003:7ADE var_4 = word ptr -4
seg003:7ADE var_2 = word ptr -2
seg003:7ADE
seg003:7ADE push bp
seg003:7ADF mov bp, sp
seg003:7AE1 sub sp, 4
seg003:7AE4 push di
seg003:7AE5 push si
seg003:7AE6 sub di, di
seg003:7AE8 mov [bp+var_2], 1
seg003:7AED jmp short loc_45540
seg003:7AEF ; ------------------------------------------------------
seg003:7AEF
seg003:7AEF loc_454FF: ; CODE XREF: sub_454EE+55↓j
seg003:7AEF nop
seg003:7AF0 push cs
seg003:7AF1 call near ptr sub_4778C
seg003:7AF4 mov cx, 0Ah
seg003:7AF7 mov bx, ax
seg003:7AF9 cwd
seg003:7AFA idiv cx
seg003:7AFC push dx
seg003:7AFD mov si, 0FF9Ch
seg003:7B00 mov ax, bx
seg003:7B02 mov bx, 64h ; 'd'
seg003:7B05 mov [bp+var_4], ax
seg003:7B08 cwd
seg003:7B09 idiv bx
seg003:7B0B mov bx, ax
seg003:7B0D imul si
seg003:7B0F add ax, [bp+var_4]
seg003:7B12 cwd
seg003:7B13 idiv cx
seg003:7B15 push ax
seg003:7B16 push bx
seg003:7B17 nop
seg003:7B18 push cs
seg003:7B19 call near ptr sub_45550
seg003:7B1C add sp, 6
seg003:7B1F push [bp+var_4]
seg003:7B22 nop
seg003:7B23 push cs
seg003:7B24 call near ptr sub_45D5D
seg003:7B27 pop bx
seg003:7B28 mov [bp+var_2], ax
seg003:7B2B or ax, ax
seg003:7B2D jz short loc_45545
seg003:7B2F inc di
seg003:7B30
seg003:7B30 loc_45540: ; CODE XREF: sub_454EE+F↑j
seg003:7B30 cmp di, 3
seg003:7B33 jl short loc_454FF
seg003:7B35
seg003:7B35 loc_45545: ; CODE XREF: sub_454EE+4F↑j
seg003:7B35 mov ax, [bp+var_2]
seg003:7B38 pop si
seg003:7B39 pop di
seg003:7B3A mov sp, bp
seg003:7B3C pop bp
seg003:7B3D retf
seg003:7B3D sub_454EE endp
코드를 펼쳐 놓으면 길게 느껴지지만 중요한 부분은 몇 줄 되지 않는다. sub_454EE 함수는 시작 후 바로 loc_45540 으로 점프를 수행한다. 그리고 점프된 곳에서는 di 레지스터를 숫자 3과 비교후 작은 경우 loc_454FF 원래 점프 이후의 코드로 되돌아온다. di 값은 seg003:7A36의 sub di, di 명령으로 0으로 초기화 되었기 때문에 조건에 의하여 탈출되지 않는다면 총 세 번 반복 수행된다. 3이라는 값은 이전에 시험한 패스워드 입력이 가능한 횟수와 정확하게 일치한다.
세 번 반복하는 구문은 다른 기능에서 사용될 수도 있기 때문에 가정이 맞는지 확인해보기 위해 실제 프로그램의 파일을 고쳐본다. 파일의 수정을 앞서 원본 파일은 꼭 백업하고 작업한다. 나는 오프닝을 보지 않기 위하여 사본 파일(MAIN-C.EXE)를 생성 후 편집하고 직접 실행하였는데 실패 후에 KOEI.COM 의 그래픽 화면 복원 절차가 빠지기 때문인지 시스템이 먹통이 되어 시스템 리셋을 수행하였다. 때문에 가급적이면 MAIN.EXE를 수정하고 KOEI.COM으로 정상 실행하고 확인하는 편이 나아보인다. 해보진 않았지만 오프닝을 건너띄기 원한다면 KOEI.COM에서 수정하면 된다.
세 번 비교를 수행하는 seg003:7B30의 파일 주소(File Offset)는 0x45540이다. 파일의 주소는 분석하는 환경에 따라 HEX 값을 검색하거나(추천) 계산을 통해(비추천) 구할 수 있다. 각자의 환경에 맞추어 진행한다. IDA 디스어셈블러를 이용한 경우 별도 계산의 필요 없이 하단 상태창에 표기된 주소를 참고한다.
세 번 비교를 위한 0x03 값을 넉넉하게 0x0A (10)으로 수정하고 프로그램을 실행해보면 암호 입력 횟수가 10번으로 늘어나는 것을 확인할 수 있다.
이를 통해 우리는
- 해당 부분이 암호 입력과 관련된 로직 부근이 맞다고 판단할 수 있고
- 프로그램의 코드를 임의 수정해도 정상 동작하는 것으로 보아 무결성 검증이 따로 없음을 알 수 있다.
다시 원래의 암호 입력과 관련된 함수 sub_454EE를 살펴보면
- 0x7B24에서 함수를 호출하고
- 그 결과 값을 저장(mov [bp+var2], ax) 후
- 검사 (or ax, ax)하여 오류가 없는 경우 (결과 값이 0인 경우) 패스워드의 입력을 중단하고 (jz) 함수를 종료한다.
- 3번의 검사 과정에서 오류가 발생된 경우에는 패스워드 입력 횟수를 증가시키고 (inc di) 입력 과정을 반복 수행한다.
크랙 방법
3번의 과정에서 검사 결과와 상관 없이 무조건적으로 점프(jmp, 0xEB)를 수행하는 경우 패스워드의 일치 여부와 관계없이 입력 과정을 종료하게 된다.
sub_454EE 패스워드 검증 함수 종료 후의 main 함수를 다시 살펴보면 결과 값을 검증하고 0인 경우 점프(jz)하고 그렇지 않은 경우에는 _exit 함수를 실행하여 프로그램을 종료시킨다.
우리는 프로그램의 종료를 원하는 것이 아니기 때문에 해당 부분에서 무조건적인 점프가 수행되어야만 한다. 그러나 이전 패스워드 검증 함수에서 강제로 점프 시킨 경우에는 ax 값이 0이 아니기 때문에 프로그램이 종료된다. 0x5F84에서도 무조건적인 점프가 이루어지도록 패치하면 패스워드의 일치 여부와 관계 없이 게임이 실행된다.
개선된(?) 크랙 방법
위의 방법은 결과적으로 게임은 잘 실행되지만 그리 깔끔한 방법은 아니다. 일단 패스워드의 입력 창이 화면에 표시되고 적어도 한 번은 코드를 입력해야만 진행이 된다. main 함수에서 패스워드 입력 함수의 호출 자체를 실행하지 않도록 패치하는 경우 패스워드 입력 창 자체가 표시되지 않는다. 코드 패치를 위해서 해당 명령 구문을 아무런 기능 수행이 없는 NOP (0x90) 으로 수정하고 조건과 상관 없이 점프하도록 JMP (0xEB)를 사용하였다.
NOP의 마지막 바이트가 0x80인 이유는 실행파일의 재배치 과정에서 변경되는 값을 보정한 것이다. 그다지 깔끔한 방법은 아니지만 위의 수정 내용은 런타임 상에서 아래처럼 반영된다.
seg001:5F7D nop
seg001:5F7E nop
seg001:5F7F nop
seg001:5F80 nop
seg001:5F81 nop
seg001:5F82 or ax, ax
seg001:5F84 jmp short loc_25B24
수정 내용이 적용된 파일을 실행하면 패스워드 부분이 나타나지 않고 게임이 실행된다. 이번의 경우에는 패스워드 로직 안에 게임의 동작과 관련된 특별한 코드가 사용되지 않았기 때문에 함수 자체를 통으로 건너띄어도 이상이 없지만 경우에 따라서는 이와 같이 간단하게 해결되지 않을 수도 있기 때문에 주의 해야한다.
공개된 크랙 방식
인터넷 상에서 게임과 함께 포함된 크랙 프로그램은 런타임에 seg003:8394 의 코드를 패치한다. 패치될 코드가 위치하는 sub_45D50 함수는 패스워드를 검증하는 함수 sub_454EE (seg003:7ADE)의 마지막 부분 seg003:7B24에서 호출된다.
seg003:8391 2A 45 04 sub al, [di+4]
seg003:8394 02 D0 add dl, al
seg003:8396 92 xchg ax, dx
seg003:8397 5F pop di
seg003:8398 5E pop si
seg003:8399 1F pop ds
seg003:839A assume ds:dseg
seg003:839A 5D pop bp
seg003:839B CB retf
seg003:839B sub_45D5D endp
이전의 분석 과정에서 패스워드 검증 함수 sub_45D50은 ax 레지스터의 값을 검사하여 0 이외의 값인 경우 3회에 한하여 패스워드 검증을 재수행하고 0인 경우 검증을 중단하는 것을 확인하였다. 검증 과정에서 사용된 ax 레지스터는 sub_45D5D의 seg003:8396에서 dx 값으로 셋팅된다. 연산 결과와 상관 없이 ax 값이 항상 0이 되도록 seg003:8394에서 dx 값이 되도록 코드를 수정한다.
공개된 크랙 도구는 해당 주소의 코드를 xor dx, dx 명령이 수행 되도록 0x33D2의 값으로 수정하고 있다.
seg003:8391 2A 45 04 sub al, [di+4]
seg003:8394 33 D2 xor dx, dx
seg003:8396 92 xchg ax, dx
seg003:8397 5F pop di
seg003:8398 5E pop si
seg003:8399 1F pop ds
seg003:839A assume ds:dseg
seg003:839A 5D pop bp
seg003:839B CB retf
seg003:839B sub_45D5D endp
패치 코드가 적용된 파일을 실행하면 암호 입력 값과 상관 없이 게임이 정상적으로 구동된다. 2바이트 코드 수정으로 결과 값을 변조하여 우회하는 깔끔한 방식이 사용되었다.
길게 설명을 하였지만 프로그램에 보호기능이 없기 때문에 많은 시간 소모 없이 손쉽게 크랙이 가능한 대상이다.
결론
과거에는 좁은 모니터 화면으로 코드를 보며 힘들게 분석을 해왔던 기억이 있는데 최근에는 환경이 좋아져서인지 아주 쉽게 크랙이 가능하였다. 물론 타겟이 아주 단순하였기 때문에 가능한 일이기도 하다. 오히려 정리의 시간이 오래걸리다 보니 다음번 글 작성이 가능할지 모르겠다.
더 나은 환경을 구축해서 다양한 시도를 할 수 있는 여유가 생기기를 바란다.
첨언
누군가에게 설명하기 위함이 아닌 순전히 재미로 한 작업이다. IDA로 코드를 확인하고 패치하는 시간은 10분이 채 걸리지 않을 작업이지만 이를 하나씩 설명하는데에는 10배의 시간이 소요된다. 때문에 16비트 도스 환경의 인터럽트나 리얼모드, 보호모드와 같은 개념은 굳이 따로 언급하지 않았다. 다른곳에서도 설명을 확인할 수 있기도 하고 나중에 기회가 된다면 다뤄보게 될지도 모르겠다.
안녕하세요. 흥미로운 내용이라 댓글을 답니다. 말씀대로 fmdrv.com은 사운드 드라이버입니다. 실행을 안 시키면 애드립 카드에서 소리가 안 납니다. 애드립은 원래 직접 포트 제어를 해도 되는데, sound.com이란 드라이버를 쓰면 좀 편하게 개발이 가능했던 모양입니다. 그런데 코에이는 왠지 자사 게임들에 fmdrv.com 이란 자체 개발 드라이버를 썼습니다. 돈 문제였는지 어쨌는지는 저도 궁금하네요. ㅎㅎ 쓰신 글 처럼 저도 fmdrv.com을 크랙해서 애드립의 포트인 388h로 가는 내용을 프린터 포트인 378h로 보내고 싶은데, 조언 좀 부탁드립니다. 좋은 하루 되세요^^
안녕하세요. 댓글을 이제서야 확인했네요.
작성된 글보다도 오히려 더 흥미로운 주제의 댓글인것 같습니다.
저도 관련된 내용을 잘 알지 못해 조심스럽지만 포트 제어와 관련된 부분이니 어셈블리의 in, out 명령이 사용되었을거 같은데 fmdrv.com 바이너리에서 관련된 코드를 찾는건 어렵지 않아 보입니다.
파일을 역공학 분석하면 아래와 같은 코드들이 몇군데서 발견 됩니다.
seg000:0654 sub_10654 proc near ; CODE XREF: sub_10674+B↓p
seg000:0654 ; sub_10F64+1C↓p
seg000:0654 50 push ax
seg000:0655 51 push cx
seg000:0656 52 push dx
seg000:0657 BA 88 03 mov dx, 388h
seg000:065A B9 04 00 mov cx, 4
seg000:065D
seg000:065D loc_1065D: ; CODE XREF: sub_10654+A↓j
seg000:065D EC in al, dx
seg000:065E E2 FD loop loc_1065D
seg000:0660 5A pop dx
코드에서 사용된 포트를 388h에서 378h로 바꾸고자 한다면 Hex Editor를 사용해서 88 03로 검색되는 부분을 78 03으로 변경하면 될 것 같습니다.
하지만 실제 원하시는 형태로 동작할지는 장담하지 못하겠네요. ^^;
감사합니다. hex editor를 사용한 방법이 있겠네요.
한 번 시도는 해 볼만한 거 같습니다.
사실 NoName님처럼 역 어셈블을 시도해봤지만
뭔가 잘못됐는지 388h를 입출력하는 코드가 안 나오더라고요. ㅎㅎ
수고하셨습니다.