본문 바로가기

리버싱/x64dbg 디버거를 활용한 리버싱과 시스템 해킹의 원리

ch04 - 어셈블리어의 이해

728x90

1. 어셈블리어 명령어

어셈블리어에선 16진수를 쓰며 10진수와 비교하면 다음과 같다.

10진수 - 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14  15 16 17   18 19 20 21 22  23  24  25   26  27  28 29 30  31  32...

16진수 - 0 1 2 3 4 5 6 7 8 9  A  B  C   D  E    F   10  11  12 13 14 15 16  17  18  19  1a  1b  1c  1d 1e  1f  20...

 

 - 인텔 기반 표기 방법(윈도우)과 AT&T 기반 표기 방법(리눅스)이 있다. 

 

 

 1.1 데이터 이동 명령어

 - 데이터 이동 방법의 5가지(x86에서는 다섯 가지 모두 사용 가능하지만, ARM에서는 메모리에서 메모리로 직접 이동하는 것은 불가능)

1) 값을 직접 레지스터로 대입하기

2) 레지스터에서 레지스터로 옮기기

3) 값을 직접 메모리로 대입하기

4) 레지스터에서 메모리로 또는 그 반대로 옮기기

5) 메모리에서 메모리로 옮기기

 

#mov, dst, src

  • mov eax, ebx - EBX 레지스터 내의 값을 EAX 레지스터로 복사
  • mov eax, 0x42 - 값 0x42를 EAX 레지스터로 복사
  • mov eax, [0x4037 c4] - 메모리 주소 0x4037c4에 있는 4바이트 값을 EAX레지스터로 복사
  • mov eax, [ebx] - EBX 레지스터가 명시한 메모리 주소의 4바이트 값을 EAX 레지스터로 복사
  • mov eax, [ebx+esi*4] - EBX+ESI*4 연산 결과가 명시한 메모리 주소의 4바이트 값을 EAX 레지스터로 복사

 

 - mov는 목적지로 값을 넣을 때 사용하는데 값은 소스(src) -> 목적지(dst)로 이동한다.

소스 부분에 큰 괄호([])로 묶인 부분은 메모리 주소가 가리키는 값을 뜻한다.

ex) [ebx]는 ebx에 주소가 저장되어 있고 그 주소에 있는 값을 나타내고 소스 부분에는 레지스터나 갑이 올 수 있지만, 계산을 필요로 하는 수식을 사용할 수는 없다.

 

#lea dst, src

lea(Load Effective Address) : 메모리 주소를 목적지에 넣을 때 사용하며 주소를 저장하는 데 사용한다.

  • lea eax, [ebx+8] - EBX+8을 EAX에 저장
  • mov eax, [ebx+8] - EBX+8 주소에 저장된 값을 EAX에 복사

 - mov와 lea의  c언어에서의 차이

어셈블리어       / c언어 코드

//esi는 sample의 주소가 있음                  struct_test_t *c;

//b배열의 주소는 [ebp-38]이다                int *a, b [10];

 

1 lea eax, dword ptr:[ebp-38]               a = &b [0];

2 mov dword prt:[ebp-c], eax                    //dword는 4바이트 크기를 의미

3 mov dword prt:[ebp-34], 3                     b [1] = 3;

4 mov eax, dword prt:[ebp-c]                    //ptr:[ebp-38] 값이 계속 이동하면서 b [0]가 eax로 이동

5 mov dword ptr:[eax], 2                           *a = 2; {4,5 줄}

6 lea eax, dword prt:[ebp-38]                  ebp-38에 값인 2가 들어간다

 

7 add eax, 8                                           ebp-38이였지만  +8을 해서 ebp-30으로 b [2]가 됐다.

8 mov dword ptr:[ebp-c], eax                     a = &b[2]; {6,7,8줄}

9 mov edx, dword ptr:[ebp-c]                      //ptr:[ebp-c]의 값은 b [2]로 edx가 b [2]를 가리킨다

10 mov dword ptr:[edx], 5                          *a = 5; //a(edx)가 가리키는 곳에 5를 넣어라

11 mov dword ptr:[ebp-10], esi                    c = *sample; esi 값을 ebp-10에 이동(구조체)

12 mov eax, dword ptr:[ebp-10]                   

13 mov dword ptr:[eax], dword ptr:[ebp-38]    c->i = b [0]; //ebp-38의 값2를 eax가 가리키는 값(esi 1번째)에 넣어라

 

mov edx, dword ptr:[ebp-c]                          a*의 값을 edx에 넣기

mov eax, dword ptr:[ebp-10]                        2의 값을 eax에 넣기

mov dword ptr:[eax+8], dword ptr:[edx]       c->j = *a; //eax+8은 j의 위치 b[5]를 esi [2]로 이동

