본문 바로가기
Reverse Engineering/리버싱 핵심 원리

1부 Stack Frame (스택 프레임)

by Ken out of ken 2025. 1. 1.

Description


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

 

ESP는 함수가 실행되는 동안 계속 변경이 된다

함수 내부에서 새로운 데이터를 스택에 push하거나 기존 데이터를 pop 하는 작업이 빈번하다
그렇기에 위치가 고정되어 있는 EBP를 기준으로 하는 것이 특정 변수나 파라미터에 접근하기 더 쉽다

 


StackFrame 예제

 

Ctrl + G 로 401000 함수 위치로 가라고 한다

 

해당 함수와 그 어셈블리어는 다음과 같다

 

// StackFrame.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;
}

 

00401000 | 55                   | push ebp                        | 기존 EBP 값을 스택에 저장 (이전 스택 프레임 보존)
00401001 | 8BEC                 | mov ebp,esp                     | 현재 ESP 값을 EBP로 복사 (새 스택 프레임 설정)
00401003 | 83EC 08              | sub esp,8                       | 스택 포인터를 8바이트 감소 (지역 변수 공간 8바이트 확보)
00401006 | 8B45 08              | mov eax,dword ptr ss:[ebp+8]    | 첫 번째 매개변수[EBP+8]를 EAX에 로드
00401009 | 8945 F8              | mov dword ptr ss:[ebp-8],eax    | 첫 번째 매개변수를 지역 변수([EBP-8])로 저장
0040100C | 8B4D 0C              | mov ecx,dword ptr ss:[ebp+C]    | 두 번째 매개변수[EBP+12]를 ECX에 로드
0040100F | 894D FC              | mov dword ptr ss:[ebp-4],ecx    | 두 번째 매개변수를 지역 변수[EBP-4]로 저장
00401012 | 8B45 F8              | mov eax,dword ptr ss:[ebp-8]    | 지역 변수[EBP-8]에서 첫 번째 매개변수를 EAX로 로드
00401015 | 0345 FC              | add eax,dword ptr ss:[ebp-4]    | 첫 번째 매개변수EAX와 두 번째 매개변수[EBP-4]를 더함
00401018 | 8BE5                 | mov esp,ebp                     | ESP를 EBP로 복원 (스택 포인터 초기화)
0040101A | 5D                   | pop ebp                         | 이전 EBP 값을 스택에서 복원
0040101B | C3                   | ret                             | 호출자 함수로 반환




00401020 | 55                   | push ebp                        | 기존 EBP 값을 스택에 저장 (스택 프레임 생성 시작)
00401021 | 8BEC                 | mov ebp,esp                     | 현재 ESP 값을 EBP로 복사 (새 스택 프레임 설정)
00401023 | 83EC 08              | sub esp,8                       | 스택에서 8바이트 공간 확보 (지역 변수 a와 b용)
00401026 | C745 FC 01000000     | mov dword ptr ss:[ebp-4],1      | 지역 변수 a에 1 저장 [EBP-4]
0040102D | C745 F8 02000000     | mov dword ptr ss:[ebp-8],2      | 지역 변수 b에 2 저장 [EBP-8]
00401034 | 8B45 F8              | mov eax,dword ptr ss:[ebp-8]    | 지역 변수 b 값을 EAX에 로드
00401037 | 50                   | push eax                        | 매개변수로 b를 푸시
00401038 | 8B4D FC              | mov ecx,dword ptr ss:[ebp-4]    | 지역 변수 a 값을 ECX에 로드
0040103B | 51                   | push ecx                        | 매개변수로 a를 푸시
0040103C | E8 BFFFFFFF          | call stackframe.401000          | add(a, b) 함수 호출
00401041 | 83C4 08              | add esp,8                       | 매개변수로 사용한 스택 공간 8바이트 해제
00401044 | 50                   | push eax                        | add(a, b) 결과(EAX)를 푸시
00401045 | 68 84B34000          | push stackframe.40B384          | 문자열 주소("%d\n")를 푸시
0040104A | E8 18000000          | call stackframe.401067          | printf("%d\n", add(a, b)) 호출
0040104F | 83C4 08              | add esp,8                       | printf 호출 시 사용한 스택 공간 8바이트 해제
00401052 | 33C0                 | xor eax,eax                     | EAX를 0으로 설정 return 0;
00401054 | 8BE5                 | mov esp,ebp                     | ESP를 EBP로 복원 (스택 프레임 해제)
00401056 | 5D                   | pop ebp                         | 이전 EBP 값을 스택에서 복원
00401057 | C3                   | ret                             | 호출자 함수로 반환

 

 

