스택프레임

1. 스택프레임

  • ESP (스택 포인터)가 아닌 EBP (베이스 포인터) 레지스터를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법
  • ESP의 레지스터의 값은 프로그램 안에서 수시로 변경
    • ESP값을 기준으로 하면 스택에 저장된 변수, 파라미터에 접근을 하기 어려움이 있음
  • ESP 값을 EBP에 저장하고 이를 함수 내에서 유지해 아무리 ESP 값이 바뀌어도 EBP를 기준으로 해당 함수의 변수, 파라미터, 복귀 주소에 접근 가능

2. 스택 프레임의 구조

PUSH EBP ; 함수 시작 (EBP 값을 저장)
MOV EBP, ESP ; 현재 ESP 값을 EBP에 저장
(함수 본체) ; EBP의 값은 변하지않음 -> 로컬 변수와 파라미터 엑세스 가능
MOV ESP, EBP ; ESP 값 복구
POP EBP ; 리턴되기 전에 저장한 EBP 값 복원
RETN ; 함수 종료
  • 스택 프레임을 이용해서 함수 호출을 관리하면, 함수 호출 depth가 깊고 복잡해도 스택을 완벽하게 관리 가능

+) 최신 컴파일러는 최적화 옵션을 가지고 있어 간단한 함수는 스택 프레임을 생성하지 않음

+) 스택에 복귀 주소가 저장된다는 점은 보안 취약점으로 작용할 수 있음

3. StackFrame.exe

1) 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;
}

2) main() 함수 시작 & 스택 프레임 생성

  • [Ctrl+G] 401000주소로 이동

  • main() 함수(401020)에 BP(Break Point) 설정 후 실행[F9]

  • 현재 EBP = 19FF70, ESP=19FF2C
00401020 | push EBP ; 값을 스택에 집어넣는 명령어
  • main() 함수는 시작하자마자 스택 프레임을 생성
  • EBP 값을 스택에 집어넣어라
    • main() 함수에서 EBP가 베이스 포인터의 역할
    • EBP가 이전에 가지고 있는 값을 스택에 백업
    • main() 함수 종료되기 전에 EBP에 백업한 값을 회복시킴
	00401021 | MOV EBP, ESP ; ESP의 값을 EBP로 옮겨라
  • EBP는 현재 ESP와 같은 값을 가짐
  • main() 함수 종료 전까지 EBP 값은 고정

  • 현재 EBP 값은 19FF28로 ESP와 동일
  • 19FF28에는 19FF70이라는 값이 저장
  • 19FF70은 EBP가 main() 함수 시작 시 EBP가 가지고 있던 초기 값

3) 로컬 변수 세팅

long a = 1, b = 2;
00401023 | SUB ESP, 8 ; ESP 값에서 8을 빼라
  • ESP에서 8을 빼는 이유
    • 함수의 로컬 변수는 스택에 저장
    • main() 함수의 로컬 변수 ‘a’, ‘b’ → long 타입으로 각각 4바이트의 크기를 가짐
    • 두 변수를 스택에 저장하기 위한 공간으로 총 8바이트 필요
    • 두 변수에게 필요한 공간을 확보하기 위해 ESP에서 8을 빼준 것
00401026 | MOV DWORD PTR SS:[EBP-4],1 ; [EBP-4]에 1을 넣어라
0040102D | MOV DWORD PTR SS:[EBP-8],2 ; [EBP-8]에 2를 넣어라
  • C언어의 포인어와 같은 개념

어셈블리 C언어 Type casting

DWORD PTR SS:[EBP-4] *(DWORD *)(EBP-4) DWORD (4바이트)
WORD PTR SS:[EBP-4] *(WORD *)(EBP-4) WORD (2바이트)
BYTE PTR SS:[EBP-4] *(BYTE *)(EBP-4) BYTE
  • [EBP-4]는 로컬 변수 a를 의미, [EBP-8]은 로컬 변수 b를 의미

 💡 SS(Stack Segment) 표시 이유

  • 해당 메모리가 어떤 세그먼트에 소속되어 있는 지 표시
  • ESP, EBP는 스택을 가리키는 레지스터로 SS 레지스터를 붙여주는 것
  • SS(Stack segment), DS(Data segment), ES(Extra data segment)의 값은 모두 0

  • 해당 명령어 실행 후 ESP, EBP의 값

  • 스택에 1과 2가 저장되어있음

4) add() 함수 파라미터 입력 및 add() 함수 호출