mov eax, dword ptr:[ebp-10]

mov byte ptr:[eax+4], 41                          c->ch = 'A'; 41의 16진수 65의 문자값 'A'를 ch로 이동

 

 

#push와 pup 명령어

 : 스택에 데이터를 넣거나 꺼내는 명령어로 push는 오퍼랜드 값을 스택에 넣고 pop은 스택에서 값을 가져와 레지스터로 저장한다. 이 명령어는 스택을 사용하기 때문에 스택 포인터(ESP) 값이 변경된다.

  • pop eax - 스택에서 값을 꺼내 eax에 저장함(ESP가 4만큼 증가함)
  • mov eax, [esp] - 스택에서 값을 읽어와 eax에 저장함(ESP가 변하지 않음)

 - 레지스터 값을 스택에 일괄 저장하기 위한 명령어로 작업 중인 내용을 일시에 저장하고 다른 작업을 수행하기 위한 것

 

  1.2 산술 연산 명령어

 - 사칙연산, 증감 연산, 음수 연산 등..

 

  1.3 비트 연산 명령어

 - 비트 연산자는  비트단위로 연산을 수행하고 논리곱, 논리합, 배타적 논리합, 비트 반전 등이 연산이 있다.

 

  1.4 제어 명령어

 - 두 개의 값을 비교하고 그 결과에 따라 다른 루틴을 수행하게 하는 cmp나 test 명령어가 있다. cmp의 경우 오퍼랜드의 종류와 크기에 따라 cmps, cmppd, cmpsd 등이 있다.

 

  1.5 분기 명령어

 : 프로그램의 흐름 전환을 결정하는 분기 명령어로 jmp가 있다.

  • jmp address - 무조건 해당 주소로 옮겨감(EIP)
  • jcc address - cc 조건에 따라 해당 주소로 옮겨감(EIP)

 NE - Not Equal /NZ - Not zero / L - less / NGE - not great equal 크거나 작다 / NLE - Not Less Equal 작거나 같다 / B - Beyond / A - Above / P - Parity / O - overflow /ECXZ - ecx가 0이면 플래그 값이 0

 

 

  1.6 루프 명령어

#REP 명령어

 - ECX 레지스터에 지정된 횟수만큼 또는 ZF 플래그의 지정된 조건이 더 이상 충족되지 않을 때까지 문자열 명령어를 반복하며 REP 명령어는 INS, OUTS, MOVS, LODS 및 STOS 명령어에 추가할 수 있고, REPE, REPNE, REPZ 및 REPNZ 명령어는 CMPS 및 SCAS 명령어에 추가할 수 있다.

 -> 데이터 버퍼에 일정 연산을 수행하는 연산을 위해 ESI, EDI, EAX, DX 레지스터가 이용된다. n(8비트) , w(16비트) , d(32비트) , q(64비트)

 

#roop 명령어

 : 관계된 위치까지를 ECX 또는 RCX 레지스터 크기만큼 반복 수행한다.

 

  1.7 다형성 코드

 (polymorphism) : 같은 의미를 갖는 여러 가지 코드

 - 컴파일하게 되면 코드 최적화 과정을 거치면서 적절한 어셈블리어로 변환하게 된다. 이 과정에서 큰 이동(far jump)은 목적지 주소를 그래도 사용하지만 작은 이동(near jump)은 목적지 주소와 현 위치의 차이 값만 표현하여 명령어 코드 길이를 줄여 주소 오퍼랜드 크기가 달라진다.

 

2. 프로그램 코드의 어셈블리어 표현

 2.1 어셈블리어 코드의 이해

  2.1.1 변수 값 설정

 - 변수에 값을 할당하는 코드를 확인하기 위해 아래와 같은 예제 프로그램을 컴파일하여 사용

#include <stdio.h>

int main(){

int a, b, c;

a=1, b=1;

printf("before if statement\n");

if(a> b)  c = 1;

else  c=0;

printf("after if statement\n");

return 0;

}

-> printf를 기준으로 조건 a> b면 c=1/아니면 c=0이다.

 

 -> 문자열 검색으로 'before if statement'을 찾아가 보면 변수의 값은 mov 명령어에 의해 설정된다. 지역변수와 전역 변수에 따라 오퍼랜드 주소의 위치가 달라지는데 지역변수는 "ss : [주소]"로 표현하고 전역 변수는 "ds : [주소]"로 표현

 

 - 위에서 esp+1C는 1, esp+18은 2의 값이 들어가서 이 둘을 비교하게 된다.

Jle는 jmp 분기 명령어로 (b가) 작거나 같으면 점선으로 가라는 명령어(c=0) /(b) 크면 바로 다음 명령을 실행(c=1) [위의 c코드에서 if명령어에 해당]

 

 

 -> 해당 문자열을 덤프하기에서 주소를 찾아가 보면 00(아스키코드값 0 [null])으로 문자가 구분

 

