Did you know that you can navigate the posts by swiping left and right?

abex 1st crackme 분석/해결

02 Mar 2017 . category: Reversing . Comments
#reversing #hacking #crackme #abex

본 글은 이승원이 집필한 「리버싱 핵심원리」 를 읽고 공부(정리)한 내용을 바탕으로 쓰여졌습니다.

abex’ 1st crackme 분석

먼저 실행파일을 실행해보면 MessageBox로 “Make me think your HD is a CD-Rom.”이라는 문장이 출력되며 확인을 눌러보면, “Nah… This is not a CD-ROM Drive!”라는 문장을 출력한다. 우리가 알 수 있는 것은 HD(Hard Drive)를 CD-Rom으로 인식하게 하라는 것과, 그렇게 하지 못했다는 것만 나온다. 일단 이것만봐서는 우리가 알 수 있는 정보가 거의 없다. 그래서 OllyDbg로 실행파일을 돌려보자.

Imgur

위의 사진은 실행파일을 OllyDbg로 돌린 결과이다. main 함수의 시작점부터 끝까지 캡쳐해봤다. (401000 ~ 401061) 파란 박스, 빨간박스, 그리고 보라색 박스로 각각 표시를 해놨는데, 이는 우리가 주의해서 봐야할 부분이다.

목표설정

OllyDbg를 통해서 알 수 있겠지만 우리의 목표는 “Ok, I really think that your HD is a CD-ROM! :P”가 출력되도록 하는 것이다.

방법을 생각해보자

우리가 실행해야할 명령어는 40103D부터 40104B까지 (아래쪽 빨간박스)의 주소를 가지고 있다. 그리고 그것을 실행할 수 있는 방법은 401026에 있는 명령어, JE SHORT 0040103D 를 실행해야한다. 이 명령어가 어떻게 분기(점프)할 것인가에 대해서는 JE 명령어를 알아야한다. 첫번째 방법은 JE 명령어 때문에 ZF(Zero Flag) 값을 강제로 1로 만들어서 JE 명령어를 수행하는 방법이 있다. 두번째 방법은 JE 명령어의 조건을 맞추는 방법(CMP의 조건을 맞추는 방법)이 있다. 세번째 방법은 JE 명령어 자체를 수정하는 방법이 있다. 인터넷에 검색해보면 세번째 방법을 가장 많이 사용하는 듯 하다.

해결책 1. ZF 값을 수정하는 방법

좋은 방법은 아니다. 왜냐하면 ZF 값 자체를 저장할 방법은 없기 때문이다. 어쩌면, 내가 아직 모르는 것일지도 모른다. 그러나, 이 방법은 OllyDbg를 켜져 있을 때는 아주 손쉽게 JE 구문을 분기시킬 수 있는 가장 간단한 방법이기 때문에 여기서 소개하려고 한다. (사실상 패치라고 할 수는 없다. 단순히 해커의 확인방법일 뿐..) 왜 ZF 값을 수정하면 JE 명령어를 분기할 수 있는지는 JE 명령어를 이해해야한다. JE 명령어는 ZF(Zero Flag)가 1이면 명령어에 담긴 메모리 주소로 분기한다. 고로, ZF만 바꿔주면 그 순간에는 제대로 점프할 수 있다.

풀이과정

아주 단순한 방법이다. JE 명령어 위에는 CMP 명령어가 있다. CMP 명령어를 실행하고 난 이후에 JE 명령어 위에 커서가 있을 때(메모리 주소가 빨간색으로 음영표시될 때) ZF를 더블클릭하면 0에서 1로 바뀐다. 그리고 실행을 하면 JE 명령어가 ZF가 1이기 때문에 0040103D 자리로 분기한다.

가능한 이유

CMP EAX,ESI 연산을 통해서 ZF가 0이 될지 1이 될지를 결정하는데, 본 프로그램에서는 EAX와 ESI가 같지 않기 때문에 ZF == 0 이므로 JE 명령어가 우리가 원하는 부분으로 분기하지 않는 것이다. 다시 말해서 CMP 명령어를 실행하든 말든 JE 명령어가 실행되기 전에 ZF 값을 임의로 수정해버리면 우리가 원하는대로 JE 명령어가 실행된다.

해결책 2. 조건을 맞추는 방법

해결책 3에 비해서는 좀 어려운 방법이다. 이게 제작자가 의도한 해결책인지는 모르겠지만 필자가 생각하기에는 가장 본연의 의도와 가까운 해결방법이라고 생각한다. 조건을 맞추는 방법이다. 분명히 문제에서 HD를 CD-Rom으로 인식하게끔 하라고 했다. 우리는 JE 명령어에서 0040103D 주소로 분기하기를 원한다. 즉 ZF 값이 1이 되길 원하는 것이고, 이렇게 되기 위해서는 바로 위의 명령어인 CMP 명령어의 조건이 맞아야한다. (CMP EAX,ESI 명령어에 의해서 EAX와 ESI가 일치해야한다.) 바로 조건을 맞추는 방법을 통해 문제를 해결해보려고 한다. EAX와 ESI가 일치하게 되는 방법은 매우 많지만 2가지를 소개하려고 한다.

