Reverse Engineering/Assembly Language

어셈블리 언어 기본

manchesterandthecity 2021. 6. 8. 22:12

어셈블리 언어 기본

 

 

레지스터

 

< Basic program execution register >

 

 

 

< General-Purpose Registers >

 

 

EAX : Accumulator for operands and result data)

EBX : Pointer to data in the DS Segment)

ECX : Counter for string and loop operations)

EDX : I/O pointer

 

 위의 4개의 레지스터들은 주로 산술연산(ADD, SUB, XOR, OR 등) 명령어에서 상수/변수 값의 저장 용도로 많이 사용된다. 어떤 어셈블리 명령어(MUL, DIV, LODS 등)들은 특정 레지스터를 직접 조작하기도 한다(이런 명령어가 실행된 이후에 특정 레지스터들의 값이 변경됨).

 그리고 추가적으로 ECX와 EAX는 특수한 용도로도 사용되는데, 

 ECX는 반복문 명령어(LOOP)에서 반복 카운트로 사용된다(루프를 돌 대마다 ECX를 1씩 감소시킴)

 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을 빼면, .... 1111 1111 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을 더하게 되면, 1 0011 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 명령어보다 더 쉽고 빠르다).

 

 

 

XOR EAX, EAX  => EAX를 0으로 만듬.

 

절대값 주소로 점프하는 방법.1

PUSH 401000

RETN

 

절대값 주소로 점프하는 방법.2

MOV EAX, 401000

JMP EAX

 

 



참고 : 

https://dopamine-plaza.tistory.com/34 [Dopamine Plaza]

 

https://gutte.tistory.com/24 [Tuuna Computer Science]