여기에 "mov dword ptr ss:[esp+1c], 1"  ---> c 언어에서는 "a = 1"

ss는 스택 세그먼트이고, 'esp+1c'는 ESP 레지스터에서 0x1c만큼 떨어진 곳에 저장된 변수임을 나타낸다.

 

#x32dbg에는 Snowman 디컴파일러를 지원하는데 어셈블리어를 C언어로 다시 바꿔주는 기능이다.

 온전히 디컴파일러되지 않을 수 있다.

먼저,  해당 코드를 snowman파일로 만들고 xdbg폴더의 plugin폴더에 넣는다.(프로그램을 실행한 적이 없으면 plugin 폴더가 없을 수 있다)

xdbg를 실행하고 해다 코드를 실행한 상태에서 아래와 같이 Snowman이 뜬다.

 

#xdbg에서 g를 누르면 flow차트처럼 시각화해서 보여준다.(돌아가려면 다시 g)

 

#오른쪽에 플래그를 보여주며 변하는 값은 빨간색으로 변한다.

 

  2.1.2 조건문의 사용

 - 조건문이 쓰인 곳을 보기 위해 위 사지에 "before if statement"을 보면 'cmp'와 'jmp'가 사용되고 조건부 분기로 점선 화살표, 무조건 분기는 실선 화살표로 표현하고 있다.

조건부 분기 jle는 '같거나 작으면 이동'을 뜻한다. 

 -> EFLAGS 레지스터의 ZF(제로 플래그)나 SF(부호 플래그)가 1일 때 분기이다.

 

  2.1.3 다중 조건문(switch)의 사용

#include <stdio.h>

int main() {

int  n;

printf("Input number");

scanf("%d", &n);

printf("before switch statement\n");

switch(n){

 case 0: printf("[zero]"); break;

 case 1: printf("[one]"); break;

 case 2: printf("[two]"); break;

 case 3: printf("[three]"); break;

 case 4: printf("[four]"); break;

 default: printf("[error]"); break;

}

printf("after switch statement\n");

return 0;

}

 

- printf마다 코드의 움직임을 보면 scanf 이전에 [40400E]를 덤프로 따라가면 %d이다.

 

 - scanf를 따라가 보면 scanf줄에서 플래그 TF가 1로 바뀌고 다음으로 넘어간다.

 TF는 싱글 스텝으로 명령어 하나를 실행하고 정지한다. 이 때 레지스터와 메모리 위치의 내용은 검사되는데 올바르다면 시스템은 다음 명령어를 계속 실행할 수 있다.

 

- 다음 부분은 switch문으로 case로 들어가기 직전의 어셈블리어로 어느 case 문으로 들어갈지 위치를 찾는 부분이다.

 

  2.1.4 루프의 사용, 함수의 사용

#include <stdio.h>

 

int func(int x, int y, int z){

int result;

result = x+y+z;

return result;

}

int main(){

int i,n,sum,value;

printf("step #1 \n");

sum = 0;

for(i=1;i<=10;i++){

 sum += i;

 }

printf("step #2\n");

value = func(1,2,3);

return 0;

}

 

 - 디스컴파일러 해보면 다음과 같다. [f2]로 중단점을 설정하고 [f8]로 한줄씩 보면서 [f9]로 중단점까지 실행시키면서 반복문을 편하게 볼 수 있다.

모듈 분석[ctrl+A]을 누르면 함수를 묶어서 보여준다.($가 맨앞에 표시)

 

함수 구조는 시작할때 수행되는 프롤로그와 함수가 끝날 때 수행되는 에필로그가 있다.

 - 함수 호출 다음 문장(주소 0x40159E)에는 EAX 레지스터 값을 변수 value(esp+44)에 저장하는 코드가 있다. 결과 값은 EAX 레지스터를 통해 전달된다.

 

#함수 호출(순서)

1. push 명령어를 이용해 함수 매개변수(argument)를 스택에 저장함

2. 현재 명령어 주소(EIP)를 스택에 저장하고 함수의 시작 주소로 EIP를 설정함

3. 함수 프롤로그를 수행함[EBP를 스택에 저장하고 현재 ESP로 EBP를 설정함, 함수의 지역변수용 스택 공간을 확보]

4. 함수의 기능을 수행함

5. 함수의 에필로그(epilog)를 수행함[함수 지역변수 공간을 해제하고 저장된 EBP를 복원, Leave 명령어는 ESP를 EBP로 옮기고 EBP를 스택에서 꺼냄]

6. 함수 매개변수를 다시 사용하지 않을 경우 스택에서 제거