abstract
goal
- Stack Frame 개념의 이해
- 간단한 프로그램을 만들고 디버거를 이용해서 stack frame 을 확인
Stack
프로세스에서 스택(stack) 메모리의 역할은 아래와 같습니다.
1. 함수 내의 로컬 변수 임시 저장
2. 함수 호출 시 파라미터 전달
3. 복귀 주소(return address)를 저장
위와 같은 역할을 수행하기에는 스택의 FILO(First In Last Out) 구조가 아주 유용합니다.
그리고 스택에 대해서 주의하실 점은 아래와 같습니다.
1. 32/64 bit 컴퓨팅 환경에서 스택 크기는 각각 32/64 bit (4/8 byte) 이다.
2. 스택은 아래로 자란다.
- 스택에 값을 입력하면 (PUSH 명령) 스택 포인터는 4 byte 만큼 줄어든다.
- 스택에서 값을 가져오면 (POP 명령) 스택 포인터는 4 byte 만큼 늘어난다.
Stack Frame
stack frame 이란 쉽게 말해서 EBP (베이스 포인터) 레지스터를 사용하여
스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 말합니다.
IA-32 Register 기본 설명 에서 ESP 레지스터가 stack pointer 로써 스택의 top 을 가리키고,
EBP 레지스터는 base pointer 역할을 한다고 설명했었습니다.
ESP 레지스터의 값은 프로그램 안에서 수시로 변경되기 때문에
스택에 저장된 변수, 파라미터에 접근하고자 할때 ESP 값을 기준으로 하면
프로그램을 만들기 힘들고, CPU 가 정확한 위치를 참고할때 어려움이 있습니다.
따라서 어떤 기준 시점(함수 시작)의 ESP 값을 EBP 에 저장하고 이를 함수 내에서 유지해주면,
ESP 값이 아무리 변하더라도 EBP 를 기준(base)으로 안전하게 변수, 파라미터, 복귀 주소에 접근할 수 있습니다.
이것이 바로 EBP 레지스터의 base pointer 로써의 역할입니다.
assembly 코드로 보면 이런 형식입니다.
PUSH EBP ; 함수 시작 (EBP 를 사용하기 전에 기존의 값을 스택에 저장)
MOV EBP, ESP ; 현재의 ESP(스택포인터) 를 EBP 에 저장
... ; 함수 본체
; 여기서 ESP 가 변경되더라도 EBP 가 변경되지 않으므로
; 안전하게 로컬변수와 파라미터를 access 할 수 있음
MOV ESP, EBP ; ESP 를 정리 (함수 시작했을 때 값으로 복원시킴)
POP EBP ; 리턴 되기 전에 저장해 놓았던 원래 EBP 값을 복원
RETN ; 함수 종료
stack frame 을 이용해서 함수 호출을 관리하면,
아무리 함수 호출 depth 가 깊어지고 복잡해져도 스택을 완벽하게 관리할 수 있습니다.
* 최신 컴파일러는 최적화(Optimization) 옵션을 가지고 있어서
간단한 함수 같은 경우에 stack frame 을 생성하지 않을 수 도 있습니다.
* stack 에 복귀주소가 저장된다는 점이 보안 취약점으로 작용할 수 있습니다.
buffer overflow 기법을 사용하여 복귀주소가 저장된 스택 메모리를 의도적으로 다른 값으로 변경이 가능합니다.
(향후 buffer overflow 에 대해서 정리해보는 시간을 갖도록 하겠습니다.)
간단한 예제 - stackframe.exe
stack frame 설명을 위해서 아주 간단한 프로그램을 만들어 보겠습니다.
int add(long, long);
int main(int argc, char* argv[])
{
long a = 1, b = 2;
add(a, b);
return 0;
}
int add(long a, long b)
{
long x = a, y = b;
return (x + y);
}
VC++ 의 '최적화(Optimization)' 옵션을 끄고(disabled) 빌드하면 (/Od 옵션), 아래와 같은 바이너리 코드가 생성됩니다.
(VC++ 9.0 의 main() 함수 찾는 법은 "Hello World 디버깅" 을 참고하세요.)
<Fig. 1>
위 코드를 상세하게 살펴보도록 하겠습니다.
00401000 PUSH EBP ; main() 함수 시작
00401001 MOV EBP,ESP ; ESP -> EBP (스택 프레임 생성)
00401003 SUB ESP,8 ; local 변수 a, b 를 위한 공간을 확보한다.
00401006 MOV DWORD PTR SS:[EBP-4],1 ; EBP-4 = a = 1
0040100D MOV DWORD PTR SS:[EBP-8],2 ; EBP-8 = b = 2
00401014 MOV EAX,DWORD PTR SS:[EBP-8] ; b
00401017 PUSH EAX ; add() 함수의 두번째 파라미터 b 를 스택에 입력
00401018 MOV ECX,DWORD PTR SS:[EBP-4] ; a
0040101B PUSH ECX ; add() 함수의 첫번째 파라미터 a 를 스택에 입력
0040101C CALL stackfra.00401030 ; add() 함수 호출
눈여겨 보실 부분은 main() 함수의 로컬 변수 a, b 를 가리킬 때 ESP 가 아닌 EBP 를 사용한다는 점입니다.
(a = EBP-4, b = EBP-8)
그리고 0040101C 주소의 CALL 에 의해서 자동으로 스택에 복귀주소가 입력됩니다.
스택의 모양은 이렇게 됩니다. (ESP = 12FF68)
address value comment
------------------------------------------
EBP-14 0012FF68 00401021 ; 복귀주소 -> add() 함수의 실행이 종료되면 이 주소로 돌아온다.
EBP-10 0012FF6C 00000001 ; add() 함수의 첫번째 파라미터 a
EBP-C 0012FF70 00000002 ; add() 함수의 두번째 파라미터 b
EBP-8 0012FF74 00000002 ; main() 함수의 변수 b
EBP-4 0012FF78 00000001 ; main() 함수의 변수 a
EBP ==> 0012FF7C 0012FFC0 ; main() 함수 stack frame 의 EBP 값 = 12FF7C
add() 함수 내부로 디버깅을 계속합니다.
00401030 PUSH EBP ; add() 함수 호출
00401031 MOV EBP,ESP ; ESP -> EBP (add() 함수의 스택 프레임 생성)
00401033 SUB ESP,8 ; local 변수 x, y 를 위한 공간을 확보한다.
00401036 MOV EAX,DWORD PTR SS:[EBP+8] ; EBP+8 = a
00401039 MOV DWORD PTR SS:[EBP-8],EAX ; EBP-8 = x
0040103C MOV ECX,DWORD PTR SS:[EBP+C] ; EBP+C = b
0040103F MOV DWORD PTR SS:[EBP-4],ECX ; EBP-4 = y
add() 함수를 위한 스택 프레임이 생성됩니다.
이제부터 EBP 는 add() 함수의 스택 프레임을 구성하는 베이스 포인터가 됩니다.
(main() 함수에서 사용되던 베이스 포인터는 401030 주소의 PUSH 명령에 의해 스택에 저장됨)
여기까지 오면 스택의 모양은 이렇게 됩니다. (ESP = 12FF5C)
address value comment
------------------------------------------
EBP-8 0012FF5C 0000000A ; add() 함수의 변수 x
EBP-4 0012FF60 00000014 ; add() 함수의 변수 y
EBP ==> 0012FF64 0012FF7C ; add() 함수 stack frame 의 EBP 값 = 12FF64
EBP+4 0012FF68 00401021 ; 복귀주소 -> add() 함수의 실행이 종료되면 이 주소로 돌아온다.
EBP+8 0012FF6C 00000001 ; add() 함수의 첫번째 파라미터 a
EBP+C 0012FF70 00000002 ; add() 함수의 두번째 파라미터 b
EBP+10 0012FF74 00000002 ; main() 함수의 변수 b
EBP+14 0012FF78 00000001 ; main() 함수의 변수 a
EBP+18 0012FF7C 0012FFC0 ; main() 함수 stack frame 의 EBP 값 = 12FF7C
스택이 상당히 복잡하게 보입니다만, 위에서부터 설명을 차근차근 읽어 내려오면 이해할 수 있으실 겁니다.
(address 를 기준으로 main() 함수의 스택과 비교해 보세요.)
00401042 MOV EAX,DWORD PTR SS:[EBP-8] ; EBP-8 = x
00401045 ADD EAX,DWORD PTR SS:[EBP-4] ; EBP-4 = y
00401048 MOV ESP,EBP ; ESP 복원 (12FF64) -> add() 스택 제거
0040104A POP EBP ; EBP 복원 (12FF7C) -> main() 스택 프레임 EBP
0040104B RETN ; add() 함수 종료
add() 함수가 종료되면서 스택 프레임을 제거하고 EBP 값을 원래대로 복원시킵니다.
(main() 함수의 스택 프레임 EBP 로 변경됩니다.)
RETN 명령에 의해서 12FF68 에 저장되어 있던 복귀주소가 스택에서 빠지면서 그 복귀주소로 되돌아 갑니다.
00401021 ADD ESP,8 ; add() 함수 호출을 위해 스택에 입력한 파라미터를 제거함
00401024 XOR EAX,EAX
00401026 MOV ESP,EBP ; ESP 복원 -> main() 스택 제거
00401028 POP EBP ; EBP 복원 (12FFC0)
00401029 RETN ; main() 함수 종료
여기서 또하나 눈여겨 보실 부분은 00401021 주소의 ADD ESP, 8 명령입니다.
이 명령의 의미는 add() 함수의 파라미터 크기 만큼 스택을 보정(정리)시키는 역할을 합니다.
이 스택 보정(정리) 명령을 수행하지 않으면 함수를 호출 할 때마다 스택에 파라미터들이 쌓여서
결국 스택 메모리가 다 차버리고 말 것입니다.
이렇게 함수 호출시 스택에 파라미터를 전달하고 함수가 리턴될 때
스택을 정리하는 방식에 대한 규약을 Calling Convention 이라고 합니다.
* Calling Convention 또한 리버싱에서 필수적으로 배워야 할 내용이며 다음 포스트에서 설명하도록 하겠습니다.
Epilogue
지금까지 스택 프레임에 대해서 알아보았습니다.
수시로 변경되는 ESP 레지스터 대신 EBP (베이스 포인터) 레지스터를 사용하여
로컬 변수, 파라미터, 복귀주소 등을 관리하는 방법입니다.
위 간단한 예제를 직접 디버깅 하면서 스택의 변동사항을 확인하면,
스택 프레임에 대해서 쉽게 이해할 수 있으실 겁니다.
'study' 카테고리의 다른 글
| PE(Portable Executable) File Format (8) - PE Header (12) | 2009/04/22 |
|---|---|
| PE(Portable Executable) File Format (7) - PE Header (37) | 2009/04/18 |
| PE(Portable Executable) File Format (6) - PE Header (57) | 2009/04/06 |
| PE(Portable Executable) File Format (5) - PE Header (24) | 2009/04/03 |
| PE(Portable Executable) File Format (4) - PE Header (21) | 2009/03/31 |
| PE(Portable Executable) File Format (3) - PE Header (16) | 2009/03/29 |
| PE(Portable Executable) File Format (2) - PE Header (29) | 2009/03/28 |
| PE(Portable Executable) File Format (1) - PE Header (51) | 2009/03/23 |
| Calling Convention - 함수 호출 규약 (28) | 2009/03/08 |
| Stack Frame (40) | 2009/03/03 |
| IA-32 Register 기본 설명 (45) | 2009/02/28 |
Trackback Address :: http://www.reversecore.com/trackback/11
- Tracked from www.reversecore.com 2009/03/11 05:42 삭제
Subject: Calling Convention
cdecl.exe stdcall.exe abstract Calling Convention (함수 호출 규약) 에 대해서 알아 보겠습니다. Calling Convention 우리 말로 '함수 호출 규약' 이라고 합니다. "함수를 호출할 때 파라미터를 어떤 식으로 전달하고, 스택을 어떻게 정리하는가" 에 대한 일종의 약속입니다. 우린 이미 함수 호출전에 파라미터를 스택을 통해서 전달한다는 것을 알았습니다. 스택이란 프로세스에서 정의된 메모리 공간이며 아..
stackframe.exex