본문 바로가기

리버싱

[리버싱] 메모리 구조, 스택프레임, 함수 규약과 프롤로그&에필로그




프로세스



프로그램이 실행되는 동안을 프로세스라 하며 프로세스는 운영체제에 의해 메모리 영역을 할당 받아 실행된다. 프로그램이 운영체제로부터 할당받는 대표적인 메모리 공간은 다음과 같다.




메모리 구조









1. 코드 세그먼트 (Code Segment)


메모리의 코드 영역은 실행할 프로그램의 코드가 저장되는 영역으로 텍스트 영역이라고도 부른다. CPU는 코드 영역에 저장된 명령어를 하나씩 가져가서 처리하게 된다. 상수도 이곳에 포함된다.




2. 데이터 세그먼트 (Data Segment)


메모리의 데이터 영역은 프로그램의 전역 변수와 정적(static) 변수가 저장되는 영역이다. 데이터 영역은 프로그램의 시작과 함께 할당되며, 프로그램이 종료되면 소멸한다.




3. 힙 세그먼트 (Heap Segment)


메모리의 힙 영역은 사용자가 직접 관리할 수 있고 관리해야만 하는 영역이다. 힙 영역은 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다. 힙 영역은 메모리의 낮은 주소에서 높은 주소의 방향으로 할당된다.




4. 스택 세그먼트 (Stack Segment)


메모리의 스택 영역은 함수의 호출과 관계되는 지역 변수와 매개 변수가 저장되는 영역이다. 스택 영역은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸한다. 스택 영역은 PUSH 를 통해 데이터를 저장하고, POP 을 통해 데이터를 인출한다. 이러한 스택은 후입선출 (LIFO, Last-In First-Out) 방식에 따라 동작하므로, 가장 늦게 저장된 데이터가 가장 먼저 인출된다. 이렇게 스택 영역에 저장되는 함수의 호출 정보를 스택 프레임 (Stack Frame) 이라고 한다.







스택 프레임 (Stack Frame)



스택 프레임이란 함수가 호출되었을 때 그 함수가 가지는 공간 구조이다. 함수가 동작을 종료하고 복귀 주소로 돌아갈 때 스택 프레임은 소멸된다.


ESP 레지스터는 스택 포인터 역할을 하고, EBP 레지스터는 베이스 포인터 역할을 한다. ESP 레지스터의 값은 프로그램 안에서 수시로 변경되기 때문에 스택에 저장된 변수, 파라미터에 접근하고자 할 때 ESP 값을 기준으로 하면 프로그램을 만들기 어려워진다. 따라서 어떤 기준 시점 (함수 시작) 의 ESP 값을 EBP에 저장하고 이를 함수 내에서 유지해주면, ESP 값이 아무리 바뀌어도 EBP 를 기준으로 안전하게 해당 함수의 변수, 파라미터, 복귀 주소에 접근할 수 있게 된다.




스택 프레임의 구조



PUSH EBP                     :  함수 시작 (EBP 사용하기 전에 초기 값을 스택에 저장)


MOV EBP, ESP               :  현재의 ESP를 EBP에 저장



···                               :  새로운 함수의 내용



MOV ESP, EBP               :  ESP를 함수 시작했을 때의 값으로 복원


POP EBP                      :  리턴되기 전에  저장해놨던 원래 EBP 값으로 복원


RETN                          :  함수 종료







함수의 호출




1. 함수가 사용할 파라미터를 스택에 넣고 함수 시작지점으로 점프 (함수 호출) 한다


2. 함수 내에서 사용할 스택 프레임을 설정한다 (프롤로그)


3. 함수의 내용을 수행한다


4. 수행을 마치고 처음 호출한 지점으로 돌아가기 위해 스택을 복원한다 (에필로그)


5. 호출한 지점의 다음 라인으로 점프한다




2번 과정을 프롤로그 (Prolog) 라고 부르며, 4번 과정을 에필로그 (Epilog) 라고 부른다. 이러한 프롤로그와 에필로그는 함수 호출 규약에 따라 조금씩 달라진다. 