풀이과정 1. MOV 명령어를 사용하자.

괜찮은 방법이다. CMP 명령어 위에는 몇개의 INC 명령어와 DEC 명령어로 되어있는데 이 부분은 CMP의 피연산자(Operand)에 대한 연산이다. 우리가 수정해줘서 결과적으로 CMP의 수행결과가 ZF == 1로 바꾸기만 한다면, 즉 CMP의 Operand가 서로 같다면 수정해도 되는 별 필요 없는 부분이다. (따로 다른 API를 호출한다던가, 스택을 건든다던가 하는 부분이 아니니깐 말이다.) 그러므로 위의 명령어 부분(INC나 DEC)의 마지막 (꼭 마지막을 해야한다. 마지막으로 안하면, INC나 DEC에 의해 CMP 조건을 충족하지 않을 수도 있으니깐..)을 MOV EAX, ESI 명령어로 수정하기만 하면 된다.

INC ESI
MOV EAX,ESI ; 이 부분이 수정된 부분이다.
CMP EAX,ESI
JE SHORT 0040103D

풀이과정 1이 가능한 이유.

대부분 위에서 설명했지만, CMP의 조건만 충족하면 되기 때문에 MOV 명령어를 이용해서 강제로 EAX와 ESI 값을 일치시켜줬기 때문에 CMP의 연산결과가 0이 되고 고로 ZF = 1이 수행되는 것이다. 한가지 주의해야할 점은 MOV 명령어는 2바이트짜리 연산자라는 것이다. 반면에 INC나 DEC 명령어는 1바이트짜리 연산자이기 때문에, MOV 명령어로 어셈블리 코드를 수정하게 될 경우 2줄이 사라지는 마법을 볼 수 있다. 고로, 이 부분에 유념하여 어셈블리 코드를 수정하도록 하자.

풀이과정 2. EAX와 ESI 값을 계산해서 일치시켜주자.

항상 가능하지는 않은 방법이다. 왜냐하면, 프로그램에서 원래 할당받지 않은 메모리 주소를 참조하게 될 수도 있기 때문에 위험성이 따르는 방법이다. 40101F의 주소에 의미없는 JMP 명령어가 있다. (의미가 없다는 이유는 바로 밑에 있는 코드로 점프하기 때문에 존재 가치가 없다.) 고로, 이 JMP 명령어를 빈공간의 아무 곳으로 분기하고 그 자리에 INC EAX를 2번, JMP SHORT 00401021 명령어를 추가해주면 가능해진다. EAX와 ESI 값이 일치하고 CMP의 조건에 부합하므로 ZF = 1이 된다. 아래는 이를 수정해야하는 소스코드이다.

0040101F   . /EB 46         JMP SHORT 00401067 ; 추가 :: 4010767로 점프
00401021   > |46            INC ESI
00401022   . |46            INC ESI
00401023   . |48            DEC EAX
00401024   . |3BC6          CMP EAX,ESI
00401026   . /74 15         JE SHORT 0040103D

00401067   > \40            INC EAX ; 추가해준 부분
00401068   .  40            INC EAX ; 추가해준 부분
00401069    ^ EB B6         JMP SHORT 00401021 ; 추가 :: 다시 원래대로 복귀

풀이과정 2가 가능한 이유

실제로 디버거로 실행해보면 CMP EAX,ESI를 연산할 때, EAX와 ESI 값이 2 차이난다. 고로, EAX에 2를 더해주면 EAX와 ESI 값이 같아지므로 CMP 조건에 부합하게된다.

해결책 3. JE 명령어를 수정

아주 단순하게 JE 명령어를 이용할 생각을 하지말고, JE를 JMP로 어셈블리 명령어를 바꿔서 수행하면 간단하게 해결할 수 있다.

풀이과정

00401026   . /74 15         JE SHORT 0040103D

위의 코드를 아래의 코드로 바꾸면 된다. 매우 간단하다.

00401026     /EB 15         JMP SHORT 0040103D

가능한 이유

JE는 ZF가 0이면 분기하지 않고 1이면 분기하는데, JMP 명령어는 반드시 Operand로 받은 주소로 분기한다. 또한 JE와 JMP 모두 2바이트이므로 그 다음 함수를 고려할 필요도 없다. (사실 고려해봤자 실행이 안될 것이기 때문에 상관이 없다.)


Me

Coding Future, Decoding Society.