본문 바로가기

REVERSING

[SWING] Reversing 02

과제 1

 

stackframe.exe 다운받기 >>

book/StackFrame.exe at master · reversecore/book · GitHub

 

#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;
}

 

stackframe.exe 의 소스 코드는 다음과 같이 이루어져 있다.

 

1. 메인함수 시작
2. a 변수에 1 저장, b 변수에 2 저장
3. add() 함수 호출
4. add() 함수의 a 파라미터에 1, b 파라미터에 2가 들어가 있음
5. a의 값을 x 변수에 저장, b의 값을 y 변수에 저장
6. x+y 값을 리턴, 리턴된 값이 printf() 함수의 %d에 들어갈 변수가 됨
7. return 0; 을 통해 종료

 

스택 프레임이란 ESP(스택 포인터)가 아닌 EBP(베이스 포인터) 레지스터를 사용하여 스택 내의 로컬 변수, 파라미터, 복귀 주소에 접근하는 기법입니다.
ESP 레지스터의 값은 프로그램 안에서 수시로 변경되기 때문에 스택에 저장된 변수와 파라미터에 접근하고자 할 때 ESP 값을 기준으로 하면 프로그램을 만들기 힘들고, CPU가 정확한 위치를 참고할 때 어려움이 있습니다.

그렇기 어떤 기준 시점(함수 시작)의 ESP 값을 EBP에 저장하고 이를 함수 내에서 유지시켜 주면, ESP 값이 아무리 변하더라도 EBP를 기준(base)으로 안전하게 해당 함수의 변수와 파라미터, 복귀 주소에 접근할 수 있습니다.
이것이 EBP 레지스터의 베이스 포인터 역할입니다.

 

 

 

OllDbg로 StackFrame.exe 파일을 열고 401000 주소로 이동합니다. (Go to 명령어 사용[Ctrl+G])

 

 

아니면 문자열 참조 기능을 이용해 401000 주소로 이동해도 됩니다.

 

 

ebp : 함수가 종료되었을 때 스택이 돌아가야 할 주소를 가지고 있다.

esp : 현재 스택의 위치를 가리킨다.

 

스택 프레임을 어셈블리 코드로 보면,

 

push ebp ; main() 함수 시작(ebp를 사용하기 전에 기존의 값을 생성한 스택에 저장)

mov ebp, esp ; 현재의 esp(스택 포인터) 값을 ebp에 저장 (ebp 고정)

mov dword ptr  ss:[ebp-4],1 ; a=1

mov dword ptr  ss:[ebp-8],2 ; b=2

mov eax, dword ptr  ss:[ebp-8] ; eax에 2를 저장

mov ecx, dword ptr  ss:[ebp-4] ; ecx에 1을 저장

call stackframe.401000 ; main() 함수 호출, add() 함수 스택 프레임 생성

... ; 함수 본체. 여기서 esp가 변경되더라도 ebp는 변경되지 않기 때문에 안전하게 로컬 변수와 파라미터를 엑세스할 수 있음

mov esp, ebp ; esp를 정리(스택을 함수 시작했을 때의 값으로 복원시킴)

pop ebp ; 리턴되기 전에 저장해 놓았던 원래 ebp 값으로 복원

retn ; 함수 종료

 

 

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

코드 흐름 순서에 맞게 main() 함수부터 시작합니다.

main() 함수(401020)에 BP(Break Point)를 설치[F2]한 후 실행[F9]합니다.

(여러번 누른 후 원상태로 돌아오게한 후 한줄씩 내려올 때는 [F8])

main() 함수 시작 시 스택의 상태입니다.

 

 

현재 ESP = 19FF2C, EBP = 19FF70 입니다. ESP에 저장된 값 401250은 main() 함수의 실행이 끝난 후 돌아갈 리턴 주소(Return Address)입니다.

main() 함수는 시작하자마자 스택 프레임을 생성합니다.

 

push ebp

 

'PUSH'는 값을 스택에 집어넣는 명령입니다. 'EBP 값을 스택에 집어넣어라' 라고 해석할 수 있습니다.
main() 함수에서 EBP가 베이스 포인터의 역할을 하게 되기 때문에 EBP가 이전에 가지고 있던 값을 스택에 백업해두기 위한 용도로 사용됩니다.
나중에 main() 함수가 종료(RETN)되기 전에 이 값을 회복시켜 줍니다.

 

