1. CPU 레지스터
우선 CPU가 무슨 일을 하는지 알아보자. 간단하게 CPU(Central Processing Unit)는 메모리로부터 명령어를 가져와서(fetch) 어떤 명령어인지 해석하고(decode) 실행하는(execute) 동작을 한다. CPU에 대해서는 리버싱 작업에서 많이 사용되는 레지스터에 대해서만 알아보도록 하겠다.
레지스터는 CPU에서 사용하는 고속의 기억장치다. CPU는 연산을 수행하기 위해 메모리에 있는 데이터를 CPU 내부에 있는 레지스터로 가지고 온다. 레지스터의 종류로는 범용 레지스터, 세그먼트 레지스터, 상태 플래그 레지스터, 명령 포인트 레지스터 등이 있다. 올리디버거에 나타나는 레지스터 정보를 살펴보자.
범용 레지스터
□ EAX(Extended Accumulator Register)
곱셈과 나눗셈 명령에서 자동으로 사용되고 함수의 리턴 값이 저장되는 용도로도 사용된다.
□ EBX(Extended Base Register)
ESI나 EDI와 결합하여 인덱스에 사용된다.
□ ECX(Extended Counter Register)
반복 명령어 사용시 반복 카운터로 사용된다. ECX 레지스터에 반복할 횟수를 지정해 놓고 반복 작업을 수행하게 된다.
□ EDX(Extended Data Register)
EAX와 같이 쓰이며 부호 확장 명령 등에 쓰인다.
□ ESI(Extended Source Index)
데이터 복사나 조작 시 Source Data의 주소가 저장된다. ESI 레지스터가 가리키는 주소의 데이터를 EDI 레지스터가 가리키는 주소로 복사하는 용도로 많이 사용된다.
□ EDI(Extended Destination Index)
복사 작업 시 Destination의 주소가 저장된다. 주로 ESI 레지스터가 가리키는 주소의 데이터가 복사된다.
□ ESP(Extended Stack Pointer)
하나의 스택 프레임의 끝 지점 주소가 저장된다. PUSH, POP 명령어에 따라서 ESP의 값이 4Byte씩 변한다,
□ EBP(Extended Base Pointer)
하나의 스택 프레임의 시작 지점 주소가 저장된다. 현재 사용되는 스택 프레임이 소멸되지 않는 동안 EBP의 값은 변하지 않는다, 현재 스택 프레임이 소멸되면 이전에 사용되던 스택 프레임을 가리키게 된다.
명령 포인터
□ EIP(Extended Instruction Pointer)
다음에 실행해야 할 명령어가 존재하는 메모리 주소가 저장된다. 현재 명령어를 실행 완료한 후에 EIP 레지스터에 저장되어 있는 주소에 위치한 명령어를 실행하게 된다. 실행 전 EIP 레지스터에는 다음 실행해야 할 명령어가 존재하는 주소의 값이 저장된다.
세그먼트 레지스터
□ CS(Code Segment)
실행 가능한 명령어가 존재하는 세그먼트의 오프셋이 저장된다.
□ DS(Data Segment)
프로그램에서 사용되는 데이터가 존재하는 세그먼트의 오프셋이 저장된다.
□ SS(Stack Segment)
스택이 존재하는 세그먼트의 오프셋이 저장된다.
플래그 레지스터
□ CF(Carry Flag)
부호 없는 연산 결과가 용랑보다 클 때 세트(1)된다.
□ ZF(Zero Flag)
연산 결과가 0일 때 세트(1)된다. 연산 결과가 0이 아닐 때 해제(0)된다.
□ OF(Overflow Flag)
부호 있는 연산 결과가 용량보다 클 때 세트(1)된다.
□ SF(Sign Flag)
연산 결과가 음수가 되었을 때 세트(1)된다. 연산 결과가 양수가 되었을 때 해제(0)된다.
□ DF(Direction Flag)
문자열 처리에서 연속되는 문자열의 처리 방향에 따라 세트(1)된다.
부동 소수점 레지스터
□ ST(0), ST(1), ST(2), ST(3), ST(4), ST(5), ST(6), ST(7)
범용 레지스터는 레지스터 이름 첫 글자가 ‘E’인 것을 알 수 있다. ‘확장되었다(Extended)’ 라는 의미로 32bit 컴퓨터 환경이 되면서 16bit 레지스터인 AX, BX, CX, DX 등의 레지스터를 32bit로 확장한 것이라고 보면 된다. EAX, EBX, ECX, EDX 레지스터는 32bit, 16bit, 8bit로 사용할 수 있다. EAX 레지스터를 예로 들면 32bit는 EAX, 16bit는 AX, 8bit는 AH, AL로 사용할 수 있다.
32비트 | 16비트 | 상위 8비트 | 하위 8비트 |
EAX | AX | AH | AL |
EBX | BX | BH | BL |
ECX | CX | CH | CL |
EDX | DX | DH | DL |
상위 8비트는 High라서 ‘H’가 붙고, 하위 8비트는 Low라서 ‘L’이 붙는다고 생각하면 외우기 쉽다.
ESI, EDI, EBP, ESP 레지스터는 32bit, 16bit로 사용이 가능하다.
32비트 | 16비트 |
ESI | SI |
EDI | DI |
EBP | BP |
ESP | SP |
데이터 타입
타입 | 설명 |
BYTE | 8비트 부호 없는 정수 |
SBYTE | 8비트 부호 있는 정수 |
WORD | 16비트 부호 없는 정수 |
SWORD | 16비트 부호 있는 정수 |
DWORD | 32비트 부호 없는 정수 |
SDWORD | 32비트 부호 있는 정수 |
FWORD | 48비트 정수 |
QWORD | 64비트 정수 |
TBYTE | 80비트 정수 |
피연산자(operand) 타입
피연산자 | 설명 |
r8 | 8비트 범용 레지스터 |
r16 | 16비트 범용 레지스터 |
r32 | 32비트 범용 레지스터 |
Reg | 임의의 범용 레지스터 |
Sreg | 16비트 세그먼트 레지스터 |
Imm | 8, 16, 32비트 즉시 값 |
imm8 | 8비트 즉시 값 |
imm16 | 16비트 즉시 값 |
imm32 | 32비트 즉시 값 |
r/m8 | 8비트 범용 레지스터, 메모리 |
r/m16 | 16비트 범용 레지스터, 메모리 |
r/m32 | 32비트 범용 레지스터, 메모리 |
mem | 8, 16, 32비트 메모리 |
어셈블리 명령어
□ INC(increase)
피연산자에 1을 더한다. 연산 결과에 따라 ZF나 OF가 세트 될 수 있다.
INC reg
INC mem
□ DEC(Decrease)
피연산자에서 1을 빼는 명령이다. 연산 결과에 따라 ZF나 OF가 세트 될 수 있다.
DEC reg
DEC mem
□ ADD(Add)
Destination에 Source의 값을 더해서 Destination에 저장하는 명령이다. 연산결과에 따라서 ZF, OF, CF가 세트 될 수 있다.
ADD destination, source
ADD reg, reg
ADD reg, imm
ADD mem, reg
ADD mem, imm
ADD reg, mem
ADD eax, 123
위 명령은 eax 레지스터에 123을 더해서 eax 레지스터에 저장한다.
□ SUB(Subtract)
Destination에 Source의 값을 빼서 Destination에 저장하는 명령이다. 연산 결과에 따라서 ZF, OF, CF가 세트(1)될 수 있다.
SUB destination, source
SUB reg, reg
SUB reg, imm
SUB mem, reg
SUB mem, imm
SUB eax, 123
위 명령은 eax 레지스터에 123을 빼서 eax 레지스터에 저장한다.
□ MUL(Multiply Unsigned Integer)
부호 없는 al, ax, eax의 값을 피연산자와 곱한다. 피연산자가 8비트이면 al과 곱해서 ax에 저장되고, 16비트이면 ax와 곱하고 dx:ax에 저장된다.
MUL reg
MUL mem
결과에 따라 OF, ZF가 세트(1) 될 수 있다.
□ IMUL(Integer Multiplication)
부호 있는 al, ax, eax의 값을 피연산자와 곱한다. 결과에 따라 CF, OF가 세트(1) 될 수 있다.
IMUL r/m8
IMUL r/m16
IMUL r/m32
단일 피연산자이고, 피연산자를 al, ax, eax에 곱한다.
IMUL destination, value
IMUL r16, r/m16 IMUL r16, imm8
IMUL r32, r/m32 IMUL r32, imm8
IMUL r16, imm16 IMUL r32, imm32
value를 al, ax, eax와 곱해서 destination에 저장한다.
IMUL destination, value, value
IMUL r16, r/m16, imm8 IMUL r16, r/m16, imm16
IMUL r32, r/m32, imm8 IMUL r32, r/m32, imm32
value끼리 곱해서 destination에 저장한다.
연산결과가 destination 레지스터의 크기보다 크다면 OF, CF가 세트(1)된다.
□ DIV(Divide Unsigned Integer)
8, 16, 32비트 부호 없는 정수의 나눗셈을 수행한다.
DIV reg
DIV mem
결과에 따라서 CF, OF, ZF가 세트(1) 될 수 있다.
□ MOV(Move)
Source에서 Destination으로 데이터를 복사한다.
MOV Destination, Source
MOV reg, reg
MOV reg, imm
MOV mem, reg
MOV mem, imm
□ MOVS(Move String)
Source에서 Destination으로 데이터를 복사한다.
MOVS Destination, Source
□ MOVSB, MOVSW, MOVSD(Move String)
SI 또는 ESI 레지스터에 의해 지정된 메모리 주소의 내용을 DI 또는 EDI 레지스터에 의해 지정되는 메모리 주소로 복사한다.
MOVSB는 BYTE 단위로 복사, MOVSW는 WORD 단위로 복사, MOVSD는 DWORD 단위로 복사한다. 방향 플래그(DF)가 1로 세트 되어 있으면 ESI와 EDI는 복사 시에 감소하게 되고, DF가 0으로 세트 되어 있으면 ESI와 EDI는 복사 시에 증가하게 된다.
MOVSB
MOVSW
MOVSD
□ MOVSX(Move with Sign-Extended)
BYTE나 WORD 크기의 피연산자를 WORD나 DWORD 크기로 확장하고 부호는 그대로 유지한다.
MOVSX reg32, reg16 MOVSX reg32, mem16
MOVSX reg16, reg8 MOVSX reg16, mem8
□ MOVZX(Move with Zero-Extended)
BYTE나 WORD 크기의 피연산자를 WORD나 DWORD 크기로 확장하고 남은 비트는 0으로 채운다.
MOVZX reg32, reg16 MOVZX reg32, mem16
MOVZX reg16, reg8 MOVZX reg16, mem8
□ INT(Interrupt)
소프트웨어 인터럽트를 발생시켜 운영체제의 서브루틴을 호출한다.
INT imm
INT 3
□ AND(Logical AND)
Destination과 Source 피연산자의 각 비트가 AND 연산된다. AND 연산을 통해서 OF, CF가 0으로 세트 되고 결과에 따라서 ZF가 1로 세트 될 수 있다.
AND reg, reg AND reg, imm
AND mem, reg AND mem, imm
AND reg, mem
□ OR(Inclusive OR)
Destination과 Source 피연산자의 각 비트가 OR 연산된다. OR 연산을 통해서 OF, CF가 0으로 세트 되고 결과에 따라서 ZF가 1로 세트 될 수 있다.
OR reg, reg OR reg, imm
OR mem, reg OR mem, imm
OR reg, mem
□ XOR(Exclusive OR)
Destination과 Source 피연산자의 각 비트가 XOR 연산된다. XOR 연산을 통해서 OF, CF가 0으로 세트 되고 결과에 따라서 ZF가 1로 세트 될 수 있다. 피연산자의 두 값이 같은 값이라면 결과는 항상 0이 된다.
레지스터를 0으로 초기화 시킬 때 MOV 명령어를 사용하기보다는 XOR reg, reg로 많이 사용한다.
XOR reg, reg XOR reg, imm
XOR mem, reg XOR mem, imm
XOR reg, mem
□ TEST(Test)
두 피연산자 사이에 논리적인 AND 연산을 수행하여 플래그 레지스터에 영향을 주지만 결과값은 저장하지 않는다. OF, CF는 항상 0으로 세트 되고 TEST 연산 결과값이 0이면 ZF가 1로 세트, 0이 아니면 ZF가 0으로 세트 된다.
TEST reg, reg TEST reg, imm
TEST mem, reg TEST mem, imm
TEST reg, mem
□ STC(Set Carry Flag)
캐리 플래그(CF)를 1로 세트 한다.
STC
□ CLC(Clear Carry Flag)
캐리 플래그(CF)를 0으로 세트 한다.
CLC
□ STD(Set Direction Flag)
방향 플래그(DF)를 1로 세트 한다.
STD
□ CLD(Clear Direction Flag)
방향 플래그(DF)를 0으로 세트 한다.
CLD
□ STI(Set Interrupt Flag)
인터럽트 플래그(IF)를 1로 세트 한다.
STI
□ CLI(Clear Interrupt Flag)
인터럽트 플래그(IF)를 0으로 세트 한다.
CLI
□ SHL(Shift Left)
Destination 피연산자를 Source 피연산자의 크기만큼 왼쪽으로 각 비트를 시프트 시킨다. 최상위 비트는 캐리 플래그(CF)로 복사되고 최하위 비트는 0으로 채워진다.
SHL reg, imm8 SHL mem, imm8
SHL reg, CL SHL mem, CL
□ SHR(Shift Right)
Destination 피연산자를 Source 피연산자의 크기만큼 오른쪽으로 각 비트를 시프트 시킨다. 최상위 비트는 0으로 채워지고, 최하위 비트는 캐리 플래그(CF)로 복사된다.
SHR reg, imm8 SHR mem, imm8
SHR reg, CL SHR mem, CL
□ PUSH(Push on Stack)
스택에 값을 넣는다. ESP의 값이 4만큼 줄어들고 이 위치에 새로운 값이 채워진다.
PUSH reg16 PUSH reg32
PUSH mem16 PUSH mem32
PUSH imm16 PUSH imm32
□ PUSHAD(Push All)
EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP 레지스터의 값을 스택에 PUSH한다. 레지스터의 값을 보관해야 할 필요가 있을 때 사용한다.
PUSHAD
□ PUSHFD(Push Flags)
플래그 레지스터를 스택에 PUSH한다. 플래그 레지스터의 값을 보관해야 할 필요가 있을 때 사용한다.
PUSHFD
□ POP(Pop from Stack)
ESP 레지스터가 가리키고 있는 위치의 스택 공간에서 4Byte 만큼을 Destination 피연산자에 복사하고 ESP 레지스터의 값에 4를 더한다.
POP destination
POP reg16 POP reg32
POP mem16 POP mem32
□ POPAD(Pop All from Stack)
스택에 존재하는 값을 EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP 레지스터로 POP한다. PUSHAD 명령어로 스택에 보관해 놓은 레지스터 정보를 다시 이용하려고 할 때 사용한다.
POPAD
□ POPFD(Pop Flags from Stack)
스택에 존재하는 값을 플래그 레지스터로 POP한다. PUSHFD 명령어로 스택에 보관해 놓은 레지스터 정보를 다시 이용하려고 할 때 사용한다.
POPFD
□ XCHG(Exchange)
두 피연산자 내용이 서로 교환된다. XCHG 명령은 imm 값이 피연산자로 올 수 없다.
XCHG reg, reg
XCHG reg, mem
XCHG mem, reg
□ NEG(Negate)
피연산자의 2의 보수를 계산하여 결과를 피연산자에 저장한다.
NEG reg
NEG mem
□ PTR
피연산자의 크기를 재설정한다.
MOV eax, DWORD PTR value
위의 명령어는 value의 크기를 DWORD 크기로 재설정하여 eax 레지스터에 복사하라는 의미이다.
□ OFFSET
세그먼트의 시작으로부터 변수가 위치한 거리까지의 상대적 거리를 리턴한다.
MOV esi, OFFSET value
위 명령어는 value가 존재하는 위치를 세그먼트 시작 지점으로부터의 상대적 거리로 구해서 esi 레지스터에 복사하라는 의미이다.
□ LEA(Load Effective Address)
Source 피연산자의 유효 주소를 계산하여 Destination 피연산자에 복사한다. 간단히 주소를 알아내서 복사하는 명령어라고 생각하면 쉽다.
LEA reg, mem
□ REP(Repeat String)
ECX 레지스터를 카운터로 사용해서 문자열 관련 명령을 ECX > 0인 동안 반복한다.
REP MOVS destination, source
□ JMP(Jump Unconditionally to Label)
피연산자의 위치로 실행 흐름이 변경된다. 피연산자가 가리키는 코드로 점프해서 실행한다고 생각하면 된다. 피연산자에는 레이블이나 레지스터, 메모리 값이 올 수 있다.
short 점프는 -127~127바이트 범위 안에서 사용되고, near 점프는 같은 세그먼트 내부에서 사용된다. far 점프는 현재 세그먼트를 벗어날 때 사용되며 JMP 명령어는 되돌아올 리턴 어드레스 값을 저장하지 않는다. 이 명령어는 무조건 점프 명령어인데 점프 명령은 어떤 형식에 맞을 때만 점프하는 조건 형식의 점프 명령도 있다.
JMP shortlabel JMP reg16
JMP nearlabel JMP mem16
JMP farlabel JMP mem32
□ CALL(Call a Procedure)
함수 호출 시 사용된다. JMP 명령어 같이 프로그램의 실행 흐름이 변경되지만 JMP 명령어와 다른 점은 되돌아올 리턴 어드레스(CALL 다음 명령)를 스택에 저장한다는 것이다. 되돌아올 주소를 저장하기 때문에 함수 호출 후 원래 위치로 실행 흐름을 되돌릴 수 있다. 호출한 함수가 일을 다 마치면 원래 위치에서 다시 프로그램이 실행될 수 있음을 의미한다.
CALL nearlabel CALL farlabel
CALL mem16 CALL mem32 CALL reg
(디버깅을 하면서 보게 되는 Call 형식들)
Call 함수 주소
Call DWORD PTR[EAX+5]
Call <JMP to API> 특정 api 지목
□ CMP(Compare)
두 피연산자를 비교하는 작업을 한다. Destination 피연산자에서 Source 피연산자를 묵시적으로 빼서 값을 비교한다. 두 피연산자의 값이 같다면 결과는 0이 되고 제로 플래그(ZF)가 1로 세트 된다. 다르다면 제로 플래그(ZF)는 0으로 세트 된다.
CMP reg, reg CMP reg, imm
CMP mem, reg CMP mem, imm
CMP reg, mem
□ NOP(No Operation)
아무 일도 하지 않는 명령어이다. 리버싱 작업에서 목적에 따라 유용하게 사용될 수 있다.
NOP
조건 점프 명령
조건 점프 명령은 JMP 명령어와는 다르게 CMP 명령 같이 특정 플래그 레지스터를 변경시킬 수 있는 명령어를 통해서 특정 조건이 만족하게 된다면 점프를 수행하게 되는 명령어이다. 명령어 자체가 처음 볼 땐 어떤 일을 하는지 이해하기가 힘들게 되어 있다. 다음에 여러 조건 형식의 점프 명령과 명령의 의미, 그리고 어떤 조건(플래그 레지스터의 상태)을 만족해야 명령어가 수행되는지 표로 정리하였다.
명령어 | 명령어의 의미 | 명령어가 수행되기 의한 플래그 레지스터와 범용 레지스터의 상태 |
JA | Jump if (unsigned) above | CF=0 and ZF=0 |
JAE | Jump if (unsigned) above or equal | CF=0 |
JB | Jump if (unsigned) below | CF=1 |
JBE | Jump if (unsigned) below or equal | CF=1 or ZF=1 |
JC | Jump if carry flag set | CF=1 |
JCXZ | Jump if CX is 0 | CX=0 |
JE | Jump if equal | ZF=1 |
JECXZ | Jump if ECX is 0 | ECX=0 |
JG | Jump if (signed) greater | ZF=0 and SF=0 |
JGE | Jump if (signed) greater or equal | SF=OF |
JL | Jump if (signed) less | SF!=OF |
JLE | Jump if (signed) less or equal | ZF=1 and SF!=OF |
JNA | Jump if (unsigned) not above | CF=1 or ZF=1 |
JNAE | Jump if (unsigned) not above or equal | CF=1 |
JNB | Jump if (unsigned) not below | CF=0 |
JNBE | Jump if (unsigned) not below or equal | CF=0 and ZF=0 |
JNC | Jump if carry flag not set | CF=0 |
JNE | Jump if not equal | ZF=0 |
JNG | Jump if (signed) not greater | ZF=1 or SF!=OF |
JNGE | Jump if (signed) not greater or equal | SF!=OF |
JNL | Jump if (signed) not less | SF=OF |
JNLE | Jump if (signed) not less or equal | ZF=0 and SF=OF |
JNO | Jump if overflow flag not set | OF=0 |
JNP | Jump if parity flag not set | PF=0 |
JNS | Jump if sign flag not set | SF=0 |
JNZ | Jump if not zero | ZF=0 |
JO | Jump if overflow flag is set | OF=1 |
JP | Jump if parity flag set | PF=1 |
JPE | Jump if parity is equal | PF=1 |
JPO | Jump if parity is odd | PF=0 |
JS | Jump if sign flag is set | SF=1 |
JZ | Jump if zero flag is set | ZF=1 |
어셈블리 명령어에 대한 이해를 돕기 위해서 상황별로 어떻게 사용되는지 정리하도록 하자.
데이터 이동
어셈블리에서 데이터를 옮기는 방법은 MOV 명령어를 사용하면 된다.
MOV [복사될 곳], [읽어들일 곳]
MOV EAX, EBX의 경우 EBX 레지스터를 EAX에 대입하는 것이다.
MOV EAX, [EBX]로 표현되어 있는 경우에는 EBX가 가리키는 값을 EAX에 대입한다.
MOV EAX, [EBP + 10]의 경우 EBP의 주소에서 10만큼 증가한 주소지의 값을 EAX로 대입한다.
MOV EAX, [EBP – 10]의 경우 EBP의 주소에서 10만큼 감소한 주소지의 값을 EAX로 대입한다.
[EBP + 10]과 [EBP – 10]의 경우에 함수 내부에 존재하는 명령이었다면 EBP를 기준으로 10 증가된 경우에는 파라미터로 넘어오는 값일 수 있고, EBP를 기준으로 10 감소한 경우에는 함수 내부에서 쓰이는 지역변수 일 수 있다.
LEA EAX, [EBP + 10] 의 경우에는 EBP 주소에서 10만큼의 주소지를 더한 값을 의미하는 것이 아니라 EBP의 주소지 값에서 10을 더한 값을 EAX로 대입하게 된다.
전역변수
전역변수의 경우 data 섹션에 저장이 되고, 프로그램을 초기화하는 과정에서 세팅되거나 실행 중에 변경이 될 수도 있다. 예를 들어 문자열이 참조되는 방법을 보도록 하자.
.data:0088A1A1 ‘This Program is powerful’, 0
data 섹션의 문자열이 이렇게 존재하면, 어떻게 사용하는지 확인하자.
.text:006A2A22 mov eax, 88A1A1h
.text:006A2A27 retn
비교구문 cmp와 test
cmp 명령어는 주어지는 두 값을 뺄셈을 해서 처리하고, test 명령어는 주어지는 두 값을 더해서 처리하는 점이 다르며, 두 명령어가 가진 조건 분기문을 결정하기 위해서 사용된다는 점은 같다.
cmp eax, ebx는 두 값을 빼서 0이면 참(같은 값)이 되는 형태이다
test 연산은 보통 호출된 함수들이 일반적으로 리턴 값을 EAX 레지스터에 저장하는 것을 이용하여, 다음과 같이 사용된다.
CALL my_function
TEST EAX, EAX
JZ 주소지
함수를 call하고, 함수의 리턴 값으로 EAX 값이 세팅되면, 비교를 하는 것이다.
CMP가 영향을 미치는 FLAG들은 ZF, OF, SF, CF이고, TEST가 영향을 미치는 FLAG는 SF, ZF, PF가 있다. 만약 ZF가 1로 세팅되었다면 주어진 두 값이 같았다는 것을 의미한다. 그 외에 같지 않았을 경우에는 OF, SF, CF를 이용해서 어느 쪽이 더 큰지를 알 수 있다.
분기문의 사용
분기문은 일반적으로 점프 구문을 이야기하며, if문을 어떻게 사용하는지에 따라서 달라지는 어셈블리 코드들을 살펴보겠다.
□ C언어 소스
if (변수 == 0) {
if문 내의 함수 호출
} 이후 동작
□ 어셈블리어 변환
mov eax, 변수값
test eax, eax
jnz 이후 동작
if문 내의 함수 호출
if문에서 사용되는 변수를 먼저 mov로 값을 대입하고, test로 비교를 한 다음에 JNZ를 이용하여 이후 동작을 할지 if문 내부의 함수를 호출할지를 결정한다.
JZ가 아닌, JNZ를 사용하여 반대의 조건을 뜻하는 N(not)이 추가되어서 처리되는 것을 알 수 있다. 이외에 if문은 if 다음에 else 구문이 여러 번 더 나타날 수 있다는 점을 고려해서 변형된 코드들도 비슷한 방식으로 분석하면 된다.
반복문의 사용
반복문의 경우에는 같은 내용이 반복되어 처리되다가 반복문을 종료하는 비교문을 만나서 점프하여 반복문을 벗어나도록 구현되어 있다.
loop:
mov al, [ecx]
mov [edx], al
inc ecx
inc edx
cmp al, ‘p’
jnz short loop
리버싱 분석 중에 이와 같은 코드를 만나면 inc와 cmp, jnz를 보고 대략 파악이 되고, jnz에서 다시 위쪽의 loop 레이블로 가게 되어있는 것이 정확히 반복문이라는 것을 이해하게 해준다.
ECX와 EDX는 메모리상의 문자열을 지정하는 포인터이고, ECX가 가리키는 문자열에서 EDX가 가리키는 문자열로 복사가 일어나고 있다는 것을 보면 알 수 있다. 반복을 할 때마다 포인터가 둘 다 1씩 증가되고, ‘p’ 문자열이 나타날 때까지 복사를 하는 것으로 생각할 수 있다.
함수의 사용
함수는 시작하는 부분과 끝나는 부분을 파악할 수 있는 구분자가 있어서 확인이 그리 어렵지 않다.
push ebp
mov ebp, esp
sub esp, 20
EBP 값은 스택에 넣어두고, 현재 ESP 값을 EBP에 대입하고, 지역변수를 만들 공간을 확보하기 위해서 ESP 값을 변경한다.
그리고 끝나는 부분은 호출이 끝나서 리턴되기 때문에 RET가 쓰이게 된다.
mov eax, -1
mov esp, ebp
pop ebp
ret
이와 같이 ESP는 원래 값으로 돌아가고, EBP는 저장되었던 값으로 복구된다. 그런 후에 RET를 이용하여 함수를 CALL했던 부분으로 다시 돌아간다. 그리고 eax에 -1을 넣어서 결과가 -1을 표현해주기도 한다.
'Reversing > ▷ Study' 카테고리의 다른 글
프로그램 실행구조 (0) | 2017.03.15 |
---|---|
어셈블리와 C언어의 포인터 구문 형식 (0) | 2017.03.08 |
Stack Frame(스택 프레임) (0) | 2017.03.08 |
스택(Stack) 요약 (0) | 2017.03.08 |
General Purpose Register (범용 레지스터) (0) | 2017.03.08 |