해보자고

[리버싱 핵심 원리] 7장 정리 본문

리버싱/리버싱 핵심 원리

[리버싱 핵심 원리] 7장 정리

초코맛동산 2024. 1. 6. 11:55

# 학습 목표

- 7장 :  스택 프레임


7. 스택 프레임

7.1 스택 프레임

함수의 시작과 같은 어떤 기준 시점의 ESP 값을 EBP에 저장하고 이를 함수 내에서 유지해주면, 자주 변하는 ESP 값에도 EBP를 기준으로 안전하게 함수의 변수, 파라미터, 복귀 주소에 접근할 수 있다.

스택 프레임을 활용하여 함수 호출을 관리하면 함수 호출 depth가 깊고 복잡해도 스택을 완벽하게 관리할 수 있다. 

 

 

7.2 StackFrame.exe 예제

 

7.2.0 간단한 설정

+) 간단한 함수의 경우 스택 프레임을 생성하지 않는 최적화 옵션이 있으니 이를 빌드 전에 꺼준다. (디버그 - 속성 - 구성 속성 - C/C++ - 최적화 )

 

+) ollydbg 에 올렸을 때 401000 주소가 없는 경우에는 주소를 계속해서 바꿔주는 임의 기준 주소가 '예'일 수 있다. 임의 기준 주소를 '아니오'로 설정 바꿔준다. (디버그 - 속성 - 구성 속성 - 링커 - 고급)

 

+) 디버그 모드로 빌드하지 말고, Release 모드로 빌드하기. 디버그 모드랑 교재의 코드 등이 사뭇 다를 수 있다. 

 

7.2.1 main 함수 찾기 

- 1장&2장에서 말했듯 초보자인 나는 일단 주석 및 라벨 등을 참고하여 노가다로 F8 명령어를 눌러가며 찾아다녔고, main 함수로 의심되는 주소를  찾아 F7을 해보았다. 다행이 교재와 같이 main 함수였고 BP를 걸어준다.

 

 

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

