[리버싱 - 1] : 함수의 스택 프레임 이해하기

어셈블리코드에서 함수의 진입점을 파악하는 법과 스택 프레임 이해하기


push ebp
mov ebp, esp

위 코드는 함수의 진입점에서 흔히 볼 수 있는 코드이다. 저 코드의 목적은 함수 내부에서 사용할 스택 공간을 개설하는 작업이다. ebp는 스택의 시작점을 알려주는 레지스터이고 esp는 스택의 종료점을 알려주는 레지스터이다.

함수가 진행하면 push ebp로 ebp를 스택에 넣고 esp의 값을 ebp에 넣어준다. 이로써 esp와 ebp의 포인터가 같은 공간을 바라 보는 작업이 진행된 상황이고 이 상황에서의 스택 크기는 0이다.

만약 함수 내부에서 지역변수를 사용하지 않거나, 지역변수들의 크기가 작은 경우 함수의 스택 공간 작업은 여기서 마무리된다. 지역변수를 사용하지 않는다면 스택을 사용하지 않을 것이고, 지역변수의 크기가 적다면 굳이 메모리를 사용하지 않고 레지스터로 변수들 작업이 가능하기 때문이다.

그렇다면 함수에서 스택 공간이 필요한 경우는 어떻게 하나?

push ebp
mov ebp, esp
sub esp, 54

위 코드의 3번째 명령어 처럼 ebpesp의 간격을 벌려서 스택 공간을 만들면 된다.

기본적으로 ebp의 값이 esp의 값보다 크다. 그러므로 스택 공간을 만들때 esp의 값을 마이너스시켜서 ebp와 esp의 간격을 만든다. 그 간격이 스택의 공간이다. 위 코드의 sub esp, 54는 54바이트 만큼의 스택을 공간을 만든 것이다.

그러므로 어셈블리 코드에서 지역변수를 확인하는 방법은 ebp에서 "-"하며 접근하던가 esp에서 "+"하며 접근하는 방식 두 개가 대표적이다.

즉, 함수에서 만약 지역변수를 2개 사용하여 스택 공간을 8바이트 확보 했다면 ebp-4ebp-8 두 곳에 변수 2개가 있을 것이다. 반대로 esp+0esp+4로도 가능할 것이다. esp는 스택의 종료점이므로 esp를 통해 접근하는 방법은 스택 프레임이 변하지 않는 경우만 유효하다. esp를 기준으로 접근하는 것보다 ebp로 접근하는 방식이 더욱 안정적이고 대중적이다.

그러나 실제로 지금 바로 C언어로 함수가 존재하는 프로그램을 하나 만들어서 디버깅하면 epb-4ebp-8의 위치는 실제 지역변수의 위치와 다를 것이다. 왜나면 지금의 대부분 컴퓨터 환경은 64비트 환경이고 위 예제는 32비트 환경에서의 예제이기 때문이다. 64비트 컴퓨터에서는 rbp-8rbp-16에 변수가 위치할 것이다.

rbp는 64비트 환경에서 사용할 수 있도록 ebp(32비트)에서 확장된 형태이다.

이는 당연하게도 32비트에서는 주소 표현에 4바이트(32비트)를 이용하지만 64비트에서는 8바이트(64비트)를 이용하기 때문이다.

x86-64-register https://dreamhack.io/

마지막으로 ebp에서 "+"하는 영역에는 무엇이 저장되어 있을까?

ebp + 4에는 함수가 끝나고 돌아갈 리턴 주소가 담겨있다. 해당 함수의 작업을 마치고 함수를 호출했던 곳으로 다시 돌아가기 위한 주소이다.

그 위는 함수의 파라미터들의 공간이다. 함수가 파라미터를 총 2개 이용하는 함수라면 ebp+8ebp+12에 각각 저장되어 있을 것이다.

|       ...       |
|     param2      |  <- EBP + 12
|     param1      |  <- EBP + 8
| return address  |  <- EBP + 4
|     saved EBP   |  <- ★EBP★
|  local_var1     |  <- EBP - 4
|  local_var2     |  <- EBP - 8

리버싱에서 핵심은 아래의 어셈블리 코드를 보고

push ebp
mov ebp, esp
sub esp, 54

"아! 지금부터 함수가 시작되겠군 그리고 이 함수의 스택공간은 54바이트네?"

를 예측할 수 있어야 된다는 것이다.