본문 바로가기

리버싱 개인 공부/[리버싱핵심원리]

[리버싱핵심원리] 스택프레임

스택 프레임이란 ESP(스택 포인터)가 아닌 EBP(베이스 포인터) 레지스터를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 말한다.

스택 프레임의 구조

스택 프레임을 이용해서 함수 호출을관리하면, 아무리 함수호출 depth가 깊고 복잡해져도스택을완벽하게 관리할수 있습니다.

StackFrame.cpp 파일을 열어 코드를 살펴보겠습니다.

코드를 살펴보면 간단한 두 정수의 덧셈 결과를 반환하는 함수를 만들어 호출하고 그 결과를 출력하는 함수입니다.

 

Ollydbg 실습

Ollydbg에서 StackFrame.exe 파일을 열고 Go to 명령으로 401020주소로 갑니다.

 

401020주소에 Break Point를 걸고(F2) 실행(F9)해보면서 스택의 변화를 살펴보겠습니다.

먼저 push ebp를 통해 스택 프레임을 생성합니다. (ebp 값을 stack을 넣는다.) main()에서 EBP가 가지고 있던 값을 스택에 백업합니다.

Stack Window창을 보면 401250에 있던 값이 스택에 들어간걸 볼 수 있습니다.

 

다음 줄 mov ebp, esp를 실행시켜보겠습니다. (esp값을 ebp에 옮겨라.)

esp값과 ebp값이 똑같아진 걸 볼 수 있습니다.

esp값과 같아진 ebp값은 main()함수가 끝날 때까지 고정됩니다.

(esp값 19FF28, ebp값 19FF28으로 같음)

 

스택창을 main()함수가 시작될 때 ebp 초기값인 19FF70이 저장됩니다.)

 

다음줄 sub esp,8를 실행 하겠습니다.(esp 값에서 8을 빼라.)

8만큼의 공간을 확보

0019F28 - 8 = 0019FF20

 

❓ esp에서 8을 빼는 이유

함수의 로컬 변수는 스택에 저장됩니다. StackFrame.cpp의 코드를 보면 main() 함수의 로컬 변수는 ‘a’와 ‘b’입니다. ‘a’와 ‘b’는 long 타입이므로 각각 4바이트의 크기를 가집니다. ‘a’와 ‘b’를 스택에 저장하기 위해서는 총 8바이트가 필요하므로, esp에서 8을 빼서 필요한 메모리 공간을 확보하는 것입니다. main() 함수 내에세 ESP값이 아무리 변해도 ‘a’와 ‘b’ 변수를 위해서 확보한 스택 영역은 훼손되지 않습니다. EBP 값은 main() 함수 내에서 고정이므로 이를 기준으로 삼아서 로컬 변수에 엑세스할 수 있습니다.

 

다음 코드입니다.

[EBP-4] 에는 1을 넣고, [EBP-8]에는 2를 넣으라는 의미입니다. 각각 ‘a’와 ‘b’를 의미합니다.

 

코드 실행 후 스택을 확인해보면

19FF28 - 4 = 19FF24 / 19FF28 - 8 = 19FF20

 

덧셈을 해주는 add()함수

main()에서 add함수를 호출하는데 전형적인 함수 호출 과정입니다. Call 401000에서 401000이 바로 add()입니다. 위에서 봤다시피 add() 함수는 a,b를 파라미터로 받는데 [EBP-8], [EBP-4]의 b,a 역순으로 스택에 저장된다는 것이 중요. b가 먼저 스택에 드러가고 변수 a가 나중에 들어갑니다.(함수 파라미터의 역순 저장)

 

실행한 후 스택 모습

(main 은 a,b/ add함수 호출은 b,a)

 

CALL 401000에서 Stelp in to(F7)로 들어가고

 

스택을 보면

401041로 복귀되는 것을 알 수 있습니다.

main()함수처럼 add()함수가 시작되면 자신만의 스택 프레임을 따로 생성합니다.

 

