-
[리버싱 핵심원리 study] 7장 Stack FrameReverse Engineering 2020. 12. 1. 02:02
1. Stack Frame 개념 설명
Stack Frame이란 ESP(스택 포인터)가 아닌 EBP(베이스 포인터) 레지스터를 이용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소 등에 접근하는 기법을 말한다. ESP 값은 프로그램 안에서 수시로 변하기 때문에 EBP를 함수 시작 전에 저장하고 유지하면 안전하게 변수, 파라미터, 복귀 주소 등에 접근할 수 있다.
Stack Frame의 어셈블리 코드 부분은 다음과 같다.
PUSH EBP MOV EBP, ESP ... ... ... MOV ESP, EBP POP EBP RETN
함수가 호출되고 나면 PUSH EBP를 통해 기존 EBP값(함수 호출 이전의 EBP 주소)을 저장한다. 이후에 MOV EBP, ESP를 통해 ESP 값을 EBP에 저장하게 되면, 함수 내부의 로컬 변수는 EBP-4 등으로, 파라미터는 EBP+4등으로 접근이 가능해진다. 함수의 기능을 다 수행하고 나면 MOV ESP, EBP를 통해 스택을 정리해주고 마지막으로 POP EBP를 통해 함수 호출 이전의 EBP 값을 복원한다. 마지막으로 RETN을 통해 스택에 저장되었던 복귀 주소로 돌아간다.
그림으로 EBP, ESP를 도식화하면 다음과 같다. (단, 스택은 거꾸로 자라므로 ESP가 위로 올라가면 주소는 줄어든다.)
2. 프로그램 작성
본격적으로 stack frame을 어셈블리 레벨에서 확인해보기 위해 간단한 프로그램을 작성해보자. 다음과 같은 코드를 작성하고 빌드하여 프로그램을 생성한다. (visual c++ express 2010 release 모드 기준으로 빌드하였으며, 최적화 옵션을 끈 상태로 컴파일하였다. 최적화 옵션을 켰을 경우 함수 생성이 안 될 수도 있다.)
#include "stdio.h" long add(long a, long b) { long x=a, y=b; return (x+y); } int main(int argc, char* argv[]) { long a=1, b=2; printf("%d\n", add(a, b)); return 0; }
(StackFrame.cpp)
코드를 보면 알겠지만, 간단한 두 정수의 덧셈 결과를 반환하는 함수를 만들어 호출하고 그 결과를 출력하는 함수다.
3. Debugging(with x32dbg)
x32dbg를 이용하여 해당 프로그램을 열어보자.
cpp 파일은 몇 줄 안되는데 언제나처럼 굉장히 많은 코드 정보가 눈에 들어온다. F7, F8을 연타하여 main 함수를 찾아낼 수도 있겠지만, 보다 수월하게 찾는 방법을 생각해보자. 다양한 방법이 있겠지만, printf 함수를 main에서 호출하므로 마우스 우클릭-> 모듈 간 호출을 통해 한번 찾아보자.
고맙게도 첫 번째 줄에 printf가 나와주었다. 더블클릭하여 해당 주소로 이동해보자.
위와 같은 형태의 코드가 보인다. 과연 이것이 main 함수일까? 위 코드를 한 줄씩 분석해보면 다음과 같은 특징을 보인다.
(1) 우선 첫 두 줄은 push ebp / mov ebp, esp 명령을 수행한다. 이는 앞서 설명한 Stack Frame을 형성하는 과정과 동일하다. main 함수 역시 함수이므로 같은 방식으로 함수가 시작하는 것을 확인할 수 있다.
(2) 00B81026 와 00B8102D 주소의 mov 명령어는 1, 2를 각각 ebp로부터 4, 8 떨어진 곳에 저장한다. 이는 StackFrame.cpp 파일의 long a=1, b=2; 부분과 관련지을 수 있다.
(3) 00B81034 ~ 00B8103B의 코드는 eax, ecx 레지스터에 아까 1, 2를 저장했던 변수에 저장된 값을 mov 연산을 통해 옮겨주고, 이후에 call 연산을 통해 add함수를 호출하는 것을 확인할 수 있다.
즉, 우리가 찾는 main함수임을 알 수 있다. 이제 main함수를 찾았으니, main의 시작 부분에 bp를 설치하고 실행을 한 후, EBP, ESP 값이 어떻게 변화하는지 관찰해보자. 우선 main()의 함수 시작 시 스택은 다음과 같은 상태이다.
이때 ESP에 저장된 값은 main()의 실행이 끝난 후 돌아갈 리턴 주소임을 알고 있자. 현재 상태에서 PUSH EBP를 실행하면 스택에 EBP값이 저장된다. 이렇게 저장하는 이유는 함수가 실행되기 이전의 EBP값을 백업하기 위함이다. 이후에 MOV EBP, ESP를 통해 ESP의 값을 EBP에 옮긴다. 이후에는 main 함수가 끝날 때까지 EBP 값은 고정된다. 스택 창을 마우스 우클릭에서 EBP 따라가기, ESP 따라가기 등을 선택하여 EBP, ESP 관점에서 스택을 볼 수도 있다.
이제 push ebp / mov ebp, esp를 수행한 후의 스택창을 살펴보자.
위와 같이 Stack Frame이 정상적으로 생성되었음을 확인할 수 있다.
이제 add 함수를 호출하기 이전 과정을 살펴보자.
우선 main 함수에서는 2개의 long 변수를 선언한다. 하나의 long 변수는 4바이트를 차지하므로, sub esp, 8을 통해 2개의 변수를 할당할 수 있게 스택에 공간을 할당해준다. 이후에 mov 연산을 통해 1, 2를 새로 생성된 공간에 할당해준다. 이때, ebp를 통해 로컬 변수에 접근하는 것을 확인할 수 있다. 해당 변수가 할당된 직후 StackFrame.cpp 파일에서는 add 함수를 호출한다. 이를 위해 argument를 넘겨줘야 하는데, eax와 ecx 레지스터에 각각 값을 이동시킨 후, push 연산을 통해 스택에 채워넣는다. 이 때 눈여겨볼 점은 바로 변수가 역순으로 스택에 쌓인다는 것이다. 스택은 LIFO(Last In First Out) 구조이기 때문에, 역순으로 집어넣게 되면 함수 쪽에서는 올바른 순서로 파라미터를 꺼낼 수 있기 때문이다. push ecx까지 수행한 후 ESP, EBP 값은 다음과 같다.
이제 call 부분을 실행시켜 add 함수 내부로 들어가 보겠다. call 부분이 실행되면 해당 함수로 들어가기 전에 CPU는 해당 함수가 종료될 때 복귀 주소(다음으로 실행되어야 할 명령어 주소)를 스택에 저장한다.
위와 같이 add 함수 내부 역시 push ebp / mov ebp, esp로 시작하고, ret(return) 이전에 mov esp, ebp / pop ebp를 이용해 Stack Frame이 생성/삭제되는 것을 확인할 수 있다. add 함수는 return 값이 있는 함수인데, return 값이 있는 함수의 경우 해당 값을 eax 레지스터에 저장한다. 이는 00B81015 주소의 명령어에서 확인할 수 있다. 이제 ret까지 수행하고 난 후의 스택 창을 확인해보자.
위와 같이 함수 호출 직전의 스택과 동일한 것을 확인할 수 있다. 이제 마지막으로 main 함수의 add 함수 호출 이후 부분의 코드를 잠시 살펴보자.
가장 상단 부분의 add esp, 8은 함수의 호출이 끝났으니, 사용했던 argument는 더 이상 필요가 없으므로 스택에서 제거해주는 과정이다. StackFrame.cpp에서는 이후에 printf 함수를 호출하므로 add 함수를 호출하는 과정과 마찬가지로 eax(add 함수의 반환 값)과 B820F4 주소(서식 문자 %d가 있다.)를 push 해주고 printf함수를 호출한다. 마찬가지로 add esp, 8을 통해 스택에 쌓아두었던 argument를 해제해준다.
위와 같이 함수가 호출되면 stack frame이 먼저 생성되고 ebp 레지스터를 이용해 각종 변수/파라미터에 접근한 후, 함수가 종료되기 직전에 stack frame을 해제하는 것을 확인할 수 있다.
반응형'Reverse Engineering' 카테고리의 다른 글
[리버싱 핵심원리 study] 8장 abex' crackme#2 분석(2) (0) 2020.12.03 [리버싱 핵심원리 study] 8장 abex' crackme#2 분석(1) (1) 2020.12.02 [dreamhack.io study] x64dbg로 hello world 디버깅해보기 (0) 2020.11.26 [리버싱 핵심원리 study] 6장 abex' crackme #1 분석 (0) 2020.11.25 [리버싱 핵심원리 study] 2장 Hello World! 리버싱 (0) 2020.11.22