보안/리버스 엔지니어링

리버스 엔지니어링을 위한 어셈블리 기초

Seonyoung Jeong 2022. 9. 27. 20:31

반 이상 써놨던걸 날려먹고 다시 쓴다^^

참고로 내가 정리할 글들은 누군가가 보기 위함보다는 내 스스로 기초를 다지려는 목적이므로 정갈한 글은 아니다.

혹시나 읽게 되시는 분들은 이 점 참고하시길 바랍니다:)


리버스 엔지니어링이란?

소스코드를 역추적하는 것.

EXE, DLL 등의 바이너리를 분석하여 원래의 소스코드에 어떤 라이브러리가 링크되었는지, 흐름이 어떤지 등을 파악하는 행위.

 

리버스 엔지니어링을 위한 기초 어셈블리

어셈블리어는 명령어+인자 형태를 기본으로 갖추고 있다.

push 337이나 mov eax, 1 이런 형식으로 작성한다.

이 때 명령어 부분을 opcode, 인자 부분을 operand라고 하는데 operand는 웬만하면 1개 아니면 2개다.

mov eax, 1처럼 operand가 2개일 경우 앞의 것을 destination, 뒤의 것을 source로 본다.

그래서 mov eax, 1을 해석해보자면 1을 레지스터 eax에 옮기라는(넣으라는) 뜻이다.

 

Register

어셈블리어를 보면 eax, ecx이런걸 볼 수 있다. 이게 바로 레지스터다.

어렵게 레지스터의 정의까지 생각하지말고, 단순하게 CPU가 사용하는 변수라고 생각하자.

 

레지스터 설명
EAX Accumulator, 즉 산술 계산에 쓰이는 레지스터이며 리턴값을 전달하기도 함.
만약 100을 리턴하면서 함수를 끝내고자 할 때, EAX에 100을 넣어놓고 리턴한다는 뜻.
EDX EAX와 역할은 같지만 리턴값 전달은 안 함.
ECX Counting, 루프문 수행 중 카운팅하는 역할을 함.
코드상에서는 i=0부터 100까지 돌아가지만 어셈블리에서는 ECX에 미리 100을 넣어놓고 0될때까지 카운트함.
카운팅할 일이 없을 때는 그냥 변수로 사용함.
EBX 레지스터 여유분. 위의 세개로 부족하면 얘도 씀
ESI & EDI 각각 Source Index, Destination Index의 약자로, 말그대로 시작지와 목적지를 가리킴.
속을 들여다보면 ESI는 문자열이나 각종 반복 데이터를 처리 또는 메모리를 옮김.
예를 들어 memcpy를 쓸 때는 ESI에서 메모리를 읽어 EDI로 복사함. strcpy에서는 ESI에서 문자열을 읽어 EDI로 복사함. (근데 사실 복사할 메모리 크기가 엄청 큰거 아니고서는 얘네 잘 안씀)
EBP Base Pointer, 스택의 베이스 주소를 가리키는 레지스터. 베이스를 가리키는 만큼 고정값임.
ESP Stack Pointer, 스택의 크기를 조정할 때 사용됨. 자세한건 뒤에 더 공부하면서 알 수 있음

위의 8개의 레지스터는 32bit인데, 이 외에 16비트의 레지스터가 있다.

EAX 32bit 중 뒤쪽 16bit를 AX레지스터라고 부르며, 그 중에서도 앞쪽 8bit는 AH, 뒤쪽 8bit는 AL이라고 불린다.

이런 형식은 EAX, EDX, ECX, EBX에 모두 있는데, 각 레지스터의 중간 알파벳을 따오면 된다.

EBX는 BX레지스터, BX레지스터 안에 BH, BL 이런 식으로!

 

Opcode

opcode는 아주 직관적이라서 딱히 외울것도 없다.

Opcode 설명
PUSH, POP 말그대로 스택에 PUSH, 스택에서 POP.
MOV 아까 설명했던대로 뒷 operand에서 앞 operand로 옮기기
LEA 주소 가져오기.

다음과 같이 가정해보자.
esi : 0x401000(esi에 0x401000이라는 값이 들어있음.)
*esi : 5640EC83 (esi가 가리키는 주소에 5640EC83이라는 값이 들어 있음)


다음의 두줄의 어셈코드를 같이 보면 이해가 됨.
1. lea eax, dword ptr ds:[esi]  => esi에 있는 주소값 0x401000을 eax에 넣음.
2. mov eax, dword ptr ds[esi] => esi에 있는 주소 0x401000에 있는 5640EC83이라는 값을 eax에 넣음
ADD 더하기!
SUB 빼기!
INT 인터럽트를 일으키는 명령어. 리버싱에서는 INT3을 많이 보게 될 것. 뒤에서 더 자세히 보자.
CALL 함수 호출 명령어. CALL로 호출된 함수 내에는 RET가 반드시 있음.
INC, DEC 증감 연산
AND, OR, XOR AND, OR, XOR 연산
NOP 아무것도 안함. 해킹이나 리버스 엔지니어링에서 가장 많이 쓰이는 명령어임.
CMP, JMP Compare, Jump

 

엔디언

바이트 저장 순서를 엔디언이라고 부르며, 리틀 엔디언과 빅 엔디언이 있다.

12345678이라는 DWORD 값이 있을 때, 빅엔디언 방식으로는 0x12345678, 리틀 엔디언 방식으로는 0x78563412로 표기한다. 리틀엔디언이 바로 인텔 CPU에서 채택한 방식이다.

 

스택 구조

일단 스택은 모두 거꾸로라는걸 알기.

아래에서 위로 접근하려고 할때는 빼기!! 위에서 아래로 접근하려고 할 때는 더하기!!!!!!!

 

push ebp
mov ebp, esp
sub esp, 50h

세줄의 어셈코드가 실행되는걸 한줄한줄 살펴보자.

 

1. push ebp

먼저 다음과 같이 ebp를 스택에 넣어준다. 즉, 베이스 주소를 스택에 보관한다.

2. mov ebp, esp

esp 값을 ebp에 넣어준다. 즉, 현재의 스택 포인터 값을 베이스 포인터로 변경하여 이걸 이용해 스택에서 움직인다.

 

3. sub esp, 50h

아까 말했다시피 스택은 거꾸로잖아? 저렇게 뺄셈을 해줌으로써 사용할 공간을 사용하겠다는 거.

다만 저렇게 아래로 공간을 확보하는건 지역변수를 저만큼 사용하겠다는 뜻! 

이 세가지 문장은 함수의 앞쪽에 무조건 나오는 구조이므로 알아두기!!!!!!

 

함수 파라미터를 스택에 쌓는 법

 아까 말했다시피 스택은 다 거꾸로다.

DWORD dwRET=HelloFunction(0x37, 0x38, 0x39);

위의 코드를 실행하면

push 39h
push 38h
push 37h
call 401300h

이렇게 인자가 거꾸로 들어가는걸 확인할 수 있다.

그럼 스택에는

이렇게 거꾸로 쌓인다. 401300h는 함수 종료 후 리턴할 위치, 그 아래로는 첫번째 인자, 두번째 인자.... 다.

EBP는 저 위쪽을 가리키고 있고, 필요한 값들은 아래에 있다. 스택은 거꾸로니까 EBP에서 더해가며 움직인다.

리턴주소는 4바이트, 인자들도 DWORD로 4바이트이므로 리턴주소는 EBP+4, EBP+8, ... 이렇게 더한다.