스택 프레임은 ESP(스택 포인터)가 아닌 EBP(베이스 포인터) 레지스터를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법을 말한다.
ESP 레지스터의 값은 프로그램 안에서 수시로 변경되기 때문에 스택에 저장된 변수, 파라미터에 접근하고자 할 때 ESP 값을 기준으로 하면 프로그램을 만들기 힘들고, CPU가 정확한 위치를 참고하고자 할 때 어려움이 있다. 따라서 어떤 기준 시점(함수 시작)의 ESP 값을 EBP에 저장하고, 이를 함수 내에서 유지해주면, ESP 값이 아무리 변해도 EBP를 기준으로 안전하게 해당 함수의 변수, 파라미터, 복귀 주소에 접근할 수 있다. 이것이 EBP 레지스터의 베이스 포인터 역할이다.
스택 프레임을 어셈블리 코드로 보면 이런 형식이다.
이렇게 스택 프레임을 이용하면 함수가 아무리 길어도 스택을 완벽하게 관리할 수 있다.
※ 최신 컴파일러는 최적화 옵션을 가지고 있어서 간단한 함수 같은 경우에 스택 프레임을 생성하지 않는다.
※ 스택에 복귀 주소가 저장된다는 점이 보안 취약점으로 작용할 수 있다. BOF(Buffer Overflow) 기법을 사용하여 복귀 주소가 저장된 스택 메모리를 의도적으로 다른 값으로 변경할 수 있다.
StackFrame을 알아보기 위해 다음과 같은 코드로 릴리즈한다. 참고에서 말했듯이 최신 컴파일러는 최적화 옵션을 가지고 있기 때문에 다음 코드에서 스택 프레임을 확인하기 위해서는 해당 옵션을 꺼야 한다.
릴리즈 한 파일을 OllyDbg에 올려본다. 올리고 나서 Main 함수를 호출하는 부분까지 Step Into와 Step Over를 적절히 활용하여 찾아간다.
찾았으니 Enter를 입력해 main 함수를 확인한다.
함수 안에서 add함수와 printf함수를 호출하는 것을 볼 수 있다. 그리고 main 함수의 시작과 동시에 우리가 위에서 확인했던 Stack Frame의 기본 형태, 즉 PUSH EBP 후에 MOV EBP, ESP를 확인할 수 있다. 이후 main 함수가 RETN 되기 전에도 MOV ESP, EBP 후에 POP EBP가 있는 것으로 보아 Stack Frame을 사용하는 것을 직접 확인했다.
여기서 PUSH는 값을 집어넣는 명령이다. 그래서 PUSH EBP란 EBP의 값을 스택에 집어 넣으라는 의미이다. MOV는 데이터를 옮기는 명령이다. MOV EBP, ESP라는 명령은 해석하면 ESP의 값을 EBP에 옮기라는 의미이다. 이 때 ESP의 값은 변하지 않는다.
SUB ESP, 8 명령에서 SUB는 빼기 명령이다. 즉, ESP에서 8만큼 빼라는 뜻이다. 이후 DWORD PTR SS: [EBP-8]과 같은 것이 보이는데, 그냥 포인터 같은 개념이라고 생각하면 편하다. DWORD형 타입을 가지는 EBP-8의 주소에 있는 값에 1을 저장하겠다는 뜻이다. 즉, C언어로 표현하자면 *(EBP-8)=1이다.
@ 스택 창에서 우클릭하여 다음과 같은 옵션을 선택해주면 EBP의 현재 위치에 대한 상대적인 주솟값으로 스택 창을 확인할 수 있다. 즉, EBP가 어떤 값을 스택에 저장했는지 스택 창에서 직관적으로 볼 수 있다.
계속해서 add 함수로 가보자.
Add 함수는 파라미터로 a와 b를 받는다. Main 함수에서 두 번째 파라미터의 값과 첫 번째 파라미터의 값을 순서대로 스택에 넣는다. 그리고 add 함수를 호출해서 복귀할 주소를 스택에 저장한다.
그 후 add 함수에서는 main 함수에서 변수를 두 개 만들 때와 동일하게 EBP에서 8을 빼준다. EBP-4의 주솟값에 EBP+8에 들어있는 값을 넣어주고, EBP-8의 값에 EBP+4에 들어있는 값을 넣어준다. 이후 EAX에 EBP-4에 들어있는 값을 넣고 EBP-8에 있는 값을 더해준 다음 함수를 빠져나온다.
Step Into로 하나씩 실행시키면 스택과 레지스터 에서 위 과정을 확인할 수 있다. Add 함수 내용을 모두 실행한 후 스택은 다음과 같다.
함수를 빠져나온 후 스택에 저장해 놓았던 돌아올 위치로 돌아간다. 이후에 main함수에서 ADD ESP, 8 명령을 볼 수 있는데, add 함수에게 넘겨주었던 파라미터 a와 b를 정리해주는 것이다. (A와 b는 long타입으로 각각 4바이트로 합하여 8바이트이다.) 더 이상 필요하지 않기 때문이다.
※ 이와 같이 함수를 호출한 쪽(Caller)에서 스택에 저장된 파라미터를 정리하는 것을 ‘cdecl’ 방식이라고 한다. 반대로 호출당한 쪽(Callee)에서 스택에 저장된 파라미터를 정리하는 것을 ‘stdcall’ 방식이라고 한다. 여기서는 main 함수가 add를 호출하고, main 함수에서 add의 파라미터를 정리하고 있으니 cdecl 방식이다. 이러한 함수 호출 규약을 일컬어 Calling Convention이라고 한다.
이후 스택은 다음과 같아진다.
그런 다음 add 함수의 리턴 값이었던 EAX와 “%d\n”을 스택에 저장하고 printf를 호출한다. 호출한 이후는 아까 add 함수 때와 마찬가지로 파라미터로 넘겨준다.
이렇게 printf 함수가 끝난 후에 다시 파라미터를 main에서 정리해준다. main함수의 리턴 값을 XOR EAX, EAX 명령으로 0을 만들어준 뒤 스택 프레임을 해제(MOV ESP, EBP; POP EBP;)한다.
Main 함수가 종료된 후에는 컴파일러의 Stub Code 영역으로 다시 돌아간다. 프로세스 종료 코드가 실행되는 것이다.
※ 다음과 같이 옵션[Alt + O] 의 Analysis 1 탭에서 ‘Show ARGs and LOCALs in procedures’ 옵션을 체크하면 DWORD PTR SS:[EBP-4]와 같은 로컬 변수와 파라미터들을 가독성 좋게 보이도록 한다.
'Reversing > Reversing' 카테고리의 다른 글
Calling Convention (함수 호출 규약) (0) | 2017.07.02 |
---|---|
Abex’ Crackme #2 (0) | 2017.06.30 |
Abex' Crackme #1 (0) | 2017.06.27 |
OllyDbg Commands & Assembly Basic & etc (0) | 2017.06.27 |
Hello, World! 디버깅하기 (0) | 2017.06.26 |