티스토리 뷰

리버싱

리버싱 엔지니어링 바이블 - 0307

삼전동해커 2022. 3. 7. 20:42

ㅋ책을 보고 공부한 내용을 정리함.

책의 내용들은 IA-32를 기본 플랫폼으로 하고 있다.

 

01. 리버스 엔지니어링 기본

 

01.리버스 엔지니어링만을 위한 어셈블리

어셈블리 기본 구조

 

먼저 아래 psudo code를 예시의 어셈블리어로 바꿔본다.

void 물마심(){
	BOOL bOpen = 냉장고 오픈();
    if(bOpen){
    	물을 꺼냄();
        물을 마심();
    }
}
__asm{
	냉장고 앞으로 간다
    낭장고 문을 잡는다
    냉장고 문을 연다
  오픈 성공:
  	냉장고 안을 본다
    손을 든다
    냉장고 안에 넣는다
    물병을 잡는다
    물병을 꺼낸다
  뚜껑을 연다
  컵을 잡는다.
  물을 따른다.
  컵에 든 물을 마신다.

위에서 보듯이 어셈블리어는 한번에 하나의 동작만을 수행할 수 있다. 이처럼 어셈블리어는 코드를 한두줄만 봐선 어떤 동작을 수행하는지 파악하기가 어렵다. 이렇게 자세히 구분해 놓은 어셈을 읽는 방법은 뒤에서 나온다고 한다.

 

 

어셈블리의 명령 포멧

어셈블리의 명령어 형태는 "명령어(Opcode)+인자(operand)"로 구성되어 있다. 

push 337

이라는 어셈블리는 push라는 명령어와 337이라는 인자로 구성되어 있다.

 

mov eax, 1

이는 오퍼랜드가 2개인 경우이다.

eax에 1을 넣으라는 명령으로 앞의 오퍼랜드가 목적지 오퍼랜드, 뒤의 오퍼랜드가 출발지 오퍼랜드가 된다.

 

레지스터, 복잡한 설명은 그만

레지스터를 어렵게 생각하는 경우가 더러 있는데, 사실 레지스터를 간단히 목적이 분명한 변수라고 생각하면 쉽다.

완벽하게 변수와 일치하는 개념은 아니지만, 그저 CPU가 사용하고 메모리의 힘을 빌려서 연산을 시작하는 것이라고 생각하자.

레지스터는 총 8개이다. EAX, EDX, ECX, EBX, ESI, EDI, EBP, ESP.  32비트 플랫폼을 사용하기 때문에 이 레지스터들의 크기는 모두 32비트이다. 64비트의 플랫폼을 사용하는 경우에는 RAX, RDX, RCX, RBX 등을 사용하면 레지스터의 크기는 64비트이다.

 

EAX

EAX 레지스터의 역할은 산술 계산을 하며, 리턴 값을 전달한다. 덧셈, 뺄셈, 곱셈, 나눗셈 등에서 사용되는 값들을 저장하기 위한 변수로 사용되거나, 함수의 진행이 끝난 후의 결과 값인 return 값을 저장하기 위해 사용되는 레지스터라고 생각하면 된다. A는 Accumulator의 약자이다.

 

EDX

EDX는 EAX와 역할은 비슷하지만 리턴 값의 저장 용도로 사용되지 않는다. 그저 산술연산의 값들을 저장하기 위한 레지스터이다. D는 DATA의 약자이다.

 

ECX

먼저 C는 COUNT의 약자이다. 반복문을 생각해보면 반복이 몇번 돌았는지를 기록하는 변수가 있다. 이 반복될 값을 저장해두는 레지스터이다.

 

EBX

이 EBX는 그냥 EAX,EDX,ECX만으로 메모리 사용이 버거울 때나 하나 더 필요할 것 같을 때 사용하는 백업용도의 레지스터이다.

 

ESI, EDI

위 4개의 레지스터는 주로 산술연산에 사용되는 레지스터 였지만 ESI,EDI는 문자열이나 각종 반복 데이터를 처리 또는 메모리를 옮기는데 사용된다. ESI는 Source Index로 출발지 레지스터, EDI는 Destination Index로 목적지 레지스터라고 생각하면 된다.

 

경우에 따라 AL,AH 등을 사용하는 경우도 있는데 이는 AX(16비트 플랫폼 환경의 레지스터)을 절반으로 나눴을 때 앞부분을 AH 뒷부분을 AL라고 부른다. 

예를 들어 0xaabbccdd 중 ccdd는 AX이고 cc는 AH이고 dd는 AL이다.

 

다음과 같은 레지스터가 값들이 있다고 할 때 아래의 코드를 생각해보자.

첫번째 줄의 mov ah, byte ptr ds:[esi]는 esi 주소에 담긴 값(포인터니까)을 바이트 단위로 ah에 넣으라는 것이고,

두번째 줄은 esi 주소에 담긴 값을 al에 넣으라는 것이고, 같은 값을 ax에 넣으라는 의미이다.

세번째 줄은 ax에 워드단위(2바이트)로 esi의 값을 넣는다는 의미이다.

 

esi에는 현재 0x401020이 담겨있고 이 주소의 값은 0x5640EC83이다.(책에서 알려주는 내용임)

EAX에는 0x78563412가 들어 있다.

ah는 34이고 al은 12라는걸 기억하자.

 

첫번째 줄을 실행하면 esi의 83값을 ah에 넣어 0x78568312가 된다.

두번째 줄을 실행하면 esi의 83 값을 al에 넣어 0x78568383이 된다.

세번째 줄을 실행하면 esi의 EC83 값을 ax에 넣어 0x7856EC83이 된다.

 

따라서 EAX의 값은 0x7856EC83이 된다.

 

이번엔 EAX~EDX를 사용하는 과정을 살펴보자.

 

int Plus(int a, int b){
	return a+b;
}

위 코드는 a,b를 인자로 받아 합을 리턴해주는 코드이다.

이 코드를 디스어셈블해 보면 다음과 같다.

 

mov eax. dowrd ptr ss:[esp+8]
mov ecx, dword ptr ss:[esp+4]
add eax, ecx
ret

여기서 주의해야할 점은 매개변수 a는 esp+4에 담기고 b는 esp+8에 담긴다. 왜냐하면 함수를 호출하고 스택에 함수를 call을 push 매개변수들을 push한다. 이후 pop할 때는 거꾸로 나오기 때문이다.

각 값을 eax(=a)와 ecx(=b)에 넣는다. 

이 후 add eax,ecx를 실행한다. 그냥 안헷갈리게 add esp+8, esp+4라고 적으면 안되는가 생각이 들겠지만 메모리끼리는 연산을 할 수 없다. 따라서 각 값을 레지스터에 저장한 후 연산을 해야한다. add 연산의 경우 첫번째 오퍼랜드와 두번째 오퍼랜드를 더하는 연산을 한 후 첫번째 오퍼랜드에 결과값을 저장한다.

 

외울 필요가 없는 어셈블리 명령어

이번엔 opcode에 해당하는 명령어를 살펴보자. 

이런 opcode들을 당장에 외우기보다는 한두번 어셈블리어를 읽다보면 어떤 의미인지 알 수 있다.

 

push,pop

스택에 값을 넣는 것을 push, 값을 가져오는 것을 pop이라고 한다. 

 

mov

mov는 앞에서 설명했다시피 첫번째 오퍼랜드에 두번째 오퍼랜드를 옮기는 명령이다.

 

lea

lea는 mov와 굉장히 유사하지만 명백한 차이점이 하나 있다. mov가 값을 가져오는 명령어라면 lea는 주소를 가져오는 명령어이다.  따라서 lea를 사용하는 경우엔 오퍼랜드가 []로 싸여있다.

예를 들어

esi : 0x401000

*esi : 564083

 

위와 같은 레지스터 상태가 있을 때 다음 명령어의 값들을 살펴보자.

 

lea eax, dword ptr ds:[esi]

eax에 dword 단위로 esi의 주소(=0x401000)를 가져온다는 명령어.

 

mov eax, dword ptr ds:[esi]

eax에 esi주소의 값(564083)을 가져온다는 명령어.

 

INT

인터럽트의 줄임말이다. 뒤의 오퍼랜드의 숫자에 따라 다른 처리가 일어난다. 가장 많이 볼 것은 INT 3으로 DebugBreak()을 의미한다.

 

CALL

함수를 호출하는 명령어다. CALL 뒤의 오퍼랜드에 주소가 붙는다. 해당 주소를 호출해 함수의 작업이 끝나면 CALL 다음 명령어로 돌아온다. 

 

나머진 보면 다 앎.

 

리버싱 엔지니어링에 필요한 스택

스택은 LIFO(Last In First Out)구조인건 다들 알것이다. 먼저 책에서 배울건 다음 3가지이다.

1.함수 호출 시 파라미터가 들어가는 방향

2.리턴 주소

3.지역 변수 사용

 

함수 안에서 스택을 사용하게 되면 보통 다음과 같은 코드가 엔트리 포인트(함수 시작부분)에 생성된다.

push ebp
mov ebp esp
sub esp, 50h

 

해석을 해보면

먼저 ebp 레지스터를 스택에 쌓는다. 이 작업으로 이전에 진행하던 작업들과 앞으로 진행할 작업(함수)을 구분한다. 이전 함수로 다시 돌아가기 위한 RET를 쌓는 작업을 선행하고 본 함수의 ebp를 쌓는다는 점을 기억해야 한다.

 

다음은 mov ebp, esp인데 현재 esp 값을 ebp에 넣는다. 이제 ebp와 esp가 같아지면서 이 함수에서 지역변수는 얼마든지 계산할 수 있다. ebp를 기준으로 오프셋을 더하고 빼는 작업(ex : [ebp-8])으로 스택의 위치를 계산할 수 있다.

 

그리고 sub esp,50h는 esp에서 50h만큼 뺀다는 의미인데 스택은 LIFO 특성으로 높은 주소에서 낮은 주소로 자란다. 따라서 특정 값만큼 뺀다는 것은 그만큼 스택을 사용한다는 의미이다. 

 

 

함수의 호출

다음과 같이 함수를 호출하는 경우를 생각해보자.

DWORD HelloFunction(DWORD dwpa1, DWORD dwpa2, DWORD dwpa3){
...

}

int main(){
	DWORD dwret = HelloFunction(0x37,0x38,0x39);
    if(dwret)
    ...
}

함수 호출을 리버싱해보면 다음과 같다.

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

함수의 매개변수가 거꾸로 들어가는 이유도 LIFO 특성 때문이다. 

0x401300(HelloFunction)안으로 들어가서 생각해보자. 

HelloFunction 함수에 대한 push ebp mov ebp, esp 등을 실행해 ebp와 esp가 생성되었다.

이때 불러와야하는 매개변수들은 ebp를 기준으로 더 높은 위치에 존재한다. 따라서 거꾸로 넣긴 했지만 불러오는 순서는 EBP+0x8에 위치한 0x37, EBP+0xC에 위치한 0x38, EBP+0x10에 위치한 0x39 순서대로 불러오게 된다.

이때 EBP+0x4에는 main문으로의 return 주소값이 담겨있다.

 

 

'리버싱' 카테고리의 다른 글

리버스 엔지니어링 바이블-0320  (0) 2022.03.21
리버싱 엔지니어링 바이블-0311  (0) 2022.03.11
PinTool BBL 개수 출력  (0) 2022.02.15
Taint Analysis란  (0) 2022.02.14
PinTool Opcode binary 출력 구현  (0) 2022.02.14
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함