int main(int argc, char* argv[])
{

- 초기 ESP와 EBP 값. 이때의 ESP 값은 main() 함수 실행이 끝난 후 돌아갈 리턴 주소다. 

- EBP는 함수에서 베이스 포인터의 역할을 하고 EBP가 이전에 가지고 있던 값을 스택에 백업해두기 위해 PUSH로 넣어 둠. 

- ESP와 EBP는 MOV 명령어를 통해 같아짐. 

- EBP는 계속 고정된 값을 가지고 이 값을 통해 로컬 변수 및 파라미터에 접근할 것. 

- 두 명령어를 통해 스택 프레임이 생성됨. 

 

 

- 스택 프레임 우클릭 - Address - Relative to EBP 를 통해 직관적으로 EBP 기준의 주소를 확인할 수 있는데 EBP가 현재 ESP의 값과 같은 것을 확인할 수 있다.

- 0014FF28 에는 다시 0014FF70 값이 저장되어 있는데 이 주소는 main() 함수가 시작할 때 EBP가 처음 가지고 있던 초기 값임을 확인할 수 있다. 

 

 

7.2.3 로컬 변수 세팅

long a = 1, b = 2;

 

- SUB 명령어를 통해 ESP에서 8을 빼준다. 이 공간은 로컬 변수 a, b가 담길 공간으로 long 타입(4바이트)의 로컬 변수 a, b 의 값 할당 확보를 위한 작업이다.  

 

- 아래 두 명령어는 EBP-4 주소에는 1할당, EBP-8에는 2를 넣으라는 의미이다. 이때 EBP-4는 a, EBP-8은 b인 것을 알 수 있다. 

 

 

[ 교재와 다른 디버거 코드에 대한 의문점 ] 

더보기

 [ 기존 디버거에서의 코드 ]  

 

 

- 아래 두 명령어는 EBP-8 주소에는 1할당, EBP-4에는 2를 넣으라는 의미이다. 이때 EBP-8은 a, EBP-4는 b인 것을 알 수 있다. 

-> 다만 의문은 로컬 변수는 선언한 순서대로 스택에 저장된다. 그런 의미에서 a, b 선언은 문제 없지만 저장되는 주소에 대한 의문이 남는다. EBP-8은 EBP-4보다 주소가 작다. 스택은 높은 주소 -> 낮은 주소의 경향이 있기에 응당 교재에서처럼 EBP - 4 주소에 a가 저장되고, EBP - 8 주소에 b가 저장되는 것이 맞다고 생각하는데,, 디버거의 저장 순서는 위 그림과 같아서 의문이 남는다. 

 

-> 아무튼 교재와 같도록 어셈블리 언어를 패치해주었다. 이는 7.2.3 로컬 변수 세팅 에서 확인  

 

-> 대충 스택 구조를 직접 그려보다가 교재랑 제법 많이 달라서 그냥 교재대로 코드와 값들을 적절히 패치해주는 게 이해에 더 나을 것 같았다. (스택 프레임 뷰나, 어셈블리 코드가 교재랑 너무 많이 다르면 그냥 교재대로 바꾸는 것을 추천한다.)

-> 그런데 왜,, 다를까?

 

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

printf("%d\n", add(a, b));

 

- 004010B4 ~ 004010BB는 변수 a, b를 스택에 넣어주는 역할을 하고 있다. 

- 변수를 스택에 넣어줄 때 파라미터가 C언어의 입력 순서와는 반대로 저장된다.(함수 파라미터의 역순 저장)

ex. 순서: b -> a

add() 호출 이전까지의 실행 모습

- call 명령어가 실행되어 call한 함수로 들어가기 이전에 CPU는 복귀할 주소를 무조건 스택에 저장한다. 

       ->004010BC라는 주소에서 CALL명령어를 통한 add함수 호출 이후 004010C1을 실행할 것을 알 수 있는데 이 주소가 스택에 들어가 있는 것을 확인. 

 

 

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

long add(long a, long b) 
{

 

- add() 함수가 시작되면 자신만의 스택 프레임을 따로 생성한다. 

+) 함수 호출마다 해당 함수의 스택 프레임이 따로 생성된다는 것을 알면 좋겠다. 

- 그 외에는 처음 main 함수에서 스택 프레임이 생성되었을 떄와 같은 과정(EBP 값 스택에 백업, EBP와 ESP 값 같게 만들기)

 

 

7.2.6 add() 함수의 로컬 변수(x, y) 세팅

long x = a, y = b;
// 로컬 변수 x, y에 파라미터 a, b 값을 대입하고 있다.

 

- long 타입의 로컬 변수 x, y를 위한 스택 메모리 영역(8바이트) 확보

 

- add() 함수에서 새롭게 스택 프레임이 생성되면서 EBP 값이 변했기에 EBP+8, EBP+C는 파라미터 a, b를 가리킨다.

- EBP-8, EBP-4는 로컬 변수 x, y를 의미한다. 

- 스택 뷰를 가시적으로 그려봤는데 스택 뷰나, 손 그림 스택이나 똑같은 의미. 

+) 스택을 직접 그려보며 공부하시는 것을 추천드립니다... 

 

 

7.2.7 ADD 연산

return (x + y);

 

- EBP-8(로컬 변수 x)값을 EAX에 저장. 즉 EAX에는 1(a값)이 저장

 

- EAX에 EBP-4(로컬 변수 y)값을 더한다. 즉, 1(a값) + 2(b값)

 

=> 최종적으로 EAX에는 3이 저장된다. 

 

+) EAX 레지스터는 리턴 값으로도 사용되기에 함수가 리턴하기 직전 EAX에 어떤 값을 입력하면 리턴 값이 된다.

 

 

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

return (x + y);

 

- add() 스택 프레임 해제. 

 

- 해당 명령어가 실행되면 스택에 저장된 복귀 주소로 리턴

 

+) 스택에 로컬 변수, 함수 파라미터, 리턴 주소 등이 한번에 보관되어 문자열 함수의 취약점 등을 이용한 Stack Buffer Overflow 기법 위협에 노출되어 있다. 

 

 

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

 

 

- add() 함수 호출시 파라미터로 넘겨준 a, b(8바이트)를 +8 하여 스택을 정리한다. 

 

 

7.2.10 print() 함수 호출

printf("%d\n", add(a,b));

 

- 현재 EAX에는 add()의 리턴 값 3이 들어있다.

- 4010CA에는 printf() 함수가 호출되고 있다.

- EAX라는 32비트 레지스터 + %d 포맷 문자열 32비트 = 8바이트이므로 4010CF 주소에서 ESP에 8을 더해 아까와같이 스택을 정리하고 있다. 

 

 

 

7.2.11 리턴 값 세팅

return 0;

 

 

- xor 연산자는 같은 값을 xor 했을 때 0이되므로 레지스터 초기화할 때 주로 사용된다.

 

 

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

return 0;
}

 

 

- 스택 프레임은 두 명령으로 인해 해제된다. 

- 이후 Return 되며 프로세스가 곧 종료된다.