기초부터 다시 정리를 해보자 한다.
스택 프레임에 대하여 알아보자.
우선 함수 하나가 호출된다면 그 스택 내에는
이렇게 저장된다.
//gcc -m32 -mpreferred-stack-boundary=2 -fno-stack-protector -z execstack -no-pie -fno-pic -o stack stack.c
#include <stdio.h>
int add(int a, int b){
int c = a + b;
return a+b;
}
int main(void){
int var1 = 1;
int var2 = 2;
add(var1, var2);
return 0;
}
32비트로 보호기법을 해제하고 컴파일 해보자.
main 함수에서 add 함수를 호출할 때를 잘 살펴보자. 우선 main+17까지 확인을 해보자. main+17까지 실행이 된다면 스택은 이런 형태를 띌 것이다. 실제로 그런지 디버깅 해보자. 실제로 스택 영역을 확인해 본다면, 일치한다는 것이 보인다. 이제 함수를 호출시켜보자.
main+24 코드부터는 함수 호출을 진행하는 코드이다.
0x080491ad <+24>: push DWORD PTR [ebp-0x8]
0x080491b0 <+27>: push DWORD PTR [ebp-0x4]
0x080491b3 <+30>: call 0x8049176 <add>
[ebp-0x8] (main_var1)을 스택에 PUSH 한다.
이후 [ebp-0x4] (main_var2)도 스택에 PUSH 한다.
이후 함수를 호출하게 된다. 호출하기 직전까지의 스택을 살펴보자.
이런 스택 구조를 가지겠다. 여기서 중요한건, push된 두 개의 값에 대해서는 내가 add함수의 인자인 a, b 변수라고 지정을 했다는 것이다. 왜 그랬는 지 확인을 해보자. add를 call한다.
gdb-peda$ disas add
Dump of assembler code for function add:
0x08049176 <+0>: endbr32
0x0804917a <+4>: push ebp
0x0804917b <+5>: mov ebp,esp
0x0804917d <+7>: sub esp,0x4
0x08049180 <+10>: mov edx,DWORD PTR [ebp+0x8]
0x08049183 <+13>: mov eax,DWORD PTR [ebp+0xc]
0x08049186 <+16>: add eax,edx
0x08049188 <+18>: mov DWORD PTR [ebp-0x4],eax
0x0804918b <+21>: mov edx,DWORD PTR [ebp+0x8]
0x0804918e <+24>: mov eax,DWORD PTR [ebp+0xc]
0x08049191 <+27>: add eax,edx
0x08049193 <+29>: leave
0x08049194 <+30>: ret
End of assembler dump.
gdb-peda$ disas main
Dump of assembler code for function main:
0x08049195 <+0>: endbr32
0x08049199 <+4>: push ebp
0x0804919a <+5>: mov ebp,esp
0x0804919c <+7>: sub esp,0x8
0x0804919f <+10>: mov DWORD PTR [ebp-0x4],0x1
0x080491a6 <+17>: mov DWORD PTR [ebp-0x8],0x2
0x080491ad <+24>: push DWORD PTR [ebp-0x8]
0x080491b0 <+27>: push DWORD PTR [ebp-0x4]
0x080491b3 <+30>: call 0x8049176 <add>
0x080491b8 <+35>: add esp,0x8
0x080491bb <+38>: mov eax,0x0
0x080491c0 <+43>: leave
0x080491c1 <+44>: ret
End of assembler dump.
이 쯤에서 한 번 다시 적는 어셈블리.
main+30에서 add를 call 하는데, call 명령은 push EIP, jmp ADDR 이다.
위와같이 main에서 call 한 다음 실행될 주소가 들어가게 된다. 저 곳을 RET라 부른다. 이제 호출된 함수에서 처음에 무슨 일을 할까 ? 이것을 함수의 프롤로그라고 부른다.
gdb-peda$ disas add
Dump of assembler code for function add:
0x08049176 <+0>: endbr32
0x0804917a <+4>: push ebp
0x0804917b <+5>: mov ebp,esp
0x0804917d <+7>: sub esp,0x4
0x08049180 <+10>: mov edx,DWORD PTR [ebp+0x8]
0x08049183 <+13>: mov eax,DWORD PTR [ebp+0xc]
0x08049186 <+16>: add eax,edx
0x08049188 <+18>: mov DWORD PTR [ebp-0x4],eax
0x0804918b <+21>: mov edx,DWORD PTR [ebp+0x8]
0x0804918e <+24>: mov eax,DWORD PTR [ebp+0xc]
0x08049191 <+27>: add eax,edx
0x08049193 <+29>: leave
0x08049194 <+30>: ret
End of assembler dump.
push ebp
mov ebp, esp
위의 과정이 함수 프롤로그이다. 원래 함수에서 쓰이던 ebp(SFP)를 push하고, 현재의 esp를 ebp로 만들게 된다. ebp는 함수 내에서 변수들 위치를 설정하는 기준이 되므로 매우 중요하다.
아무리 pop을 하거나 아무리 add를 한다고 한들, 함수 내에서 esp가 ebp보다 커질 수는 없다.
이제 [ebp+0x8]과 [ebp+0xc]를 각각 edx, eax에 집어넣게 된다.
이후 eax = eax + edx를 진행하게 되는데, 여기서 알 수 있는 것이 [ebp+0x8]과 [ebp+0xc]가 인자로 들어가졌다는 것이다. 내가 지금까지 그린 스택의 경우 아래의 주소가 위보다 크기에 (위로 자라나는 구조) ebp+n의 주소를 보고자 한다면, 아래로 내려가야 한다.실제로 main+8은 인자 a이며 main+c는 인자 b인 것을 스택에서 확인할 수 있다. 이후 eax에는 그 합이 들어가 있으며, [ebp-0x4]에 그 값을 집어넣게 된다. 즉 스택은 최종적으로 위의 구조를 이루고 있다.
return c;
만약 add 함수가 위처럼 끝난다면 바로 함수의 에필로그를 진행하면 되지만, 나의 실수로 인해
return a+b;
를 해버렸기에 add+21부터 add+27까지 똑같은 과정을 또 반복하고 있다.
어쨌든 이제는 함수의 에필로그를 확인해 보도록 하자.
0x08049193 <+29>: leave
0x08049194 <+30>: ret
함수의 에필로그는 leave와 ret로 이루어져 있다.
leave는 mov esp, ebp; pop ebp로 이루어져 있다.
이 과정을 거치면 무슨 일이 벌어질까 ? 먼저 mov를 해보자.
스택은 위와같이 구성된다.
즉 esp가 ebp가 가리키던 add의 SFP를 가리키게 된다. 이 add의 SFP에는 main SFP의 주소가 들어가있는데, 그 값을 pop 하여 ebp에 넣게된다. 즉 ebp는 원래 main의 SFP 주소를 가지게 된다.
이후 ret를 거쳐보자.
ret는 pop EIP; jmp EIP 이다. SFP에서 pop을 한 상태이기에, 현재 esp는 RET의 주소를 가지고 있다. 이 때 pop EIP를 하게 된다면, EIP는 RET를 가지게 되고 jmp까지 하면 main+35의 주소를 가지게 된다.
즉, main+35부터 나머지 과정을 계속 실행하게 된다.
64비트의 경우 32bit 처럼 stack을 통한 인자 전달이 아닌 레지스터를 통한 인자전달이라고 생각하면 된다.
push ebp
mov esp, ebp
leave
ret
위의 과정은 동일하다. 포너블 풀이 또는, 앞으로 한 번 더 정리를 하게 될 것이다.