위의 4개의 레지스터들은 주로 산술연산(ADD, SUB, XOR, OR 등) 명령어에서 상수/변수 값의 저장 용도로 많이 사용된다. 어떤 어셈블리 명령어(MUL, DIV, LODS 등)들은 특정 레지스터를 직접 조작하기도 한다(이런 명령어가 실행된 이후에 특정 레지스터들의 값이 변경됨).
EAX는 일반적으로 함수 리턴값에 사용된다. 모든 Win32 API 함수들은 리턴 값을 EAX에 저장한 후 리턴한다.
EBP: Pointer to data on the stack (in the SS segment)
ESP: Stack pointer (in the SS segment)
위 4개의 레지스터들은 주로 메모리 주소를 저장하는 포인터로 사용된다.
ESP는 스택 메모리 주소를 가리키며, 어떤 명령어들(PUSH, POP, CALL, RET)은 ESP를 직접 조작하기도 한다.
EBP는 함수가 호출되었을 때 그 순간의 ESP를 저장하고 있다가, 함수가 리턴하기 직전에 다시 ESP에 값을 되돌려줘서 스택이 깨지지 않도록 한다. (이것을 Stack Frame 기법이라고 함)
ESI : source pointer for string operations
EDI : destination pointer for string operations
ESI와 EDI는 특정 명령어들(LODS, STOS, REP MOVS 등)과 함께 주로 메모리 복사에 사용된다.
< EFLAGS: Program Status and Control Register >
0. CF(Carry Flag) :
부호 없는 수(unsigned integer)의 오버플로가 발생했을 때 1로 세팅된다.
예를 들어 1바이트의 값 1111 1111에 1을 더하면 1 0000 0000이 되어 마지막 1이 1바이트를 넘어서는 오버플로가 발생하여 CF는 1로 세팅된다. 그런데 더하기 뿐만 아니라 뺄셈에서도 오버플로가 발생하는데,
예를 들어 1바이트 값 0000 0001에 10을 빼면, .... 11111111 1111이 되어 1바이트를 넘어서는 오버플로가 발생하게 되어 CF는 1로 세팅된다. 이러한 성질 때문에 조건 점프 명령어 JA, JB에 이용된다.
2.PF(Parity Flag):
연산 결과, 하위 8비트에 '1'이 짝수개 있으면 1로 세팅되고 그 외에는 0으로 세팅된다.
4.AF(Auxiliary carry Flag):
연산 결과 하위 니블(4bits)에서 비트 범위를 넘어섰을 때 참이 된다. 이진화 십진법(BCD) 연산에 사용된다.
6. ZF(Zero Flag)
연산 명령 후에 결과 값이 0이 되면 ZF가 1(True)로 세팅된다. CMP 명령어도 내부적으로 피연산자끼리 빼는 역할을 하기 때문에, 두 피연산자가 같으면 ZF가 1이 된다. 단, MOV 명령어로 0을 만들어줄 경우는 제외. 주로 조건 점프 명령어 JE, JNE에 이용된다.
7.SG(Sign Flag)
연산 명령 후 최상위 비트(부호 비트)가 1이라면 SG=1이 되고, 최상위 비트가 0이라면 SG=0이 된다.
8.TF(Trap Flag)
TF값을 1로 세팅하면, CPU는 Single Step 모드로 변경된다. CPU는 Single Step 모드에서 하나의 명령어를 실행시킨 후, EXCEPTION_SINGLE_STEP 예외를 발생시킨다. 이후 Trap Flag는 자동으로 초기화(0) 된다.
11. OF(Overflow Flag) :
연산 명령후 부호 있는 수(signd integer)의 최상위 비트에 오버플로가 발생했을 때 1로 세팅된다.
예를 들어 1바이트 값 0111 1111에 1을 더하게 되면, 최상위 비트가 1로 되기 때문에 OF=1이 된다. 그러나 오버플로가 발생했더라도, 최상위 비트가 0이라면 OF=0이 되는데,
예를 들어, 1바이트 값 0111 0000에 1100 0000을 더하게 되면, 10011 000이 되어 오버플로가 발생하긴 했지만, 최상위 비트가 0이기 때문에 OF=0이 된다.
SG와 별반 차이 없어보이지만, SG는 연산결과 단순히 최상위 비트가 1이면 SG=1이지만, OF는 "오버플로" and "최상위 비트 1"이라는 조건이 충족되어야 OF=1이 된다.
< Instruction Pointer >
EIP : Instruction Pointer
EIP는 CPU가 처리할 명령어의 주소를 나타내는 레지스터이며, 크기는 32비트(4바이트)이다(16비트 IP 레지스터의 확장 형태임). CPU는 EIP에 저장된 메모리 주소의 명령어(instruction)를 하나 처리하고 난 후 자동으로 그 명령어 길이만큼 EIP를 증가시킨다. 이런 식으로 계속 명령어를 처리해 나간다.
범용 레지스터들과 다르게 EIP는 그 값을 직접 변경할 수 없도록 되어 있어서 다른 명령어를 통하여 간접적으로 변경해야 한다. EIP를 변경하고 싶을 때는 특정 명령어(JMP, Jcc, CALL RET)를 사용하거나 인터럽트(interrupt), 예외(exception)를 발생시켜야 한다.
명령어
MOV
ex) MOV Reg, Imm
ex) MOV Reg, Reg
ex) MOV [Mem], [Mem] (메모리에서 메모리로 이동 불가능)
LEA(Load Effective Address)
주소를 가져오는 명령어, 전역변수의 주소도 가져올 수 있다. Source 피연산자의 유효 주소를 계산하여 Destination 피연산자에 복사한다. 간단히 주소를 알아내서 복사하는 명령어다. ex) lea EAX,[EBP-4]
TEST: 논리 비교(Logical Compare)
bit-wise logical 'AND' 연산과 동일(operand 값이 변경되지 않고 EFLAGS 레지스터만 변경됨)
두 operand 중에 하나가 0이면 AND 연산 결과는 0 → ZF=1로 세팅됨.즉, TEST의 AND 연산 결과값이 0이면 ZF가 1로 세트, 0이 아니면 ZF가 0으로 세트된다.
(++ TEST 추가
어셈블리어에서 TEST연산은 오퍼랜드 끼리 AND연산하여 결과값을 CMP연산과 같이 저장하진 않는다.
단지 플래그 값을 세팅하여 분기문에게 영향을 준다 ex) JE, JZ.... 같은 분기문
여기서 중요한것은 TEST EAX, EAX와 같은 명령어다.
얼핏보면 같은 레지스터끼리 AND연산하면 EAX값이 나올텐데 왜 하는걸까라는 생각을 할 수 있다.
하지만 바로 위에 쓰인TEST명령어는 단지 EAX의 값이 0이냐 아니냐를 판단하기 위해서다
만약 EAX의 값이 0이라면 AND연산결과 당연히 0이 나올것이고, Z플래그 (ZF가 세팅될 것이다.)
밑에 JE 0040100
점프문이 있다면 ZF가 세팅되므로 0040100주소로 점프할 것이다. )
JMP(Jump):
피연산자의 위치로 실행 흐름이 변경된다. 피연산자가 가리키는 코드로 점프 뛰어서 실행한다고 생각하면 된다. 피연산자에는 레이블이나 레지스터, 메모리 값이 올 수 있다.
short 점프는 -128 ~ 127 byte범위 안에서,
long 점프는 -2147483648 ~ 2147483647 byte 범위에서 사용된다.
JMP 명령어는 되돌아올 리턴 어드레스 값을 저장하지 않는다.
가리키는 값을 NULL과 비교. 아니면 갯수를 세고 NULL을 만나면 갯수를 리턴한다.
레이블은 인라인 함수 밖에서도 지정가능하다. 같은 레이블은 불가하고 숫자, 공백 또한 불가능하다.
ex) JMP short Imm
ex) JMP Reg
ex) JMP [Mem]
조건점프 명령: TEST로 검사 혹은 CMP로 비교 조건에 맞는 경우에만 점프
조건 점프명령 (조건 분기)
산술, 논리 연산
JA(Jump if above)
CMP a > b
JB(Jump if below)
CMP a < b
JE(Jump if equal)
CMP a == b ZF가 1이면, 해당 주소로 점프.
JNE (Jump if not equal)
CMP a != b ZF가 0이면, 해당 주소로 점프.
JZ (Jump if zero)
TEST EAX, EAX ( EAX = 0 )
JNZ (Jump if not zero)
TEST EAX, EAX ( EAX =1 )
점프 계열 명령어들은 상당히 많은 조합이 가능한데 이러한 조합을 전부 다 외울 필요는 없으며, 아래 Keyword만 알고 있으면 된다.
J + ...
N (not) E (if equal) Z (if zero) A (if above) B (if below) L (if less than) G (if greater than) S (EFLAGS의 Sign Flag) C (EFLAGS의 Carry Flag) P (EFLAGS의 Parity Flag) O(EFLAGS의 Overflow Flag)
예를 들어 JNAE 면, Jump if not above or equal 이라는 의미로, 같거나(equal) 크지(above) 아니하면(not) 점프하라는 명령이다.
CMP
두 피연산자의 값이 같은지 검사한다. 만약 두 값이 같으면 ZF는 1이 되고, 다르다면 ZF는 0이 된다. 비교를 위해 내부적으로는 두 피연산자를 빼는 연산을 하는데, 예를 들어 A=1, B=1일 때 CMP A, B라는 명령어를 내리면 A-B==0이기 때문에 ZF는 1이 된다. (ZF가 연산결과가 0이되면 1이되는 성질을 이용한 것).
JE(Jump if equal) : 두 값이 같으면 점프(A == B) == JZ(Jump if zero): ZF가 1이면 점프.
JNE(Jump if not equal) : 두 값이 다르면 점프 (A != B) == JNZ(Jump if not zero): ZF가 0이면 점프.
JE(JZ), JNE(JNZ)는 제로 플래그(ZF)를 참조하여 점프할지 안할지 결정하는 명령어이다.
JE(JZ)는 ZF가 1이면 점프를 하고, JNE(JNZ)는 ZF가 0이면 점프를 한다.
주로 CMP 명령어와 함께 쓰이는데, CMP 명령어는 내부적으로 빼기 연산을 하기 때문에, A, B가 같으면 ZF가 1이 된다.
JE와 JZ는 제로 플래그(ZF)가 0인지 아닌지 조사하는 명령어이기 때문에, 두 개는 똑같은 동작을 하며 실제로 기계어(IA-32)도 보면 똑같이 생성된다. 그래서 OllyDbg같은 디버깅 프로그램에서 JE로 작성하든 JZ라고 작성하든 전부 JE로 통일되어 나온다. 같은 맥락으로 JNE와 JNZ도 똑같다. 이런식으로 같은 동작을하는 다른 명령어들이 꽤 있다.
JA(Jump if above): 왼쪽값이 크면 점프. (A > B)
JB(Jump if below): 왼쪽값이 작으면 점프. (A < B) ==JC(Jump if carry flag 1): CF가 1일 때 점프.
JAE(Jump if above or equal): 왼쪽값이 크거가 같으면 점프(A >= B) ==JNC(Jump if carry flag 0): CF가 0일 때 점프.
JBE(Jump if below or equal): 왼쪽값이 작거나 같으면 점프
JA와 JB는 캐리 플래그(CF) 참조하여 점프할지 안할지 결정하는 명령어이다. JA는 CF가 0일 때 점프를 하며, JB는 CF가 1일 때 점프를 하게 된다. 주로 CMP와 함께 쓰이는데, CMP 명령어는 내부적으로 빼기 연산을 하기 때문에, A, B에서 A가 크면 오버플로가 발생하지 않아 CF=0, B가 크다면 오버플로가 발생하여 CF=1이 되는 성질을 이용한 것이다.
ex)
401000 MOV EAX, 1
401005 CMP EAX, 2 (오버플로 발생되어 CF 1로 세팅)
401008 JA SHORT 401000 (CF가 1이므로 거짓)
40100A JB SHORT 401000 (CF가 1이므로 참) 40100으로 점프.
JG(Jump if greater): 왼쪽값이 크면 점프 (A > B) == JNGE()
JL(Jump if less): 왼쪽값이 작으면 점프 (A < B) == JNLE()
JA, JB와 별반 다를게 없어보이지만, JG, JL은 피연산자 값을 부호있는 데이터로 판단하여 크냐 작으냐를 판단하는 차이점이 있다. JG와 JL도 상태 플래그를 보고 판정을 내리는데,
JG의 경우 ZF==0 and SF==OF
JL의 경우 SF != OF
이다. 즉 CMP 명령어 따위를 사용하여, 위의 조건이 맞으면 해당 주소로 점프하게 된다.
왜 저런 조건이 나오냐 하면, 일단 SF만을 보면, A > B일 경우 A-B=양수값(최상위 비트 0, SF=0)이 나오고, A < B일 경우 A-B=음수값(최상위 비트 1, SF=1)이 나오므로 SF만 봐도 누가 더 큰지 알 수 있을 것 같다. 하지만 A= 7, B= -1일 경우 7-(-1) == 0111 + 0001이므로 8이 나오는데 이를 비트로 표현하면 1000(최상위 비트 1, SF=1)이 된다. 그렇다면 A가 더 작다고 나오는데 이는 틀린 것이다. 그러므로 만약 OF=1(오버플로가 발생하여 최상위 비트가 1이 됨)가 되었을 때는 SF=1이라도 A가 더 크다는 올바른 결과가 나올수 있도록 한 조건인 것이다. OF=0(오버플로가 발생하지 않을 때)는 ZF, SF값만으로 대소관계를 알 수 있다.
JLE(JBE 비슷함)
- 형식 : JLE [Code Address] - 내용 : 비교 결과 값이 '0'이거나(ZF=1), 작을 경우 해당 주소로 점프한다. - 예제 : JLE 401140 - 해석 : ZF=1 이거나, 결과값이 작은 경우 401140으로 점프한다. - 조건 : Operand 1 <= Operand 2
NOP(No Operation):
아무 동작을 하지 않는 명령어(그냥 CPU 클럭만 소모됨).
SHL(Shift Left):
Destination 피연산자를 Source 피연산자의 크기만큼 왼쪽으로 각 비트를 시프트시킨다. 최상위 비트는 캐리 플래그(CF)로 복사되고 최하위 비트는 0으로 채워진다.
SHR(Shift Right) Destination 피연산자를 Source 피연산자의 크기만큼 오른쪽으로 각 비트를 시프트시킨다. 최상위 비트는 0으로 채워지고 최하위 비트는 캐리 플래그(CF)로 복사된다.
REP(Repeat String) ECX 레지스터를 카운터로 사용해서 문자열 관련 명령을 ECX>0인 동안 반복한다. 한번 진행될 때마다 ECX 레지스터값이 -1 된다.
NOT:
1의 보수 (비트 반전)
NEG(Negate):
피연산자의 2의 보수를 계산하여 결과를 피연산자에 저장한다. 즉, 부호를 반전 시킨다.
PUSHAD
EAX → ECX → EDX → EBX → ESP → EBP → ESI →EDI 순서로 레지스터의 값을 스택에 PUSH한다. 레지스터들의 값을 보관해야 할 때 사용한다.
POPAD
PUSHAD에 대응되는 명령어.
스택에 존재하는 값을 EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP 레지스터로 POP한다.
스택의 일관성을 유지하기 위해 PUSH해준만큼POP해주는 것처럼, PUSHAD를 사용했으면, POPAD를 사용하여 일관성을 유지해주면 되겠다.
XOR
XOR EAX, EAX -> EAX를 0으로 만듬. 0으로 초기화하는 가장 쉽고 빠른 명령어이다(CPU에게는 MOV EAX, 0 명령어보다 더 쉽고 빠르다).
우선 1번문제 files 폴더에 reverseME.exe 파일을 올리디버거로 끌어서 실행시킵니다.
(Options - Add to Explorer 에서 Add Ollydbg to menu~~ 를 클릭해두시면 reverseMe 파일을 우클릭 하여 Open with Ollydbg 클릭해서 바로 올리디버거로 띄울 수 있습니다. )
그럼 이제 올리디버거 첫 화면부터 알아봅시다.
[그림 1] 가상메모리
먼저 가장 왼쪽에 나와있는 부분이 가상메모리 주소를 나타내는 부분입니다.
프로그램은 실행이 되는순간 프로세스가 되는데, 이는 가상메모리에 프로그램 파일이 올라간다라고 할 수 있습니다.
여기서 가상메모리란 운영체제가 물리메모리를 효율적으로 사용하기 위한 시스템입니다.
우리가 흔히 말하는 메모리 8GB, 16GB는 물리메모리를 뜻하며, 가상메모리가 없으면 이 물리 메모리를 효율적으로 사용하지 못할 것입니다.
[그림 2] 기계어 코드와 어셈블리어
그 오른쪽에는 기계어 코드와 어셈블리어가 있습니다.
기계어 코드는 CPU가 읽어들여 실행시키는 코드를 뜻하는데 사람이 잘 알아볼 수 없는 숫자들로 이루어져있습니다.
이를 사람이 알아볼 수 있게 1대1 매칭한 것이 바로 옆에있는 어셈블리어 입니다.
기계어 코드는 일반적으로 잘 안보고 필요할때만 가끔 보기때문에 자리가 부족하다면 살짝 가려놔도 무방합니다.
[그림 3] 주석
그 다음엔 주석이 나옵니다. 프로그램 실행에 직접적인 영향이 있는 부분은 아니며, 코드를 이해하기 쉽게 메모하는 부분이라고 생각하시면 될 것 같습니다. 주로 함수에 실행에 필요한 매개변수에 대한 정보를 나타내고 있는 것으로 보입니다.
[그림 4] 레지스터
다음은 레지스터 입니다. 레지스터는 CPU가 사용하려고 가지고 있는 데이터인데 일종의 변수라고 생각하시면 편할거같습니다. 일반적으로 각각 레지스터는 쓰이는 용도가 다르기 때문에 어느정도는 알고가시면 좋습니다.
EAX : 계산에 대한 저장을 하는 데이터
EBX : (base register) 다목적으로 사용
ECX : (counter register) 숫자를 세거나 for문 등
EDX : (data register) 다목적으로 사용
ESI : (source index) 출발지
EDI : (destination index) 목적지
=> 복사, 붙여넣기 (컨c 컨v)같은 개념
EBP : (base pointer) 스택의 아랫부분
ESP : (stack pointer) 스택의 윗부분
EIP : CPU가 다음에 실행할 명령어 주소
=> 위의 그림에서 검은색으로 표시된 00401000번지가 다음에 실행항 명령어의 위치이며, 실제 명령은 PUSH 0이다.
각 레지스터 앞에 공통적으로 E가 붙어있는데 이는 32비트 환경을 뜻합니다.
숫자 두개를 묶어서 한 바이트로 보는데 8개가 있어서 4바이트 -> 32비트 입니다.
만약 E가 없고 AX만 있으면 16비트를 의미하고,
AH와 AL은 8비트를 의미합니다.
레지스터 부분 더블클릭을 하면 직접 데이터를 컨트롤 할 수도 있습니다.
64비트에서는 RAX라고 표현을 하며, 그림으로 설명하면 아래 그림과 같습니다.
[그림 4-1] 레지스터 구조
[그림 5] 덤프
다음 왼쪽 아래에는 덤프값을 볼 수 있습니다. 위에는 명령어 형식으로 되어있고, 아래는 그냥 데이터로 쫙 표현해놓은 값입니다. 찾고싶은 위치가 있다면 컨트롤 + G를 누르고 401000 같은 주소를 입력하면 됩니다.
[그림 6] 스택
마지막으로 오른쪽 밑에는 스택을 나타내는 창 입니다. 위에 레지스터에 ESP와 EBP와 비교해보면 스택이 0012FFC4 주변을 가리키고 있다는 것을 알 수 있고, 왼쪽에 나타나있는 가상메모리와는 많이 다른 부분에 위치하고 있음을 알 수 있습니다.
이쪽 영역에 마우스 우클릭 후 Address - Relative ESP or EBP를 클릭하면 ESP나 EBP로부터 얼마나 떨어져있는지도 확인할 수 있습니다.
F8을 누르면 한단계 실행이 됩니다. 한번 눌러봅시다.
[그림 7] 한단계 실행 화면
F8을 누르고 나면 가장 왼쪽에 검은색으로 칠해진 것이 한단계 내려갔고, PUSH 0 이라는 명령어가 실행되었습니다.
오른쪽에 레지스터도 바뀐 것을 볼 수 있습니다. PUSH는 스택에 값을 넣는 것을 의미하며, 스택에 값을 넣었기 때문에 ESP가 한 바이트 이동하게 되었습니다. (12FFC4 -> 12FFC0)
컨트롤 F2를 누르면 처음으로 돌아가는데, 처음 공부할 때 F8을 하나하나 눌러가면서 레지스터나 스택이 어떤식으로 바뀌고 돌아가는지 계속 확인해 보면 유익할것 같습니다.
어느정도 사용법을 익혔으면 이제 본격적으로 문제를 풀어봅시다!
우선 실습 파일인 ReverseMe.exe를 실행시켜 봅시다.
일단 클릭을 해보면 라이센스가 없고 새로 구입하라는 메시지가 뜹니다. 1번 문제의 목적은 이 오류를 우회를 해서 라이센스가 있는 척을 하는게 문제입니다.
다시 올리디버거로 돌아가봅시다.
==================
올리디버거 단축키
한스텝 진행 (함수 진입 x) : F8
한스텝 진행 (함수 진입 x) : F7
재시작 : Ctrl + F2
브레이크 포인트 : F2
주소 이동 : Ctrl + G
브레이크 포인트까지 계속 진행 : F9
==================
좀전에 가장 처음 명령어를 실행해두었으니 그 다음부터 봅시다.
다음 나온 명령어는 CALL 이라는 명령어 입니다.
CALL은 함수를 호출하는 명령어로 예를들어 C언어로 짜여진 프로그램의 함수를 실행하는 명령어 입니다.
[그림 7] 한단계 실행 화면
다음 나온 명령어는 CALL 이라는 명령어 입니다.
CALL은 함수를 호출하는 명령어로 예를들어 C언어로 짜여진 프로그램의 함수를 실행하는 명령어 입니다.
호출하는 함수는 KERNEL32.DLL에 있는 GetModuleHandleA라는 함수 입니다.
마이크로소프트에서 제작한 DLL인데, 본 문제에서는 크게 중요한 부분이 아니기 때문에 넘어가도록 하겠습니다.
(자세한 설명이 궁금하면 MSDN 사이트에서 검색하시면 됩니다.)
다음 명령어로 가봅시다. (F8)
다음은 MOV 명령어들이 많이 나옵니다.
처음 나와있는 명령어를 보면
MOVDWORD PTR DS:[402177], EAX
라고 적혀있는데, EAX의 값을 402177 주소의 값에 넣는다는 의미입니다.
DWORD PTR DS는 크게 신경쓸건 아니고 뒤에 괄호안에있는 주소로 넣는다 정도만 생각해도 될 것 같습니다.
명령어를 실행한 후에 Ctrl + G를 눌러서 실제 저 자리에 EAX 값이 들어갔는지한번 확인해봅시다.
현재 EAX값은 400000인데, 해당 주소로 들어가보면 00 00 40 00 이 들어가있습니다.
이는 리틀엔디안 방식 때문에 그렇습니다.
그런데, 리틀엔디안과 빅엔디안에 대해서는 잘 설명해놓은 글이 많기때문에 자세히 알고싶으면 구글링해보시기 바랍니다.
간단하게 설명드리자면
12345678 이 있으면 리틀엔디안 방식이면
78 56 34 12 이런식으로 들어가게 됩니다.
따라서 400000도
00 00 40 이 됩니다.
다음 MOV 명령어들도 마찬가지로 들어가게 되며, 하나하나 보시면서 이해해보시기 바랍니다.
다만
이 명령어는 402177의 주소에 있는 값을 EAX로 넣는 명령어 입니다.
항상 콤마를 기준으로, 뒤에 있는 값을 앞에 있는 값에 붙여넣는다는 개념으로 이해하시면 됩니다.
다음 PUSH 명령으로 스택에 4와 EAX에 있는 값인 400000을 넣었습니다.
다음은 LoadIconA라는 함수를 호출하는데 별다른 내용이 없기 때문에 이런게 실행되고 있구나 라는 정도만 아시면 됩니다.
(함수에 대한 리턴값은 EAX에 저장이 되니 참고해주시면 되겠습니다.)
그 밑에도 딱히 문제될 것 같은 명령어는 안보이니 계속 실행해 주시면 되는데, 밑에 보시면 CreateFileA라는 함수가 보입니다. 다른건 몰라도 파일을 만들고 하는 함수는 살짝 의심스러우니 자세히 봅시다.
위에도 보신것 처럼 별다른 문제가 없는 명령어가 많고 하나하나 F8을 눌러가면 불편하기 때문에, 의심스러운 곳을 BreakPoint로 걸어놓고 (F2) 해당 지점까지 자동으로 실행하는 F9를 누릅시다.
보시는것 처럼 00401073 위치에 F2로 BreakPoint를 설정해 놓으면 빨간색으로 변하고, F9를 누르면 해당 위치까지 한번에 이동하게 됩니다.
CreateFileA 함수는 유명한 함수이기 때문에 msdn에서 구조를 살펴볼 필요가 있습니다.
검색은 구글에 CreateFileA msdn 이라고 치면 MS에서 만든 사이트가 나오는데 함수 구조를 잘 설명해두었습니다.
msdn에서 보시면 이처럼 CreateFileA 함수 호출에 필요한 여러가지 인자들을 소개해 주었는데,
이 그림에서 함수호출 직전 PUSH 명령어들이 많습니다. 이 과정이 인자를 채워주는 과정이라고 보시면 됩니다.
오른족에 주석을 보시면 어떤 변수에 어떤 값을 넣었는지 쉽게 볼 수 있습니다.
일단 한번 함수를 호출해 봅시다 (F8)
함수를 실행하면 EAX가 FFFFFFFF로 채워진 것을 볼 수 있다.
위에 함수에 대한 결과를 EAX에 저장한다고 했는데 CreateFileA 함수를 호출하고 난 결과가 FFFFFFFF이란 뜻입니다.
함수 리턴값은 msdn에서 Return Value를 보시면 됩니다.
해당 함수에서는 실패하면 INVALID_HANDLE_VALUE가 반환된다고 하였는데 그 결과가 -1 (FFFFFFFF) 입니다.
실패한 이유는 함수 msdn에 나와있는것 처럼 CreateFileA 함수는 파일을 만드는것 뿐만 아니라 파일을 Open하기도 합니다.
하지만 우리의 실습파일을 보면
함수의 옵션에 OPEN_EXISTING으로 설정되어 있고 해당 파일을 Keyfile.dat라고 설정해 두었습니다.
하지만 실습폴더에는 Keyfile.dat 파일이 없죠. 그래서 오류가 나게 된 것입니다.
OPEN_EXISTING 역시 msdn에서 검색하시면 어떤 옵션인지 알 수 있습니다.
(해당 함수 페이지에서 컨트롤+f로 OPEN_EXISTING 검색하시면 편리합니다.)
함수 호출에는 실패했지만 일단 다음으로 넘어가봅시다.
다음은 EAX와 -1을 비교하는 부분입니다. 현재 FFFFFFFF이 들어와있는데 이건 -1과 같은 값을 의미합니다.
00000000 에서 1을 빼면 FFFFFFFF이 되겠죠.
따라서 해당 명령어는 위의 함수가 실패했는지 성공했는지를 따지는 부분이 되겠습니다.
그리고 해당 명령어를 실행하면
우측 레지스터 부분에 Z라는 부분이 0에서 1로 바뀌었습니다.
C,P,Z ... 나와있는 부분은 Flag인데, 각 연산의 결과에 따라 바뀌는 값들입니다.
CMP 명령을 했을때 두 값이 같다면 0, 다르면 1이 결과값으로 나오는데,
Z는 Zero Flag로, CMP의 결과값이 0이면 1로 바뀌게 됩니다.
즉, CMP 명령 이후 값이 같다면 Z가 1로 된다는 뜻입니다.
위의 경우에도 EAX가 -1과 같은 값이기 때문에 Z가 1로 바뀌게 된 것입니다.
다음은 JNZ 명령어가 나옵니다.
기본적으로 앞에 J가 붙으면 JUMP 명령으로, 지정한 위치로 이동을 하는것이고, 뒤에 NZ는 옵션입니다.
NZ 같은 경우는 Not Zero 이고, Zero의 값이 0일때 JMP 명령을 실행하게 됩니다.
만약 JZ였으면 Zero가 1이기 때문에 점프를 하게 되는것이고
위치는 보기 쉽게 화살표로 나타내져 있고, 중간 명령어들을 건너뛰고 바로 화살표가 가리키는곳으로 이동하게 됩니다.
만약 점프를 안해버리면 저 위치로 넘어가지 않고, 바로 다음 명령어인 PUSH 0을 하게 되고, 결국은 MessageBoxA라는 함수를 호출하게 되버리는 것이죠. 저 메세지 박스가 처음 ReverseMe.exe를 실행했을때 라이센스를 구입하라는 메시지를 의미합니다. 그리고 그 다음은 ExitProcess를 호출하여 프로세스를 종료하는 루틴을 밟게 됩니다.
그렇다면 우리가 할 수 있는 일은 CMP 이후 JNZ 명령을 실행하기 전에 Zero Flag를 바꾼 다음 JNZ에서 정상적으로 JUMP할 수 있게 해줘야 합니다. Zero Flag는 더블클릭으로 쉽게 바꿔줄 수 있으니 한번 바꿔봅시다.
표시된 부분을 더블클릭해서 Zero Flag를 0으로 바꿨습니다.
이제 F8을 눌러 명령을 실행하면 정상적으로 JUMP가 됩니다.
다음으로 쭉 진행하다 보면 ReadFile 함수가 실행되고, 그 다음
TEST EAX, EAX 가 나옵니다.
이건 EAX의 값이 0인지 아닌지를 확인하는 명령어라고 생각하시면 됩니다.
그런 다음 JUMP 명령어가 두개가 나옵니다. 위에는 좀전에 했던 Zero Flag가 0일때 JUMP를 하고,
밑에는 아무런 조건없이 바로 JUMP를 하는 명령어 입니다.
JNZ로 점프를 하게되면 바로밑에 있는 점프 명령어는 실행되지 않을 것이고,
제로 플래그가 1이라서 JNZ명령을 건너뛰면 밑에있는 JMP 명령을 수행하게 되겠죠.
그럼 여기서의 상황을 봅시다.
Zero Flag는 TEST 명령을 통한 결과도 마찬가지로 반영을 합니다.
결과가 0이면 플래그가 1이되고, 결과가 1이면 플래그가 0이 됩니다.
지금은 EAX의 값이 0이기 때문에 TEST의 결과가 0이 되고, 오른쪽에 제로 플래그도 1이 되어버렸습니다.
이렇게 되면 JNZ 명령이 아닌 JMP 명령이 있는 곳을 수행하게 되죠. 그럼 JMP 명령을 실행했을때 어디로 가는지 한번 확인해봅시다.
아까처럼 명령어 위치에 클릭했을 때, 빨간색 화살표가 가리키는 위치를 보시면 됩니다.
그쪽을 찾아가 보면 4010F7의 위치를 볼 수 있는데, 이 위치에 가게 되면, 아까와 유사한 메세지 박스 함수와 프로세스를 종료하는 함수가 있습니다.
결론적으로 말하자면, CreateFileA함수에 대한 결과를 임의로 바꿨기 때문에, ReadFile의 결과도 00000000이 되어 EAX에 들어가게 되었고, 이때 TEST 명령을 통해 JNZ 명령을 수행하지 않고 JMP 명령을 통해 다시 KeyFile이 올바르지 않다는 메세지박스를 보게되는 것입니다.