mov ebp, esp

 

'MOV'는 데이터를 옮기는 명령입니다. 'ESP의 값을 EBP로 옮겨라' 라고 해석할 수 있습니다.
이 명령 이후부터는 EBP와 현재 ESP는 같은 값을 가지게 됩니다. 그리고 main() 함수가 끝날 때까지 EBP 값은 고정됩니다.
이 뜻은 스택에 저장된 함수 파라미터와 로컬 변수들은 EBP를 통해 접근(Access)하겠다는 것입니다.
401020과 401021 주소의 두 명령어에 의해서 main() 함수에 대한 스택 프레임이 생성되었습니다.(EBP가 세팅되었습니다.)

OllDbg의 스택 창에서 마우스 우측 메뉴 - Address - Relative to EBP 를 선택합니다. 스택 창에서 EBP의 위치를 확인할 수 있습니다.

 

 

현재 EBP 값은 19FF28로 ESP와 동일하고, 19FF28 주소에는 19FF70이라는 값이 저장되어 있습니다. 19FF70은 main() 함수가 시작할 때 EBP가 가지고 있던 초기 값입니다.

 

 

로컬 변수 세팅

long a = 1, b = 2;

sub esp, 8

 

'SUB'는 빼기 명령어입니다. 'ESP 값에서 8을 빼라' 라고 해석할 수 있습니다. 현재 ESP = 19FF28입니다.

ESP에서 8을 빼는 이유는 무엇일까요? 함수의 로컬 변수는 스택에 저장됩니다. main() 함수의 로컬 변수는 'a'와 'b'입니다. 'a'와 'b'는 long 타입이기 때문에 각각 4바이트 크기를 가집니다.
결국 이 두 변수를 스택에 저장하기 위해서는 총 8바이트가 필요한 것입니다. 그래서 ESP에서 8을 빼 두 변수에게 필요한 메모리 공간을 확보(예약)한 것입니다.
이제 main() 함수 내에서 ESP 값이 아무리 변해도 'a'와 'b' 변수를 위한 스택 영역은 훼손되지 않습니다. EBP 값은 main() 함수 내에서 고정이므로 이를 기준으로 삼아 로컬 변수에 엑세스할 수 있습니다.

 

mov dword ptr  ss:[ebp-4],1

mov dword ptr  ss:[ebp-8],2

mov eax, dword ptr  ss:[ebp-8]

mov ecx, dword ptr  ss:[ebp-4]

 

어셈블리와 C 언어의 포인터 구문 형식

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

 

위 구문에서 SS(Stack Segment)를 표시하는 이유는 해당 메모리가 어떤 세그먼트에 소속되어 있는 지를 표시해주기 위해서 입니다.
실제로 32비트 Windows OS에서는 SS(Stack Segment), DS(Data Segment), ES(Extra data Segment)의 값은 모두 0이기 때문에 이런 식으로 세그먼트를 붙여주는 것에 큰 의미는 없습니다.
ESP와 EBP는 스택을 가리키는 레지스터들이기 때문에 SS 레지스터를 붙여준 것입니다.

 

위의 MOV 명령어들은 '[EBP-4]에는 1을 넣고, [EBP-8]에는 2를 넣어라' 라고 해석할 수 있습니다.
즉 [EBP-4]는 로컬 변수 a를 의미하고, [EBP-8]은 로컬 변수 b를 의미합니다. 여기까지 실행한 후의 스택의 상태입니다.

 

 

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

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

 

위 어셈블리 코드는 전형적인 함수 호출 과정입니다. 40103C 주소의 CALL 401000 명령어에서 401000 함수가 add() 함수입니다.
add() 함수는 파라미터로 a와 b를 받습니다. 위 401034~40103B 주소의 코드에서는 변수 a와 b를 스택에 넣고 있습니다.
여기서 파라미터가 C 언어의 소스 코드 입력 순서와는 반대로 스택에 저장된다는 것을 기억해야 합니다.(파라미터의 역순 저장)
변수 b[EBP-8]가 스택에 먼저 들어가고 변수 a[EBP-4]가 나중에 들어갑니다.

40103C 주소의 CALL 명령어를 실행하여 add() 함수(401000) 안으로 들어간 이후의 스택 변화입니다. [F7]

 

 

