1. 스택 프레임
- 스택 프레임이란 쉽게 말해서 ESP (스택 포인터) 가 아닌 EBP (베이스 포인터) 를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 말한다.
- 스택 프레임을 이용해서 함수 호출을 관리하면, 아무리 함수 호출 depth가 깊고 복잡해져도 스택을 완벽하게 관리할 수 있다.
1.1 ESP vs. EBP
● ESP
- ESP : 스택에 저장된 데이터의 최하위 주소값 (FILO)
- 최근에 스택에 저장된 데이터의 주소값
- PUSH or POP 명령에 의해서 가변적, 따라서 특정 데이터가 저장된 주소 값을 알기가 어렵다.
● EBP
- EBP : 함수에서 사용될 그택의 기준 주소 값
- 함수가 호출된 후, 종료될 때까지 변경되지 않는 주소 값
- 함수에서 스택에 저장된 데이터들의 주소값을 알려고 할 때 유용
1.2 스택 프레임 구조

2. stackframe.exe 실습
2.1 stackframe.cpp 소스코드
//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.2 stackframe.exe 디버깅

x32dbg로 stackframe.exe 파일을 열고 [Ctrl + G] 명령어로
401000 주소로 이동하자
(옆에 주석은 이해를 돕기 위해 부가설명을 추가해놓았다.)
코드 흐름 순서에 맞게 main() 함수부터 살펴보자
main()함수 <401020> 에 BP[F2]를 설치한후 [F9]를 눌러 실행해보자
● main() 함수 시작 & 스택 프레임 생성
stackframe.cpp 에서 아래 코드 부분에 해당되는 내용이다.
int main(int argc, char* argv[])
{


main() 함수 시작시 스택의 상태이다.
현재 ESP = 19FF2C이고, EBP = 19FF70 이다.
여기서 ESP <19FF2C>에 저장된 값 401250은 main()함수 실행이 끝난후 돌아갈 리턴주소이다.
main() 함수는 시작하자마자 스택 프레임을 생성 시킨다.

🔍 push ebp = EBP 값을 스택에 집어넣는다
→ main() 함수에서 EBP가 베이스 포인터의 역할을 하게 되므로
EBP가 이전에 가지고 있던 값을 스택에 백업해두기 위한 용도이다.
함수 가 종료(리턴)되기 전에 이값을 회복시켜준다.
🔍 mov ebp, esp = ESP 값을 EBP로 옮긴다.
→이 명령 이후 EBP는 현재 ESP와 같은 값을 가지게 되며,
main()함수가 끝날 때까지 EBP 값을 고정된다.
401020과 401021 주소의 두명령어에 의해 main()함수에 대한 스택 프레임이 생성되었다.
🔖 <401021>까지 실행했을때


현재 EBP 값은 19FF28로 ESP 와 동일하며,
19FF28 주소에는 19FF70 = [main()함수 시작할 떄 EBP가 가지고 있던 초기값]이 저장된걸 알 수 있다.
● 로컬 변수 세팅
stackframe.cpp 에서 아래 코드부분에 해당되는 내용이다.
long a = 1, b = 2;

🔍 sub esp, 8 = ESP값에서 8을뺀다
→ 로컬 변수 'a'와 'b'가 long 타입이므로 각각 4바이트 크기를 가지는데,
이 두 변수를 스택에 저장하기 위해서는 총 8바이트가 필요하기때문에
ESP 에서 8바이트를 빼서 두 변수에 필요한 메모리 공간을 확보한 것이다.
EBP 값은 main() 함수 내에서 고정이므로 이를 기준으로 삼아 로컬 변수에 접근할 수 있다.
🔍 mov dword ptr ss:[ebp-4], 1 = [EBP-4] 에 1을 넣는다
🔍 mov dword ptr ss:[ebp-8], 2 = [EBP-8] 에 2을 넣는다
🔖 <40102D>까지 실행했을때 스택 상태


ESP 의 값이 8 줄어들었고,
mov 명령어에 의해 19FF24 = [EBP-4] 에 1이, 19FF20 = [EBP-8] 에 2가 저장되었다.
● add() 함수 파라미터 입력 및 add() 함수 호출
stackframe.cpp 에서 아래 코드 부분에 해당되는 내용이다.
printf("%d\n", add(a, b));

🔍 mov eax, dword ptr ss:[ebp-8] = 변수 b를 스택에 넣는다.
🔍 push eax = EAX 값을 스택에 집어넣는다
🔍 mov eax, dword ptr ss:[ebp-4] = 변수 a를 스택에 넣는다.
🔍 push ecx = ECX 값을 스택에 집어넣는다
→ add() 함수는 파라미터로 a 와 b 를 받는다.
※ 여기서 중요한점 : 파라미터가 C 언어의 입력 순서와 반대로 스택에 저장된다.
🔍 call <stackframe.sub_40100> = add() 함수 호출
🔖 <40102D>까지 실행했을때 스택 상태


EAX 에 파라미터 b 값인 2가 저장되었고,
ECX에 파라미터 a의 값인 1이 저장 되었다.
또한 add() 함수의 복귀 주소인 401041이 스택에 저장된 것을 확인할 수 있다.
● add() 함수 시작 & 스택 프레임 생성
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
long add(long a, long b)
{
add() 함수가 시작되면 자시만의 스택 프레임을 따로 생성한다.

※ 메인 함수와 똑같으므로 설명은 생략하겠다.
🔖 <401001>까지 실행했을때 스택 상태


원래의 EBP값(19FF28 = main()의 EBP값) 을 스택에 백업한 후,
EBP는 19FF10으로 새롭게 세팅된 것을 확인할 수 있다.
● add() 함수의 로컬 변수(x, y) 세팅
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
long x =a, y = b;
add() 함수의 로컬 변수 x, y 에 각각 파라미터 a, b 를 대입한다.

🔍 sub esp, 8 = ESP값에서 8을뺀다
→ 로컬 변수 x, y 에 대한 스택 메모리영역을 확보
🔍 <401006> ~ <40100F>
→ 주석 참고
🔖 <40100F>까지 실행했을때 스택 상태


● ADD 연산
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
return (x + y);

🔍 mov eax, dword ptr ss:[ebp-8] = 변수 x의값([ebp-8])을 EAX에 넣는다.
🔍add eax, dword ptr ss:[ebp-4] = EAX에 변수 y의값([ebp-4])을 더한다.
※ EAX는 산술연산 및 리턴 값으로 사용된다.
위와 같이 함수가 리턴하기 직전에 EAX에 값을 입력하면 그대로 리턴값이 된다.
🔖 <401015>까지 실행했을때 스택 상태
이 연산 과정에서 스택은 변하지 않았기 때문에 직전의 스택 상태와 동일하다.
● add() 함수의 스택 프레임 해제 & 함수 종료(리턴)
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
return (x + y);
}
이제 add() 함수가 리턴하기위해 add() 함수의 스택 프레임을 해제해야한다.

🔍 mov esp, epb = 현재 EBP 값을 ESP에 대입
→ 앞에서 실행된 401001주소의 mov ebp, esp 에 대응하는 명령어이다.
즉 add() 함수가 시작할 때의 ESP(19FF14)값을 EBP에 넣어두었다가 함수가 종료될 때
ESP를 원래대로 복원시키는 목적으로 사용한다.
※위 명령에 의해 401003 주소의 sub esp,8 명령의 효과는 사라진다.
즉 add() 함수의 로컬 변수 x, y 는 더이상 유요하지 않다.
🔍 pop ebp = add() 함수가 시작하면서 스택에 백업한 EBP 값 복원
→ 앞에서 실행된 401000 주소의 push ebp 명령어에 대응하는 명령어이다.
복원된 EBP 값은 19FF28 이며, 이값은 main() 함수의 EBP 값이다.
이제 add() 함수의 스택 프레임은 해제 되었다.
🔍 ret = 스택에 저장된 복귀 주소로 리턴한다.
🔖 <401015>까지 실행했을때 스택 상태

add() 함수를 호출하기 전의 스택 상태로 완벽히 돌아온 것을 알 수 있다.
※ 프로그램은 이런식으로 스택을 관라하기 때문에
함수 호출이 계속 중첩되어도 스택이 깨지지 않고 잘 유지되는 것이다.
하지만 스택에 로컬 변수, 함수 파라미터, 리턴 주소 등을
한번에 보관하기 때문에
문자열 함수 취약점 등을 이용한 Stack Buffer Overflower 기법에 취약하다.
● add() 함수 파라미터 제거(스택 정리)
이제 main() 함수 코드로 돌아왔다.

🔍 add esp, 8 = ESP에 8을 더함
→ add() 함수가 완전히 종료되었기 때문에 add() 함수에 넘겨주었던
파라미터 a, b 는 더이상 필요 없기때문에
ESP 에 8을 더하여 스택을 정리하는 것이다.
🔖 <401041>까지 실행했을때 스택 상태

● printf() 함수 호출
stackfaame.cpp에서 아래 코드 부분에 해당되는 내용이다.
printf("%d\n", add(a, b));
printf() 함수의 호출 코드이다.

🔍 push eax = add() 함수에 저장된 리턴값(3)이 들어있다.
🔍 call <stackframe.sub_401067> = printf() 함수 호출
🔍 add esp, 8 = ESP에 8을 더함
→ printf() 함수의 파라미터는 2개이며 크기는 8바이트이다
따라서 40104F 주소에 add esp, 8 명령어로 스택에서 함수 파라미터를 정리하고있다.
🔖 <401041>까지 실행했을때 스택 상태
printf() 함수 호출후 스택이 정리되었기 때문에 스택언 전과 동일하다.
● 리턴 값 세팅
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
return 0;
main() 함수의 리턴값(0) 을 세팅한다.

🔍 xor eax, eax = Exlusive OR bit 연산 실행
→ 같은 값끼리 XOR하면 0이 되는 특징이 있다.
mov eax, 0 명령어보다 실행 속도가 빨라서 위와같이 레지스터 초기화시킬때 많이 사용된다.
● 스택 프레임 해제 & main() 함수 종료
stackframe.cpp 에서 아래 코드 부분에 해당되는 내용이다.
return 0;
}
main() 함수가 종료된다. add() 함수와 마찬가지로 리턴하기 전에 스택프레임을 해제한다.