함수가 시작하자마자 바로 EBP레지스터에 ESP레지스터 값을 넣어 고정시키는 것을 볼 수 있다

또한 Stack만의 특징인 주소값에서 뺄셈을 통해 공간을 확보하는 것도 볼 수 있다 ( 스택은 위에서 아래로, 높은 수에서 낮은 수로 쌓인다 )


Calling Convention (함수 호출 규약) feat) Callee, Caller

00401020 | 55                   | push ebp                        | 
00401021 | 8BEC                 | mov ebp,esp                     | 
00401023 | 83EC 08              | sub esp,8                       | 
00401026 | C745 FC 01000000     | mov dword ptr ss:[ebp-4],1      | 
0040102D | C745 F8 02000000     | mov dword ptr ss:[ebp-8],2      | 
00401034 | 8B45 F8              | mov eax,dword ptr ss:[ebp-8]    | 
00401037 | 50                   | push eax                        | 
00401038 | 8B4D FC              | mov ecx,dword ptr ss:[ebp-4]    |
0040103B | 51                   | push ecx                        | 
0040103C | E8 BFFFFFFF          | call stackframe.401000          | 
00401041 | 83C4 08              | add esp,8                       | 매개변수로 사용한 스택 공간 8바이트 해제
00401044 | 50                   | push eax                        | 
00401045 | 68 84B34000          | push stackframe.40B384          | 
0040104A | E8 18000000          | call stackframe.401067          | 
0040104F | 83C4 08              | add esp,8                       | printf 호출 시 사용한 스택 공간 8바이트 해제
00401052 | 33C0                 | xor eax,eax                     | 
00401054 | 8BE5                 | mov esp,ebp                     | 
00401056 | 5D                   | pop ebp                         | 
00401057 | C3                   | ret                             |

 

특이하게도 함수를 호출하고 난 뒤에 add esp,* 을 통해 스택 프레임을 정리하는 것을 볼 수 있다

이는 함수 호출 규약과 관련이 있다

 

함수를 호출할 때 파라미터를 어떤 식으로 전달하는가? 

스택은 프로세스에서 정의된 메모리 공간으로 PE 헤더에 그 크기가 명시된다

즉 프로세스 실행시 스택 메모리 크기가 결정됨 함수가 실행 완료 되었을 때 스택에 들어있는 파라미터는 정리하지 않는데, 이는 지우는 데에 자원을 소모할 이유가 없기 때문이다

( 어차피 다음 번에 스택에 다른 값을 입력할 때 저절로 덮어쓰는 데다 고정되어 있어서 메모리 해제를 할 수도 없고 할 필요도 없다 )

 

ESP(스택 포인터)는? 함수 호출 전으로 복원 -> 참조 가능한 스택의 크기가 줄어들지 않는다 


종류

  • cdecl
    • C 언어에서 사용되는 방식으로, Caller에서 스택을 정리한다
    • 가변 길이 파라미터를 전달할 수 있다는 장점이 있다
  • stdcall 
    • Callee 에서 스택을 정리하는 방식
    • Callee 내부에 스택 정리 코드가 존재하므로
      함수 호출시마다 ADD ESP, XXX 명령을 써줘야 하는 cdecl 방식에 비해 코드 크기가 작아진다
    • Win32 API는 C 언어로 된 라이브러리 이지만 기본 cdecl 방식이 아닌 stdcall 방식을 채용한다
      ( C 이외의 다른 언어에서 API를 직접 호출시 호환성을 위해서= )
  • fastcall 
    • stdcall 방식과 같으나 함수에 전달하는 파리미터의 일부 (2개까지)를 스택 메모리가 아닌 레지스터를 이용하여 전달한다
    • 어떤 함수의 파라미터가 4개면 앞의 두 파라미터는 각각 ECS, EDX 파라미터를 이용하여 전달
      좀 더 빠른 함수 호출이 가능하나, ECX, EDX 레지스터를 관리하는 추가적인 오버헤드가 필요한 경우가 존재
      또한 함수내용이 복잡하여 ECX, EDX 레지스터를 다른 용도로 사용할 필요가 있는 경우 파라미터 값을 어딘가에 따로 저장해줘야 한다

P.S.

요즘 컴파일러에서는 성능 최적화를 위해 스택 프레임 포인터를 생략하는 경우가 많다

이 경우 EBP를 일반 레지스터로 활용되고, 스택의 참조는 ESP와 오프셋 계산으로 대체된다