CALL 명령어가 실행되어 해당 함수로 들어가기 전에 CPU는 무조건 해당 함수가 종료될 때 복귀할 주소(return address)를 스택에 저장합니다.
코드를 보면 40103C 주소에서 add() 함수를 호출하였고, 그 다음 명령어의 주소는 401041이기 때문에 add() 함수의 실행이 완료되면 401041 주소(add() 함수의 복귀 주소)로 돌아와야 합니다.

 

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

long add(long a, long b) {

 

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

코드는 main() 함수의 스택 프레임을 생성할 때와 완전히 동일합니다.
원래의 EBP 값(main() 함수의 Base Pointer)을 스택에 저장한 후 현재의 ESP(Stack Pointer)를 EBP에 입력합니다.
이제 add() 함수의 스택 프레임이 생성되었습니다. add() 함수 내에서 EBP 값은 고정됩니다.

 

 

 

 

main() 함수에서 사용되는 EBP 값(19FF28)을 스택에 백업한 후 EBP가 19FF10으로 새롭게 세팅되었습니다.

 

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

long x = a, y = b;

 

sub esp, 8

mov eax, dword ptr  ss:[ebp+8]

mov dword ptr  ss:[ebp-8], eax

mov ecx, dword ptr  ss:[ebp+C]

mov dword ptr  ss:[ebp-4], ecx

mov eax, dword ptr  ss:[ebp-8]

add eax, dword ptr  ss:[ebp-4]

 

add() 함수의 로컬 변수 x, y에 각각 파라미터 a, b를 대입합니다.

로컬 변수 x와 y에 대한 스택 메모리 영역(8바이트)을 확보합니다.

add() 함수에서 새롭게 스택 프레임이 생성되면서 EBP 값이 변합니다.
[EBP+8], [EBP+C]가 각각 파라미터 a와 b를 가리킵니다. [EBP-8], [EBP-4]는 각각 add() 함수의 로컬 변수 x, y를 의미합니다.

 

 

401006 ; [EBP+8] = param a

401009 ; [EBP-8] = local x

40100C ; [EBP+C] = param b

40100F ; [EBP-4] = local y

 

 

ADD 연산

return (x + y);

 

변수 x의 값([EBP-8] = 1)을 EAX에 넣습니다.

'ADD' 명령어는 덧셈 연산 명령어입니다. EAX에 변수 y의 값([EBP-4] = 2)을 더합니다. EAX 값은 1 + 2 = 3이 되었습니다.

EAX는 '범용 레지스터'로 산술 연산에 사용되며, 또 다른 특수 용도로는 리턴 값으로 사용됩니다. 위와 같이 함수가 리턴하기 직전에 EAX에 어떤 값을 입력하면 그대로 리턴 값이 됩니다.
이 과정에서의 스택 변화는 없습니다.

 

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

return (x + y); }

add() 함수가 리턴될 차례입니다. 그 전에 add() 함수의 스택 프레임을 해제해야 합니다.

현재 EBP 값을 ESP에 대입합니다. 앞에서 실행된 401001 주소의 MOV EBP, ESP 명령어에 대응합니다.
즉, add() 함수를 시작할 때의 ESP 값(19FF10)을 EBP에 넣어 두었다가 함수가 종료될 때 ESP를 원래대로 복원시키는 목적으로 사용하는 것입니다.

위 명령에 의해서 401003 주소의 SUB ESP, 8 명령의 효과는 사라집니다. add() 함수의 로컬 변수 x, y는 더 이상 유효하지 않습니다.

 

add() 함수를 실행하며 스택에 백업해두었던 EBP 값을 복원합니다. 앞에서 실행된 401000 주소의 PUSH EBP 명령에 대응합니다.
복원된 EBP 값은 19FF28이며, 이 값은 main() 함수의 EBP 값입니다. 이제 add() 함수의 스택 프레임이 해제되었습니다.

 

 

ESP = 19FF14, 주소 값은 401041입니다. 이 값은 앞 CALL 401000 명령에서 CPU가 스택에 입력한 복귀 주소입니다.

 

RETN 명령어가 실행되면 스택에 저장된 복귀 주소로 리턴합니다.

 

 

 

add() 함수를 호출하기 전의 스택 상태로 완전히 돌아왔습니다.