위 두명령으로 main() 함수의 스택 프레임은 해제 되었다.
또한 main() 함수의 로컬 변수인 a, b역시 더이상 유효하지 않다.
🔖 <401056>까지 실행했을때 스택 상태

main() 함수를 시작할때의 스택 모습과 완벽히 동일하다.
'Reverse Engineernig > study' 카테고리의 다른 글
[리버싱 핵심원리] ch.10 함수 호출 규약 (0) | 2023.01.03 |
---|---|
[리버싱 핵심원리] ch08. abex' crackme2 (0) | 2022.12.30 |
[리버싱 핵심원리] ch06. abex' crackeme #1 분석 (0) | 2022.12.28 |
[리버싱 핵심원리] ch05. 스택 (0) | 2022.12.27 |
[리버싱 핵심원리] ch04. IA-32 Register 기본 설명 (0) | 2022.12.27 |
1. 스택 프레임
- 스택 프레임이란 쉽게 말해서 ESP (스택 포인터) 가 아닌 EBP (베이스 포인터) 를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 말한다.
- 스택 프레임을 이용해서 함수 호출을 관리하면, 아무리 함수 호출 depth가 깊고 복잡해져도 스택을 완벽하게 관리할 수 있다.
1.1 ESP vs. EBP
● ESP
- ESP : 스택에 저장된 데이터의 최하위 주소값 (FILO)
- 최근에 스택에 저장된 데이터의 주소값
- PUSH or POP 명령에 의해서 가변적, 따라서 특정 데이터가 저장된 주소 값을 알기가 어렵다.
● EBP
- EBP : 함수에서 사용될 그택의 기준 주소 값
- 함수가 호출된 후, 종료될 때까지 변경되지 않는 주소 값
- 함수에서 스택에 저장된 데이터들의 주소값을 알려고 할 때 유용
1.2 스택 프레임 구조

