스택 프레임이란 ESP(스택 포인터)가 아닌 EBP(베이스 포인터)레지스터를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀주소에 접근하는 기법을 말한다.
ESP 레지스터의 값은 프로그램 안에서 수시로 변경된다. 따라서 스택에 저장된 변수, 파라미터에 접근하고자 할 때 ESP 값을 기준으로 하면 프로그램을 만들기 어렵고 CPU가 정확한 위치를 참고할 때 어려움이 있다.
함수를 시작할 때 ESP의 값을 EBP에 저장하고 이를 함수 내에서 유지해 아무리 ESP 값이 바뀌어도 EBP를 기준으로 안전하게 해당 함수의 변수, 파라미터, 복귀 주소에 접근할 수 있다.
7.1 스택 프레임의 구조
구조를 자세히 정리하면 아래와 같다.
POP EBP : 함수 시작 (EBP값을 저장) MOV EBP, ESP : 현재 ESP(스택 포인터)값을 EBP에 저장 (함수 본체) : 여기서 ESP가 변하더라도 EBP가 있기 때문에 안전하게 로컬 변수와 파라미터를 엑세스 할 수 있다. MOV ESP, EBP : ESP의 값을 복구한다. POP EBP : 리턴되기 전에 저장해 놓았던 원래 EBP 값으로 복원시킴 RETURN : 함수 종료 |
+) 최신 컴파일러는 최적화 옵션이 있어 간단한 함수 같은 경우 스택 프레임 생성 X
+) 스택에 return 주소가 저장된다는 점이 보안 취약점으로 작용할 수 있다. (buffer overflow기법을 사용해 return 주소가 저장된 스택 메모리를 의도적으로 변경 가능)
7.2 실습 예제
1. main() 함수 시작 & 스택 프레임 생성
PUSH EBP 명령어를 통해 스택 프레임을 생성시킨다. PUSH EBP는 EBP가 이전에 가지고 있던 값을 스택에 백업해두기 위한 용도 EBP가 베이스 포인터 역할을 하기 때문에 꼭 백업을 시켜야 한다. 이후, MOV EBP, ESP라는 명령어로 초기 ESP 값은 EBP에 백업한다. ESP값은 스택의 위치를 가리키기 때문에 수시로 바뀐다. 만약 함수의 호출이 끝나면 이전의 상태로 돌아가야하기 때문에 전으로 돌아갈 수 있는 값을 가리키는 ESP 값을 EBP에 저장해야 한다.
2. 로컬 변수 세팅
long a = 1, b = 2; 라는 코드에 해당되는 부분으로, 2개의 변수는 각각 4byte씩 총 8byte이다. 즉, 2개의 변수 공간을 마련하기 위해서 아래 사진의 명령어를 실행한다.
이것을 통해 ESP 값이 아무리 변해도 해당 변수들을 위해 확보한 스택 영역은 훼손되지 않는다.
이 명령어로 해당 변수의 값들을 스택에 PUSH한 것을 볼 수 있다.
3. add() 함수 파라미터 입력 및 add() 함수 호출
아래 명령어는 printf(“%d\n”,add(a,b)); 라는 코드 부분에 있는 add 함수를 call하기 위해 파라미터를 넘기는 명령어이다. 여기서 주목해야 될 점은, 파라미터가 C언어 입력 순서와는 반대로 입력된다는 점이다. C언어에선, a 다음 b 순으로 파라미터를 넘겼지만, 어셈블리어에선 반대로 b 다음 a 순으로 스택에 저장된다.
CALL 명령어로 add함수를 호출하면 스택엔 return address가 저장된다. return 주소는 401000으로 해당 함수가 종료되면 401001으로 복귀해야 한다. 그리고 401001이 add함수의 return 주소이다.
4. add()함수 시작 & 스택 프레임 생성& 로컬 변수 세팅
함수를 호출하고 add 함수만의 스택 프레임을 생성한 후에 지역 변수(x,y)를 세팅한다. (스택 프레임의 생성은 main함수와 동일하다.) 아래 명령어가 해당 과정이다. 지역 변수를 위한 공간 8byte를 마련하고 파라미터에서 값을 가져와 로컬 변수에 해당하는 스택 공간에 저장하는 모습이다.
로컬 변수의 값이 잘 세팅된 모습이다.
6. ADD 연산
해당 명령어는 x+y를 연산하는 부분이다. 여기에서 사용한 EAX는 ‘범용 레지스터’로 산술 연산에 사용되며, 또 다른 특수 용도는 리턴 값으로 사용된다. 아래 명령어와 같이 함수가 리턴하기 직전에 EAX에 어떤 값을 입력하면 그대로 리턴이 된다.
또한, 이 과정에서 스택은 변하지 않았기 때문에 위에 있는 스택 그림과 동일하다.
7. add() 함수의 스택 프레임 해제 & 함수 종료(리턴)
함수에서 return하기 전에 해당 함수의 스택 프레임을 해제해야 한다.
MOV 명령어에 의해 지역변수들은 더 이상 유효하지 않게 되고, add 함수가 시작되면서 스택에 백업한 EBP 값을 복원한다. 그리고 Return 명령어가 실행되면 스택에 저장된 복귀 주소로 이동한다.
프로그램은 이런 식으로 스택을 관리하기 때문에 함수 호출이 계속 중첩된다고 하더라도 스택이 깨지지 않고 잘 유지된다. 하지만 스택에 로컬 변수, 함수 파라미터, 리턴 주소 등을 한번에 보관하기 때문에 문자열 함수의 취약점 등을 이용한 Stack Buffer Overflow 기법에 당하기도 한다.
8. add() 함수 파라미터 제거(스택 정리)
ADD ESP 8이라는 명령어를 통해 add함수에 넘겨준 파라미터들을 정리한다.
왼쪽 그림이 정리 전 스택의 모습이고 오른쪽 그림이 정리 후 스택의 모습이다.
9. printf() 함수 호출
해당 명령어들은 add 함수에서 계산한 값(=리턴값)을 스택에 저장하고 출력하는 과정을 나타낸다. CALL을 통해 들어가는 주소는 Visual C++에서 생성한 C 표준 라이브러리 printf()함수이다.
a+b 값을 출력한 후, 마지막 줄의 ADD ESP 8을 통해 기존에 있던 main함수의 지역변수 a와 b를 정리하는 과정을 볼 수 있다.
10. 리턴 값 세팅
main 함수의 return 값을 세팅하는 명령어이다. 같은 값끼리 XOR하면 0이되는 성질을 이용한 것이다. MOV EAX,0 보다 실행 속도가 빨라 해당 그림에 있는 명령어를 자주 사용한다.
(+) 같은 값을 이용해서 2번 연속으로 XOR연산을 수행하면 원본 값이 되는데 이 특징을 암호화/복호화에 많이 적용한다.
11. 스택 프레임 해제 & main() 함수 종료
마지막으로 스택 프레임을 해제하고 main함수를 종료하는 과정은 add함수의 스택프레임을 해제하고 종료하는 과정과 동일하다.
위의 2개의 코드를 실행시키면 아래와 같이 스택이 초기 상태로 돌아온 것을 볼 수 있다.
마지막으로 RETN 명령어가 실행되며 리턴 주소로 점프하게 된다. 점프한 주소는 Visual C++의 Stub Code 영역이다. 이후에는 프로세스 종료 코드가 실행된다.
'리버싱 핵심원리' 카테고리의 다른 글
[리버싱 핵심원리] 10장 - 함수 호출 규약 (0) | 2023.05.05 |
---|---|
[리버싱 핵심원리] 8장 - abex' crackme #2 (0) | 2023.05.05 |
[리버싱 핵심원리] 5장 - 스택 (0) | 2023.05.01 |
[리버싱 핵심원리] 20장 - 인라인 패치 실습 (1) | 2023.05.01 |
[리버싱 핵심원리] 19장 - UPack 디버깅 OEP 찾기 (0) | 2023.04.27 |