이렇게 스택을 관리하기 때문에 함수 호출이 계속 중첩된다고 하더라도 스택이 깨지지 않고 유지될 수 있습니다.
하지만 스택에 로컬 변수와 함수 파라미터, 리턴 주소 등을 한 번에 보관하기 때문에 문자열 함수의 취약점을 이용한 Stack Buffer Overflow 기법에 쉽게 당할 수 있습니다.

 

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

 

add esp, 8

 

이제 main() 함수 코드로 돌아왔습니다.

'ADD' 명령으로 ESP에 8을 더합니다. ESP에 8을 더하는 이유는 위 스택의 상태에서 19FF18과 19FF1C 주소의 내용이 add() 함수에게 넘겨준 파라미터 a, b 이기 때문입니다.
add() 함수가 완전히 종료되었기 때문에 파라미터 a, b도 필요가 없어졌습니다. 그렇기 때문에 ESP에 8을 더하여 스택을 정리할 수 있습니다. (파라미터 a, b는 long 타입으로 각각 4바이트씩 8바이트 입니다.)

add() 함수를 호출하기 전에 파라미터 a, b를 PUSH 명령으로 스택에 넣었었습니다.

 

 

 

위 방식처럼 함수를 호출한 쪽(Caller)에서 (스택에 저장된) 파라미터를 정리하는 것을 'cdecl' 방식이라고 합니다. 반대로 호출당한 쪽(Callee)에서 (스택에 저장된) 파라미터를 정리하는 것을 'stdcall' 방식이라고 합니다.
이런 함수 호출 규약을 일컬어 Calling Convention이라고 부릅니다.

 

printf() 함수 호출

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

 

push eax

push stackframe.40B384

call stackframe.401067

add esp, 8

 

401044 주소의 EAX 레지스터에는 add() 함수에서 저장된 리턴 값(3)이 들어 있습니다. 40104A 주소의 CALL 401067 명령어에서 401067 함수는 Visual C++에서 생성한 C 표준 라이브러리 printf() 함수를 의미합니다.
위의 경우 printf() 함수의 파라미터 갯수는 2개이며 크기는 8바이트입니다. (32비트 레지스터 + 32비트 상수 = 64비트 = 8바이트)
따라서 40104F 주소에 ADD ESP, 8 명령으로 스택에서 함수 파라미터를 정리합니다. printf() 함수 호출 후 스택이 정리되었기 때문에 스택의 상태는 동일합니다.

 

리턴 값 세팅

return 0;

 

xor eax, eax

 

main() 함수의 리턴 값(0)을 세팅합니다.

 

'XOR' 명령어는 Exclusive OR bit 연산입니다. 같은 값끼리 XOR하면 0이 되는 특징이 있습니다.
MOV EAX, 0 명령어보다 실행 속도가 빠르기 때문에 레지스터를 초기화시킬 때 많이 사용합니다.

같은 값을 이용해서 2번 연속으로 XOR 연산을 수행하면 원본 값이 됩니다. 따라서 이 특징을 암호화/복호화에 많이 적용합니다.

 

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

return 0; }

 

mov esp, ebp

pop ebp

 

마지막으로 메인 함수가 종료됩니다. add() 함수와 마찬가지로 리턴하기 전에 스택 프레임을 해제합니다.

main() 함수의 스택 프레임이 해제되었습니다. main() 함수의 로컬 변수인 a와 b도 유효하지 않습니다.

 

 

main() 함수가 시작할 때의 스택 상태와 완전히 동일합니다.

 

retn

 

메인 함수가 종료(리턴)되면서 리턴 주소(401250)로 점프합니다. 이 주소는 Visual C++의 Stub Code 영역입니다.
이후에는 프로세스 종료 코드가 실행됩니다.

 

 

참고 자료)

리버싱 핵심원리_07_Stack Frame (tistory.com)

[리버싱 핵심 원리] 스택 프레임 (velog.io)

 

 

 

'REVERSING' 카테고리의 다른 글

[SWING] Reversing 06  (0) 2023.03.26
[SWING] Reversing 05  (0) 2022.08.26
[SWING] Reversing 04  (0) 2022.08.22
[SWING] Reversing 03  (0) 2022.08.13
[SWING] Reversing 01  (0) 2022.07.30