2. stackframe.exe 실습
2.1 stackframe.cpp 소스코드
//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.2 stackframe.exe 디버깅

x32dbg로 stackframe.exe 파일을 열고 [Ctrl + G] 명령어로
401000 주소로 이동하자
(옆에 주석은 이해를 돕기 위해 부가설명을 추가해놓았다.)
코드 흐름 순서에 맞게 main() 함수부터 살펴보자
main()함수 <401020> 에 BP[F2]를 설치한후 [F9]를 눌러 실행해보자
● main() 함수 시작 & 스택 프레임 생성
stackframe.cpp 에서 아래 코드 부분에 해당되는 내용이다.
int main(int argc, char* argv[])
{


main() 함수 시작시 스택의 상태이다.
현재 ESP = 19FF2C이고, EBP = 19FF70 이다.
여기서 ESP <19FF2C>에 저장된 값 401250은 main()함수 실행이 끝난후 돌아갈 리턴주소이다.
main() 함수는 시작하자마자 스택 프레임을 생성 시킨다.

🔍 push ebp = EBP 값을 스택에 집어넣는다
→ main() 함수에서 EBP가 베이스 포인터의 역할을 하게 되므로
EBP가 이전에 가지고 있던 값을 스택에 백업해두기 위한 용도이다.
함수 가 종료(리턴)되기 전에 이값을 회복시켜준다.
🔍 mov ebp, esp = ESP 값을 EBP로 옮긴다.
→이 명령 이후 EBP는 현재 ESP와 같은 값을 가지게 되며,
main()함수가 끝날 때까지 EBP 값을 고정된다.
401020과 401021 주소의 두명령어에 의해 main()함수에 대한 스택 프레임이 생성되었다.
🔖 <401021>까지 실행했을때


현재 EBP 값은 19FF28로 ESP 와 동일하며,
19FF28 주소에는 19FF70 = [main()함수 시작할 떄 EBP가 가지고 있던 초기값]이 저장된걸 알 수 있다.
● 로컬 변수 세팅
stackframe.cpp 에서 아래 코드부분에 해당되는 내용이다.
long a = 1, b = 2;

🔍 sub esp, 8 = ESP값에서 8을뺀다
→ 로컬 변수 'a'와 'b'가 long 타입이므로 각각 4바이트 크기를 가지는데,
이 두 변수를 스택에 저장하기 위해서는 총 8바이트가 필요하기때문에
ESP 에서 8바이트를 빼서 두 변수에 필요한 메모리 공간을 확보한 것이다.
EBP 값은 main() 함수 내에서 고정이므로 이를 기준으로 삼아 로컬 변수에 접근할 수 있다.
🔍 mov dword ptr ss:[ebp-4], 1 = [EBP-4] 에 1을 넣는다
🔍 mov dword ptr ss:[ebp-8], 2 = [EBP-8] 에 2을 넣는다
🔖 <40102D>까지 실행했을때 스택 상태


ESP 의 값이 8 줄어들었고,
mov 명령어에 의해 19FF24 = [EBP-4] 에 1이, 19FF20 = [EBP-8] 에 2가 저장되었다.
● add() 함수 파라미터 입력 및 add() 함수 호출
stackframe.cpp 에서 아래 코드 부분에 해당되는 내용이다.
printf("%d\n", add(a, b));

🔍 mov eax, dword ptr ss:[ebp-8] = 변수 b를 스택에 넣는다.
🔍 push eax = EAX 값을 스택에 집어넣는다
🔍 mov eax, dword ptr ss:[ebp-4] = 변수 a를 스택에 넣는다.
🔍 push ecx = ECX 값을 스택에 집어넣는다
→ add() 함수는 파라미터로 a 와 b 를 받는다.
※ 여기서 중요한점 : 파라미터가 C 언어의 입력 순서와 반대로 스택에 저장된다.
🔍 call <stackframe.sub_40100> = add() 함수 호출
🔖 <40102D>까지 실행했을때 스택 상태


EAX 에 파라미터 b 값인 2가 저장되었고,
ECX에 파라미터 a의 값인 1이 저장 되었다.
또한 add() 함수의 복귀 주소인 401041이 스택에 저장된 것을 확인할 수 있다.
● add() 함수 시작 & 스택 프레임 생성
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
long add(long a, long b)
{
add() 함수가 시작되면 자시만의 스택 프레임을 따로 생성한다.

※ 메인 함수와 똑같으므로 설명은 생략하겠다.
🔖 <401001>까지 실행했을때 스택 상태


원래의 EBP값(19FF28 = main()의 EBP값) 을 스택에 백업한 후,
EBP는 19FF10으로 새롭게 세팅된 것을 확인할 수 있다.
● add() 함수의 로컬 변수(x, y) 세팅
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
long x =a, y = b;
add() 함수의 로컬 변수 x, y 에 각각 파라미터 a, b 를 대입한다.

🔍 sub esp, 8 = ESP값에서 8을뺀다
→ 로컬 변수 x, y 에 대한 스택 메모리영역을 확보
🔍 <401006> ~ <40100F>
→ 주석 참고
🔖 <40100F>까지 실행했을때 스택 상태


● ADD 연산
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
return (x + y);

🔍 mov eax, dword ptr ss:[ebp-8] = 변수 x의값([ebp-8])을 EAX에 넣는다.
🔍add eax, dword ptr ss:[ebp-4] = EAX에 변수 y의값([ebp-4])을 더한다.
※ EAX는 산술연산 및 리턴 값으로 사용된다.
위와 같이 함수가 리턴하기 직전에 EAX에 값을 입력하면 그대로 리턴값이 된다.
🔖 <401015>까지 실행했을때 스택 상태
이 연산 과정에서 스택은 변하지 않았기 때문에 직전의 스택 상태와 동일하다.
● add() 함수의 스택 프레임 해제 & 함수 종료(리턴)
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
return (x + y);
}
이제 add() 함수가 리턴하기위해 add() 함수의 스택 프레임을 해제해야한다.

🔍 mov esp, epb = 현재 EBP 값을 ESP에 대입
→ 앞에서 실행된 401001주소의 mov ebp, esp 에 대응하는 명령어이다.
즉 add() 함수가 시작할 때의 ESP(19FF14)값을 EBP에 넣어두었다가 함수가 종료될 때
ESP를 원래대로 복원시키는 목적으로 사용한다.
※위 명령에 의해 401003 주소의 sub esp,8 명령의 효과는 사라진다.
즉 add() 함수의 로컬 변수 x, y 는 더이상 유요하지 않다.
🔍 pop ebp = add() 함수가 시작하면서 스택에 백업한 EBP 값 복원
→ 앞에서 실행된 401000 주소의 push ebp 명령어에 대응하는 명령어이다.
복원된 EBP 값은 19FF28 이며, 이값은 main() 함수의 EBP 값이다.
이제 add() 함수의 스택 프레임은 해제 되었다.
🔍 ret = 스택에 저장된 복귀 주소로 리턴한다.
🔖 <401015>까지 실행했을때 스택 상태

add() 함수를 호출하기 전의 스택 상태로 완벽히 돌아온 것을 알 수 있다.
※ 프로그램은 이런식으로 스택을 관라하기 때문에
함수 호출이 계속 중첩되어도 스택이 깨지지 않고 잘 유지되는 것이다.
하지만 스택에 로컬 변수, 함수 파라미터, 리턴 주소 등을
한번에 보관하기 때문에
문자열 함수 취약점 등을 이용한 Stack Buffer Overflower 기법에 취약하다.
● add() 함수 파라미터 제거(스택 정리)
이제 main() 함수 코드로 돌아왔다.

🔍 add esp, 8 = ESP에 8을 더함
→ add() 함수가 완전히 종료되었기 때문에 add() 함수에 넘겨주었던
파라미터 a, b 는 더이상 필요 없기때문에
ESP 에 8을 더하여 스택을 정리하는 것이다.
🔖 <401041>까지 실행했을때 스택 상태

● printf() 함수 호출
stackfaame.cpp에서 아래 코드 부분에 해당되는 내용이다.
printf("%d\n", add(a, b));
printf() 함수의 호출 코드이다.

🔍 push eax = add() 함수에 저장된 리턴값(3)이 들어있다.
🔍 call <stackframe.sub_401067> = printf() 함수 호출
🔍 add esp, 8 = ESP에 8을 더함
→ printf() 함수의 파라미터는 2개이며 크기는 8바이트이다
따라서 40104F 주소에 add esp, 8 명령어로 스택에서 함수 파라미터를 정리하고있다.
🔖 <401041>까지 실행했을때 스택 상태
printf() 함수 호출후 스택이 정리되었기 때문에 스택언 전과 동일하다.
● 리턴 값 세팅
stackframe.cpp에서 아래 코드 부분에 해당되는 내용이다.
return 0;
main() 함수의 리턴값(0) 을 세팅한다.

🔍 xor eax, eax = Exlusive OR bit 연산 실행
→ 같은 값끼리 XOR하면 0이 되는 특징이 있다.
mov eax, 0 명령어보다 실행 속도가 빨라서 위와같이 레지스터 초기화시킬때 많이 사용된다.
● 스택 프레임 해제 & main() 함수 종료
stackframe.cpp 에서 아래 코드 부분에 해당되는 내용이다.
return 0;
}
main() 함수가 종료된다. add() 함수와 마찬가지로 리턴하기 전에 스택프레임을 해제한다.

위 두명령으로 main() 함수의 스택 프레임은 해제 되었다.
또한 main() 함수의 로컬 변수인 a, b역시 더이상 유효하지 않다.
🔖 <401056>까지 실행했을때 스택 상태

main() 함수를 시작할때의 스택 모습과 완벽히 동일하다.
'Reverse Engineernig > study' 카테고리의 다른 글
[리버싱 핵심원리] ch.10 함수 호출 규약 (0) | 2023.01.03 |
---|---|
[리버싱 핵심원리] ch08. abex' crackme2 (0) | 2022.12.30 |
[리버싱 핵심원리] ch06. abex' crackeme #1 분석 (0) | 2022.12.28 |
[리버싱 핵심원리] ch05. 스택 (0) | 2022.12.27 |
[리버싱 핵심원리] ch04. IA-32 Register 기본 설명 (0) | 2022.12.27 |