printf("%d\\n",add(a,b)); // main문에서 add() 함수 호출
00401034 | MOV EAX, DWORD PTR SS:[EBP-8] ; 변수 b
00401037 | PUSH EAX ; 파라미터에 b 저장
00401038 | MOV ECX, DWORD PTR SS:[EBP-4] ; 변수 a
0040103B | PUSH ECX ; 파라미터에 a 저장
0040103C | CALL StackFra. 00401000 ; add() 함수 호출
  • 위 어셈블리 코드는 전형적인 함수 호출 과정
  • C언어 소스코드 입력 순서와는 반대로 스택에 저장 (파라미터 역순 저장) → 변수 b 저장 후 변수 a 저장
  • add() 함수 안으로 들어간 이후 스택 변화

  • 복귀 주소
    • CALL 명령어가 실행되어 해당 함수로 들어가기 전에 CPU는 무조건 해당 함수가 종료될 때 복귀할 주소를 스택에 저장
     

  • add() 함수 호출 이후 주소를 스택에 저장 (00401041 값이 스택에 저장) → 복귀주소 저장

5) add() 함수 시작 & 스택 프레임 생성

long add(long a, long b)
00401000 | PUSH EBP
00401001 | MOV EBP, ESP
  • main() 함수의 스택 프레임 생성과 동일
  • EBP 값을 스택에 저장 후 현재 ESP를 EBP에 입력
  • add() 함수 내에서 EBP값은 고정

  • main() 함수에서 사용되는 EBP값 스택에 백업 후 EBP가 19FF10으로 세팅

6) add() 함수의 로컬 변수 세팅

long x = a, y = b;
00401003 | SUB ESP, 8 ; 로컬 변수 x, y 공간 세팅
00401006 | MOV EAX, DWORD PTR SS:[EBP+8] ; [EBP+8] = PARAM a
00401009 | MOV DWORD PTR SS:[EBP-8], EAX ; [EBP-8] = LOCAL x
0040100C | MOV ECX, DWORD PTR SS:[EBP+C] ; [EBP+C] = PARAM b
0040100F | MOV DWORD PTR SS:[EBP-4], ECX ; [EBP-4] = LOCAL y
  • 로컬 변수 x, y의 공간을 세팅한 후 파라미터로 받아온 a, b의 값을 x, y에 넘겨줌

  • 해당 명령 실행 후 스택 변화

7) add 연산

return (x+y);
00401012 | MOV EAX, DWORD PTR SS:[EBP-8] ; 변수 x의 값을 EAX에 넣음
00401015 | ADD EAX, DWORD PTR SS:[EBP-4] ; EAX에 변수 y의 값을 더함
  • EAX는 범용 레지스터로 산술 연산에 사용 / 리턴 값을 사용
  • 해당 연산에서 스택의 변화 X

8) add() 함수의 스택 프레임 해제 & 함수 종료 (리턴)

00401018 | MOV ESP,EBP ; 현재 EBP값을 ESP에 대입 -> add() 함수의 명령의 효과는 사라짐
0040101A | POP EBP ; EBP의 값이 복원 -> 해당 주소로 이동 -> main() 함수로 이동

  • 스택 변화 모습
  • ESP = 19FF14, 주소 값은 401041 (CALL 401000 명령에서 CPU가 스택에 저장한 복귀 주소)
0040101B | RETN ; 스택에 저장된 복귀 주소로 리턴

  • add() 함수 호출 이전의 스택 상태로 완전히 돌아옴
  • 스택 프레임을 통해 스택을 관리하여 함수 호출이 충첩되더라도 스택이 깨지지 않고 유지

9) add() 함수 파라미터 제거 (스택 정리)

00401041 | ADD ESP, 8 ; ESP에 8을 더하여 스택을 정리
  • 파라미터 a, b가 필요없어 해당 값을 스택에서 정리

  • 스택 변화 모습
  • 함수 호출 규약 (Calling Convention)
    • cdecl : 함수를 호출한 쪽에서 파라미터 정리
    • stdcall : 호출당한 쪽에서 파라미터 정리

10) printf() 함수 호출

printf("%d\\n",add(a,b));
00401044 | PUSH EAX ; add() 함수에서 리턴된 3을 파라미터로 저장
00401045 | PUSH stackframe.0040B384 
0040104A | CALL stackframe.00401067 ; printf함수 호출
0040104F | ADD ESP, 8 ; 함수 파라미터 정리
  • 함수 호출 후 스택 정리하여 스택의 상태는 동일

11) 리턴 값 세팅

return 0;
00401052 | XOR EAX, EAX
  • 레지스터를 초기화할때 많이 사용

12) 스택 프레임 해제 & main() 함수 종료

00401054 | MOV ESP, EBP
00401056 | POP EBP
  • add 함수와 마찬가지로 리턴하기 이전에 스택 프레임 해제

  • 스택 변화 모습
000401057 | RETN
  • main() 함수 종료 → 리턴 주소 401250으로 점프
  • 해당 주소는 Stub Code 영역
  • 이후 프로세스 종료 코드 실행

'Security > Reversing' 카테고리의 다른 글

PE File Format Advance  (1) 2023.07.08
Calling Convention (함수 호출 규약)  (0) 2023.07.06
PE파일  (1) 2023.07.03
어셈블리어  (0) 2023.07.02
CPU 레지스터  (0) 2023.07.02