오늘은 스택 프레임에 대해서 알아보겠다!
스택 프레임
스택 프레임이란 ESP가 아닌 EBP 레지스터를 사용해서 스택 내의 변수, 파라미터, 복귀 주소에 접근하는 기법!
(어제 스택의 구조 공부했다능 ㅎㅎ 스택의 ESP값은 유동적인 놈이기 때문에 EBP값을 이용해서 정확한 위치를 참고할 수 있게 된다!)
쉽게
- 스택 프레임 : 어떠한 함수가 호출 되었을 때 그 함수가 가지는 공간의 구조!
스택 프레임의 구조는 다음과 같습니더
PUSH EBP : 함수의 시작! (EBP값을 스택에 저장)
MOV EBP, ESP : ESP값을 EBP에 저장
... : 함수본체, 여기서 ESP가 마구잡이로 변경되어도 EBP값에 영향을 미치지않음
MOV ESP, EBP : ESP값 복원(함수 시작했을 때 값으로 복원)
POP EBP : EBP값 복원
RET : 함수 종료
간단히 요약
- 함수 호출 시 EBP값을 스택에 저장해놓고, 마구잡이로 쓰다가 함수 끝날 때에~쯤 되면 다시 복원시키고 함수를 종료함
자! 실습하자 StackFrame.exe 로 실습한다
StackFrame의 코드는 다음과같다.
Stack Frame.cpp
#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;
}
그냥 더하는 프로그램이다 ㅋㅋ
그럼 리버싱 핵심원리를 따라서 ㄱㄱ
x32dbg로 StackFrame.exe 파일을 열고 401000 주소로 간다.
이후에 401020에 [F2]를 걸고 실행해보면서 스택의 변화를 살펴보자!!!
401020은 main()입니다. 실행을 시키면 바로 스택 프레임을 생성합니다.
PUSH는 스택을 집어넣을 때 사용, EBP값을 스택에 집어넣는 행위 입니다. main()에서 EBP가 이전에 가지고 있던 값을 스택에 백업!
그다음 줄을 실행합니다.
MOV 명령은 데이터를 옮기는 명령. ESP값을 EBP에 옮겨라! 이 명령 이후에는 EBP와 ESP값이 같아지는걸 확인할 수 있습니다.
그리고 이렇게 같아진 EBP값은 main()함수가 끝날 때까지 고정됩니다. (401020,401021의 실행을 통해 스택프레임 생성!)
* EBP와 ESP가 18FF40으로 같아졌고, 18FF40에는 main()함수가 시작할 때 EBP가 가지고 있던 초기값인 18FF88을 저장하게 됨
다음으로 main()안의 로컬 변수 (a,b)를 위한 공간을 만들예정 입니다
long a = 1, b = 2;
그 다음줄을 실행합니다
SUB는 알다시피 빼기 명령어 입니다 ESP에서 8을 빼라는 소리인데 a와 b가 long 타입이므로 각각 4바이트씩 크기를 가지기 때문에 8만큼의 공간을 확보합니다.
다음 코드 입니다.
[EBP-4]에는 1을 넣고, [EBP-8]에는 2를 넣어라는 의미입니다. 각각 a,b를 의미하게 되겠습니다.
여기까지 실행한 후에 스택 상태를 확인!
이제 덧셈을 해주는 add()함수로 가봅시다.
main()에서 add함수를 호출하는데 전형적인 함수호출의 모습이라고 합니다.
call 401000에서 401000이 바로 add()입니다. 위에서 봤다시피 add() 함수는 a,b를 파라미터로 받는데
[EBP-8], [EBP-4]의 b,a 역순으로 스택에 저장된다는 것이 중요. b가 먼저 스택에 드러가고 변수 a가 나중에 들어갑니다.
여기까지 실행한 후의 스택의 모습
이후에 call을통해 add()함수가 실행되고 다시 복귀주소를 통해 돌아와야 합니다.
바로밑의 401041이 바로 add()함수의 복귀 주소입니다. call 실행 후 스택의 모습은 다음과 같습니다.
main() 함수가 스택프레임을 생성했듯이, add()함수도 스택 프레임을 따로 생성합니다.
코드는 main() 함수와 똑같습니다.
401000 ~ 40100F 까지의 그것은 main()과 같기때문에 따로 설명X
직접 실행해보면서 레지스터와 스택의 변화를 몸소 체험해보자!
이후에 마지막 ADD연산을 수행합니다. ADD 연산은 아래 코드에 해당하는 내용입니다.
return (x + y);
401012 번째 줄입니다.
변수 x의 값(EBP-8)을 EAX에 넣습니다. x=1
변수 y의 값(EBP-4)를 EAX에 더합니다.
1+2 =3 이므로 EAX의 값이 3이 되는것을 EAX 레지스터를 통해 확인해봅니다.
이제 add() 함수의 스택 프레임을 해제합니다.
최초의 MOV EBP, ESP의 역으로써 원래대로 ESP를 복원하는 스택프레임 해제!
add() 함수가 시작되면서 백업했던 EBP값을 원래대로 복원 (401000과 대응되는 명령어)
* 함수 호출전의 스택 상태로 완벽히 돌아오게 됨. (Stack BOF 기법에 취약할 수 있음)
이제 main()함수로 돌아왔다. 다음 코드를 봅시다.
갑자기 ESP에 8을 더한다. 이유는 이러하다.
add() 함수에 넘겨준 파라미터 a,b의 공간이 이제 필요없기 때문에 회수해야한다.
=> add() 함수가 완전히 종료되었기 때문에 스택을 정리하는 과정ㅋ
이제 printf 함수를 호출 해야겠죠?
printf("%d\n", add(a,b));
printf() 함수 호출 코드입니다.
401044 주소의 EAX에는 아까 add() 함수에서 저장된 EAX 값 3이 들어있습니다.
그리고 위의 40104A에서 call의 401067은 Visual C++에서 생성한 printf() 함수입니다.
(printf() 함수는 복잡하기 때문에 알아서 찾아보셈 ㅎㅎ 똑같이 파라미터 2개쓰며 크기는 8바이트를 사용!)
이후에 printf() 함수 호출 후 스택이 정리되었기 때문에 ADD로 함수 파라미터를 제거해줍니다 :3
그 다음으로 main() 함수의 리턴값(0)을 셋팅해줍니다.
return 0;
이제 main() 함수를 종료하면서 스택프레임을 해제합니다.
마찬가지로 초기의 행위의 역순으로 수행합니다.
위의 두 명령으로 인해서 main() 함수의 스택프레임마저 해제되었습니다.
스택의 상태를 확인해보면 제일 처음의 스택모습과 완전히 동일한 모습입니다.