다음 코드 PUSH EBP와 MOV EBP, ESP는 위에서 설명한 main()함수의 코드와 똑같습니다. 원래 EBP값을 수택에 저장 후 ESP를 EBP에 입력합니다. 이제 add()함수의 스택 프레임이 생성됩니다. add()함수 내에서 eBP값은 고정됩니다.

ESP, EBP 레지스터값이 똑같고,

main()함수에서 사용되던 EBP값 19FF28을 스택에 백업한 후 새로운 EBP는 19FF10으로 새롭게 세팅된 것을 확인할 수 있습니다.

 

SUB ESP 8로 로컬 변수 x,y에 대한 스택 메모리 영역 8바이트를 확보합니다.add() 함수에서 새롭게 스택 프레임이 생성되면서 EBP 값이 변하였습니다. 따라서 [EBP+8], [EBP+C] 가 각각 파라미터 a, b를 가리킵니다. 그리고 [EBP-8], [EBP-4] 는 각각 add() 함수의 로컬 변수 x,y를 의미합니다.

 

 

다음 코드는 StackFrame.cpp에서 아래 코드 부분에 해당되는 내용입니다.

return (x+y)

401012번째 줄 MOV EAX, DWORD PTR SS [EBP-8] : 변수 x의 값([EBP-8] = 1)을 EAX에 넣습니다.

401015번 째 줄 ADD EAX, DWORD PTR SS [EBP-4] : EAX에 변수 y의 값([EBP-4] =2)을 더합니다. EAX의 값은 3이됩니다.

(EAX = 3)

 

다음은 add() 함수의 스택 프레임을 해제합니다.

최초의 MOV EBP, ESP의 역으로 원래대로 ESP를 복원하는 스택프레임을 해제합니다.

현재 EBP값을 ESP에 옮기고, add() 함수가 시작되면서 백업했던 EBP값을 원래대로 복원합니다.

(함수 호출전의 스택상태로 돌아온 것을 볼 수 있습니다.)

 

다시 main()함수로 와서 코드를 보겠습니다.

main()함수로 돌아오자마자 ESP에 8을 더하라고하는데, add() 함수에 넘겨준 파라미터 a,b의 공간이 더 이상 필요없기 때문에 ESP에 8을 더하여 스택을 정리하는 것입니다. (파라미터 a,b는 long 타입이므로 각각 4바이트, 총 8바이트입니다.)

add()함수가 완전히 종료되었기 때문에 정리 된것을 볼 수 있습니다.

 

다음 코드부터는 StackFrame.cpp에서 아래 코드 부분에 해당되는 내용입니다.

printf("%d\\n", add(a,b));

 

401044주소에서 EAX 값은 3입니다.

그리고 위의 40104A에서 call의 401067은 Visual C++에서 생성한 printf() 함수입니다.

이후에 printf() 함수 호출 후 스택이 정리되었기 때문에 ADD ESP 8 로 함수 파라미터를 제거해줍니다.

 

다음 코드는 StackFrame.cpp에서 아래 코드 부분에 해당되는 내용입니다.

return 0;

 

XOR EAX, EAX : 값은 값끼리 XOR 하면 0이 되는 특징이 있습니다. 같은 값을 이용해서 2번 연속으로 XOR 연산을 수행하면 원본 값이 됩니다.

 

(EAX 값이 0이 되었습니다.)

 

MOV ESP,EBP(EBP값은ESP 값에 넣어라)

POP EBP(EBP 값을 빼라)

위 두 명령을 실행시켜보면 main() 함수의 스택 프레임이 해제된걸 볼 수 있습니다. main()함수의 로컬 변수 a,b도 더 이상 유효하지 않게 됐습니다.

 

 

 

마지막으로 RETN 명령을 실행 시키면

메인 함수가 종료(리턴)되면서 리턴 주소(401250)으로 점프합니다. 그 주소는 Visual C++ 의 Stub Code영역입니다. 이후에는 프로세스 종료 코드가 실행됩니다. (생략)