함수 호출 규약



함수 호출 규약이란 함수가 호출될 때 인자의 전달방법(스택 or 레지스터), 전달 순서(오른쪽 -> 왼쪽 or 왼쪽 -> 오른쪽), 인자 전달에 이용된 스택의 해제 위치(호출한 함수 or 호출된 함수)를 정해놓은 규약이다.



#용어

caller : 함수 호출자

callee : 함수 피호출자



1) cdecl


주로 C언어에서 사용되고 함수를 호출한 쪽(caller)에서 스택을 정리한다. 그리고 인자는 오른쪽에서 왼쪽으로 전달되며, caller가 callee를 호출 시에 전달되는 인자의 개수를 알고 있기 때문에 가변 인자를 전달할 수 있다는 장점이 있다. 또한 함수 이름 앞에 언더바( _ ) 를 추가해준다. 그리고 함수 호출 후 RET하고 ADD esp, 8을 통해 인자를 정리해준다. 


ex) _Foo



2) stdcall


Win32 API에서 사용되며, callee 가 스택을 정리한다. 그리고 인자는 cdecl 방식과 마찬가지로 오른쪽에서 왼쪽으로 전달되며 Win32 API 에서는 가변 인자 함수가 없기 때문에, 매개변수의 개수가 고정적이다. 이는 caller 에서 스택을 정리하는 것보다 callee 에서 스택을 정리하는 것이 더욱 효율적이다. 또한 함수 이름 앞에 언더바( _ ) 를 추가해주고 함수 이름 뒤에 @를 붙인 후 매개변수의 전체 바이트 수를 10진수로 표기한다. 함수 호출 후 RET 8로 인자를 정리해준다.


ex) _Foo@12



3) fastcall


fastcall 방식에서는 스택이 아닌 가까운 레지스터를 사용함으로써 호출 속도가 빠르며 callee 가 스택을 정리하나 스택을 사용하지 않고 레지스터를 이용하므로 정리할 내용이 없어서 따로 정리를 하지 않는다. 두 개의 파라미터는 ECX와 EDX 레지스터를 이용하지만 나머지 매개변수는 오른쪽에서 왼쪽으로 스택에 올라간다. ECX, EDX 에 대하여 백업이 필요하거나 함수에서 ECX, EDX를 다른 용도로 써야할 경우 파라미터를 따로 저장해야 한다. 또한 함수 이름 앞과 끝에 @이 붙고 뒤에 매개변수의 전체 바이트수를 10진수로 표기한다.


ex) @Foo@12

         


4) thiscall


클래스에서 동작하는 함수 호출규약으로 인자는 오른쪽에서 왼쪽으로 전달되며 this 포인터는 ECX 레지스터에 저장된다. callee에서 스택을 정리하고 모든 파라미터는 스택으로 전달되고 this 포인터만 ECX 레지스터를 통해 전달된다. thiscall 호출 규약은 명시적으로 지정할 수 없으며, 가변 인자를 사용하지 않는 클래스 멤버 함수가 기본적으로 사용하는 호출 규약이다. 클래스 멤버 함수가 가변 인자를 사용할 경우 컴파일 시점에 호출 규약이 cdecl로 변경된다.



5) naked


     caller에서 스택의 정리를 하며, 인자는 오른쪽에서 왼쪽으로 전달되어 스택에     올라간다. 컴파일러가 프롤로그와 에필로그 코드를 생성하지 않으며 직접 inline     assembly를 사용하여 자신만의 프롤로그/에필로그 코드를 작성해야 한다. 따라서     파라미터가 있는 위치를 직접 지정하거나 레지스터 사용방식을 마음대로 정할 수     있다. 





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

[리버싱] CALL, JMP, RET (RETN) 명령어  (0) 2017.08.27
[리버싱] 브레이크포인트란  (0) 2017.08.27
[리버싱] 인터럽트란  (0) 2017.08.26
[리버싱] API 란  (0) 2017.08.25
[리버싱] 레지스터란  (0) 2017.08.25