스택 프레임
스택 프레임은 x86 어셈블리를 구성하는 매우 중요한 개념이다. 함수가 실행될 때마다 함수의 스택을 생성한다는 말은 들어보았을 것이다. 스택 프레임은 어셈블리 수준에서 함수를 구현하기 위해 필수적으로 이해해야 한다. 스택 프레임이라는 이름 중 스택은 자료구조 중 스택과 이름이 같다. 실제로 스택 프레임은 스택이라는 자료구조의 작동 방식을 이용한다. 스택에서 top을 가리키는 방법은 아래 그림 1과 같이 두 가지가 있는데 x86 어셈블리는 왼쪽의 방법을 사용한다.
| .... | +------------+ | last input | <- top +------------+ | data | +------------+ | data | +------------+ | data | +------------+ | .... | |
| .... | <- top +------------+ | last input | +------------+ | data | +------------+ | data | +------------+ | data | +------------+ | .... | |
그림 1 스택을 표현하는 두 가지 방법
x86 어셈블리는 스택을 위한 두 개의 레지스터(EBP, ESP)를 제공한다. ESP는 스택의 top에 해당하는 곳의 메모리 주소를 저장한다. 주소가 작은 쪽이 스택의 top에 해당되고 4바이트 단위로 이동한다. Push를 수행하면 데이터가 스택에 삽입되고 ESP는 4만큼 작아진다. 반대로 pop을 수행하면 데이터가 꺼내어지고 ESP는 4만큼 커진다. EBP는 이전 스택으로 복귀하기 위해 사용한다. 이전 스택이라는 말이 이해가 가지 않을 테니 다음의 예시로 설명하겠다.
실행 환경
Microsoft Visual Studio Community 2015
버전 14.0.24720.00 Update 1
Microsoft .NET Framework
버전 4.6.01038
설치된 버전: Community
...
Visual C++ 2015 00322-20000-00000-AA691
Microsoft Visual C++ 2015
...
그림 2 디버깅 정보 있으면 VS도 좋음
위의 프로그램을 이용하여 예제 프로그램을 디버깅하면서 설명한다.
예제 코드
int frame(int a, int b, int c, int d)
{
int localA;
int localB;
int localC;
int localD;
localA = a;
localB = b;
localC = c;
localD = d;
return 1;
}
int main(int argc, char *argv[])
{
frame(1, 2, 3, 4);
}
int frame(int a, int b, int c, int d)
{
00C81000 push ebp
00C81001 mov ebp,esp
00C81003 sub esp,50h
00C81006 push ebx
00C81007 push esi
00C81008 push edi
int localA;
int localB;
int localC;
int localD;
localA = a;
00C81009 mov eax,dword ptr [ebp+8]
00C8100C mov dword ptr [ebp-4],eax
localB = b;
00C8100F mov eax,dword ptr [ebp+0Ch]
00C81012 mov dword ptr [ebp-8],eax
localC = c;
00C81015 mov eax,dword ptr [ebp+10h]
00C81018 mov dword ptr [ebp-0Ch],eax
localD = d;
00C8101B mov eax,dword ptr [ebp+14h]
00C8101E mov dword ptr [ebp-10h],eax
return 1;
00C81021 mov eax,1
}
00C81026 pop edi
00C81027 pop esi
00C81028 pop ebx
00C81029 mov esp,ebp
00C8102B pop ebp
00C8102C ret
00C8102D int 3
00C8102E int 3
00C8102F int 3
int main(int argc, char *argv[])
{
00C81030 push ebp
00C81031 mov ebp,esp
00C81033 sub esp,40h
00C81036 push ebx
00C81037 push esi
00C81038 push edi
frame(1, 2, 3, 4);
00C81039 push 4
00C8103B push 3
00C8103D push 2
00C8103F push 1
00C81041 call 00C81000
00C81046 add esp,10h
}
00C81049 xor eax,eax
00C8104B pop edi
00C8104C pop esi
00C8104D pop ebx
00C8104E mov esp,ebp
00C81050 pop ebp
00C81051 ret
4바이트 주소로 시작하지 않는 줄은 Visual Studio에서 디버깅 용으로 삽입한 참고용 정보이다.
Argument 전달
opcode |
registers |
frame(1, 2, 3, 4); 00C81039 push 4 00C8103B push 3 00C8103D push 2 00C8103F push 1 ->00C81041 call 00C81000 00C81046 add esp,10h |
EAX = 0F9A3700 EBX = 0037E000 ECX = 00000001 EDX = 00000000 ESI = 00C81470 EDI = 00C81470 EIP = 00C81041 ESP = 0018FDD8 EBP = 0018FE34 EFL = 00000200 |
stack |
|
0x0018FDD8 00000001 .... 0x0018FDDC 00000002 .... 0x0018FDE0 00000003 .... 0x0018FDE4 00000004 .... |
그림 3 argument 전달 방식
Call 명령으로 함수를 호출하기 직전의 상황이다. 0x00C81039부터 0x00C81041까지 argument를 역순으로 push하였다. 역순으로 push를 하였기 때문에 스택에는 1, 2, 3, 4의 순서로 저장되었다.
함수 호출
opcode | registers |
int frame(int a, int b, int c, int d) { ->00C81000 push ebp 00C81001 mov ebp,esp | EAX = 0F9A3700 EBX = 0037E000 ECX = 00000001 EDX = 00000000 ESI = 00C81470 EDI = 00C81470 EIP = 00C81000 ESP = 0018FDD4 EBP = 0018FE34 EFL = 00000200 |
stack | |
0x0018FDD4 00c81046 F.?. 0x0018FDD8 00000001 .... 0x0018FDDC 00000002 .... 0x0018FDE0 00000003 .... 0x0018FDE4 00000004 .... |
그림 4 call 이후 스택
Call 명령을 수행한 직후의 상황이다. 위의 결과에서 볼 수 있듯이 call을 수행하면 스택에 push가 수행된다. Push된 값 0x00C81046은 그림 3에서 볼 수 있다. 이 값은 수행된 call 명령 바로 다음의 명령을 가리키는 주소다. 이처럼 call 명령은 함수가 완료된 후 복귀할 주소를 push한다.
스택 프레임 생성
opcode | registers |
int frame(int a, int b, int c, int d) { 00C81000 push ebp 00C81001 mov ebp,esp ->00C81003 sub esp,50h | EAX = 0F9A3700 EBX = 0037E000 ECX = 00000001 EDX = 00000000 ESI = 00C81470 EDI = 00C81470 EIP = 00C81003 ESP = 0018FDD0 EBP = 0018FDD0 EFL = 00000200 |
stack | |
0x0018FDD0 0018fe34 4?.. 0x0018FDD4 00c81046 F.?. 0x0018FDD8 00000001 .... 0x0018FDDC 00000002 .... |
그림 5 스택 프레임 생성
함수 내부에서 첫 두 명령이 새로운 스택 프레임을 생성한다. 0x00C81000에서 이전 스택 프레임의 EBP를 스택에 저장하고, 0x00C81001에서 현재 ESP(스택의 top)를 EBP에 복사한다.
지역변수 공간 할당
opcode | registers |
int frame(int a, int b, int c, int d) { 00C81000 push ebp 00C81001 mov ebp,esp 00C81003 sub esp,50h ->00C81006 push ebx 00C81007 push esi 00C81008 push edi | EAX = 0F9A3700 EBX = 0037E000 ECX = 00000001 EDX = 00000000 ESI = 00C81470 EDI = 00C81470 EIP = 00C81006 ESP = 0018FD80 EBP = 0018FDD0 EFL = 00000200 |
stack | |
0x0018FDD0 0018fe34 4?.. 0x0018FDD4 00c81046 F.?. 0x0018FDD8 00000001 .... 0x0018FDDC 00000002 .... |
그림 6 지역변수 공간 할당
새로 진입한 함수에서 지역 변수들은 새로 생성된 스택 프레임에 저장된다. 이 공간을 확보하기 위해 스택의 top을 위로 옮긴다(sub esp, 50h). 이렇게 스택의 top을 위로 옮겼기 때문에 push와 pop으로 스택의 값이 변경되어도 아래쪽의 지역변수에는 영향을 주지 않는다.
지역 변수 확인
opcode | registers |
localA = a; 00C81009 mov eax,dword ptr [ebp+8] 00C8100C mov dword ptr [ebp-4],eax localB = b; 00C8100F mov eax,dword ptr [ebp+0Ch] 00C81012 mov dword ptr [ebp-8],eax localC = c; 00C81015 mov eax,dword ptr [ebp+10h] 00C81018 mov dword ptr [ebp-0Ch],eax localD = d; 00C8101B mov eax,dword ptr [ebp+14h] 00C8101E mov dword ptr [ebp-10h],eax return 1; ->00C81021 mov eax,1 | EAX = 00000004 EBX = 0037E000 ECX = 00000001 EDX = 00000000 ESI = 00C81470 EDI = 00C81470 EIP = 00C81021 ESP = 0018FD74 EBP = 0018FDD0 EFL = 00000200 |
stack | |
0x0018FDC0 00000004 .... 0x0018FDC4 00000003 .... 0x0018FDC8 00000002 .... 0x0018FDCC 00000001 .... 0x0018FDD0 0018fe34 4?.. 0x0018FDD4 00c81046 F.?. 0x0018FDD8 00000001 .... 0x0018FDDC 00000002 .... 0x0018FDE0 00000003 .... 0x0018FDE4 00000004 .... |
그림 7 지역 변수와 argument는 EBP를 기준으로 접근
0x00C81009부터 0x00C8101E까지 함수에서 정의한 코드를 실행했다. 4개의 지역 변수에 각각 argument의 값(1, 2, 3, 4)을 할당한다. 지역 변수 a, b, c, d의 주소는 각각 [EBP-4], [EBP-8], [EBP-0Ch], [EBP-10h]에 해당한다. 먼저 선언된 변수는 스택의 깊은 곳에 위치하고 나중에 선언된 변수는 스택의 얕은 곳에 저장된다. 스택의 상태를 보면 4, 3, 2, 1 순서로 역순으로 저장되어있는 것을 볼 수 있다.
함수 종료
opcode | registers |
return 1; 00C81021 mov eax,1 } 00C81026 pop edi 00C81027 pop esi 00C81028 pop ebx 00C81029 mov esp,ebp 00C8102B pop ebp ->00C8102C ret | EAX = 00000004 EBX = 0037E000 ECX = 00000001 EDX = 00000000 ESI = 00C81470 EDI = 00C81470 EIP = 00C8102C ESP = 0018FD64 EBP = 0018FE34 EFL = 00000200 |
stack | |
0x0018FDD4 00c81046 F.?. 0x0018FDD8 00000001 .... 0x0018FDDC 00000002 .... 0x0018FDE0 00000003 .... 0x0018FDE4 00000004 .... |
그림 8 함수 종료
함수에서 정의한 작업을 다 마치면 0x00D7102F와 0x00D71030의 두 명령으로 이전 스택 프레임을 복구한다. 두 명령 mov esp, ebp; pop ebp는 함수 처음에 실행했던 두 명령 push ebp; mov ebp, sbp와 완전히 반대의 역할을 한다. 이러한 명령을 통해 스택 프레임은 완전히 이전의 스택 프레임으로 복구될 수 있다. 하지만 아직 완전히 복구된 건 아니고 스택의 top에 call명령으로 저장된 복귀 주소가 있다. ret명령은 이 주소를 pop하고 이 주소로 EIP를 세팅한다.
함수 종료 직후
opcode | registers |
frame(1, 2, 3, 4); 00C81039 push 4 00C8103B push 3 00C8103D push 2 00C8103F push 1 00C81041 call 00C81000 ->00C81046 add esp,10h | EAX = 00000001 EBX = 0037E000 ECX = 00000001 EDX = 00000000 ESI = 00C81470 EDI = 00C81470 EIP = 00C81046 ESP = 0018FDD8 EBP = 0018FE34 EFL = 00000200 |
stack | |
0x0018FDD8 00000001 .... 0x0018FDDC 00000002 .... 0x0018FDE0 00000003 .... 0x0018FDE4 00000004 .... |
그림 9 함수 종료 직후
함수를 호출하기 직전인 그림 3과 비교해보자. 스택 프레임을 구성하는 EBP와 ESP의 값이 같고 스택 프레임의 내용이 동일하다. 스택 프레임이 함수 호출 이전의 상태로 완전히 복구되었다. 그리고 add esp, 10h 명령으로 스택에 argument들을 저장하기 이전의 상태로 스택의 top을 이동한다.
종합
스택 프레임의 생성과 소멸을 한 과정씩 살펴보았다. 아래의 내용은 스택 프레임에 대한 전체적인 설명이다.
스택 프레임은 그림 10의 4개 명령으로 생성되고 소멸된다. 첫 두 명령은 함수 prologue, 마지막 두 명령은 함수 epilogue라고 한다.
EBP는 스택 프레임의 base역할을 하며 EBP가 가리키고 있는 곳에는 이전 스택의 EBP가 저장되어있다. 이 곳은 SFP(Stack Frame Pointer)라고 한다. EBP의 값과 EBP가 가리키고 있는 값의 의미를 잘 구별해야한다. SFP를 통해 이전 스택으로 복귀할 수 있으며 이전 스택의 EBP를 push하여 저장하였기 때문에 pointer라는 이름을 갖는다.
지역 변수와 argument는 EBP를 기준으로 접근한다. 그림 11을 보면 argument의 저장 순서, 지역변수 할당 순서를 쉽게 기억할 수 있다. EBP에 가까울수록 index가 작고 EBP에서 멀어질수록 index가 커진다.
push ebp
mov ebp, sbp
...
Function code
...
mov esp, ebp
pop ebp
그림 10 함수 prologue와 epilogue
+---------+---------+---------+---------+-----+-----+-------+-------+-------+-------+
| LOCAL 4 | LOCAL 3 | LOCAL 2 | LOCAL 1 | SFP | RET | ARG 1 | ARG 2 | ARG 3 | ARG 4 |
+---------+---------+---------+---------+-----+-----+-------+-------+-------+-------+
lower address | higher address
+-EBP
그림 11 스택 프레임의 전체적 모습
참고
Visual Studio에서 위의 예제를 재현하면 어셈블리 코드가 달라질 수 있다. 위와 같은 어셈블리에서 디버깅을 진행하려면, 프로젝트 속성에서 C/C++ -> 코드 생성 -> 기본 런타임 검사를 /RTCu, 링커 -> 일반 -> 증분링크 사용을 /INCREMENTAL:NO로 설정해야한다.
https://msdn.microsoft.com/library/8wtf2dfz(v=vs.110).aspx
https://msdn.microsoft.com/en-us/library/4khtbfyf.aspx
함수 호출 규약
함수를 호출하는 방법에는 __cdecl, __stdcall, __fastcall, __clrcall, __thiscall ,__vectorcall 등 여러 방법이 있다. 주로 보게 되는 앞의 3개에 대해 설명한다.
예제 코드
void __cdecl cde(int a, int b, int c, int d) {}
void __stdcall stdcall(int a, int b, int c, int d) {}
void __fastcall fastcall(int a, int b, int c, int d) {}
class test
{
public:
void thiscall(int a, int b, int c, int d) {}
};
int main(int argc, char **argv)
{
cde(1, 2, 3, 4);
stdcall(1, 2, 3, 4);
fastcall(1, 2, 3, 4);
test call = test();
call.thiscall(1, 2, 3, 4);
}
void __cdecl cde(int a, int b, int c, int d) {}
01081000 push ebp
01081001 mov ebp,esp
01081003 sub esp,40h
01081006 push ebx
01081007 push esi
01081008 push edi
01081009 pop edi
0108100A pop esi
0108100B pop ebx
0108100C mov esp,ebp
0108100E pop ebp
0108100F ret
void __fastcall fastcall(int a, int b, int c, int d) {}
01081010 push ebp
01081011 mov ebp,esp
01081013 sub esp,48h
01081016 push ebx
01081017 push esi
01081018 push edi
01081019 mov dword ptr [ebp-8],edx
0108101C mov dword ptr [ebp-4],ecx
0108101F pop edi
01081020 pop esi
01081021 pop ebx
01081022 mov esp,ebp
01081024 pop ebp
01081025 ret 8
void __stdcall stdcall(int a, int b, int c, int d) {}
01081030 push ebp
01081031 mov ebp,esp
01081033 sub esp,40h
01081036 push ebx
01081037 push esi
01081038 push edi
01081039 pop edi
0108103A pop esi
0108103B pop ebx
0108103C mov esp,ebp
0108103E pop ebp
0108103F ret 10h
class test
{
public:
void thiscall(int a, int b, int c, int d) {}
01081050 push ebp
01081051 mov ebp,esp
01081053 sub esp,44h
01081056 push ebx
01081057 push esi
01081058 push edi
01081059 mov dword ptr [ebp-4],ecx
0108105C pop edi
0108105D pop esi
0108105E pop ebx
0108105F mov esp,ebp
01081061 pop ebp
01081062 ret 10h
};
스택 프레임을 살펴볼 때, 함수가 완료된 후 함수를 호출한 쪽에서 add esp, x를 실행하여 전달했던 인자를 정리했다. 제목과 같이 이와 반대로 호출된 함수 내부에서 스택을 정리할 수도 있다.
예제 코드에서 stdcall을 호출하는 곳(0x0108109B)을 보면 cde를 호출하는 곳과 달리 add esp, x 명령이 없다. 대신 stdcall의 마지막 부분(0x0108103F )을 보면 ret 10h라는 명령이 있다. 이는 전달된 4개의 argument의 크기인 16(10h)바이트 만큼 스택을 정리하라는 것이다.
__cdecl
void __cdecl cde(int a, int b, int c, int d) {}
01081000 push ebp
01081001 mov ebp,esp
01081003 sub esp,40h
01081006 push ebx
01081007 push esi
01081008 push edi
01081009 pop edi
0108100A pop esi
0108100B pop ebx
0108100C mov esp,ebp
0108100E pop ebp
0108100F ret
cde(1, 2, 3, 4);
01081083 push 4
01081085 push 3
01081087 push 2
01081089 push 1
0108108B call 01081000
01081090 add esp,10h
이 방식은 C언어에서 기본적으로 사용하는 방식이다. Argument를 역순으로 저장한다. 스택의 정리는 호출한 쪽에서 수행한다.
__stdcall
void __stdcall stdcall(int a, int b, int c, int d) {}
01081030 push ebp
01081031 mov ebp,esp
01081033 sub esp,40h
01081036 push ebx
01081037 push esi
01081038 push edi
01081039 pop edi
0108103A pop esi
0108103B pop ebx
0108103C mov esp,ebp
0108103E pop ebp
0108103F ret 10h
stdcall(1, 2, 3, 4);
01081093 push 4
01081095 push 3
01081097 push 2
01081099 push 1
0108109B call 01081030
이 방식은 Win32API를 호출할 때 기본적으로 사용하는 방식이다. Argument를 역순으로 저장한다. 스택의 정리는 호출된 쪽에서 수행한다.
__fastcall
void __fastcall fastcall(int a, int b, int c, int d) {}
01081010 push ebp
01081011 mov ebp,esp
01081013 sub esp,48h
01081016 push ebx
01081017 push esi
01081018 push edi
01081019 mov dword ptr [ebp-8],edx
0108101C mov dword ptr [ebp-4],ecx
0108101F pop edi
01081020 pop esi
01081021 pop ebx
01081022 mov esp,ebp
01081024 pop ebp
01081025 ret 8
fastcall(1, 2, 3, 4);
010810A0 push 4
010810A2 push 3
010810A4 mov edx,2
010810A9 mov ecx,1
010810AE call 01081010
Argument 중 왼쪽에서 두 개의 argument는 각각 ECX와 EDX 레지스터에 저장된다. 그 밖의 argument는 역순으로 스택에 저장된다. 스택의 정리는 호출된 쪽에서 수행한다.
__thiscall
class test
{
public:
void thiscall(int a, int b, int c, int d) {}
01081050 push ebp
01081051 mov ebp,esp
01081053 sub esp,44h
01081056 push ebx
01081057 push esi
01081058 push edi
01081059 mov dword ptr [ebp-4],ecx
0108105C pop edi
0108105D pop esi
0108105E pop ebx
0108105F mov esp,ebp
01081061 pop ebp
01081062 ret 10h
call.thiscall(1, 2, 3, 4);
010810B3 push 4
010810B5 push 3
010810B7 push 2
010810B9 push 1
010810BB lea ecx,[ebp-5]
010810BE call 01081050
이 방식은 C++ 클래스의 내부 멤버 함수를 호출할 때 사용된다. Argument는 역순으로 스택에 저장되고, this포인터가 ECX 레지스터에 저장된다.
종합
이름 |
스택 정리 |
argument 전달 |
기타 |
__cdecl |
Caller |
역순 |
C언어 기본 |
__stdcall |
Caller |
역순 |
Win32API |
__fastcall |
Caller |
역순 |
왼쪽의 두 인자는 ECX, EDX에 저장 |
__thiscall |
Caller |
역순 |
멤버함수 호출 방식. this포인터가 ECX에 저장 |
스택 정리, argument 전달 외에 이름 데코레이션, 대소문자 변환, 64비트 환경에서 허용 여부, argument 속성에 따른 변화 등 많은 항목이 calling convention마다 다르다. 더 자세한 내용은 MSDN(https://msdn.microsoft.com/en-us/library/984x0h58.aspx)을 참고하길 바란다.
Abex crackme 2
시리얼 인증하는 문제이다.
디버거 실행 후 문자열들을 보면 메시지 박스들에 쓰이는 문자열들을 찾을 수 있다.
실패 메시지를 출력하는 부분 위에 성공 메시지를 출력하는 부분이 있다.
Jump from 00403332을 보아 실패의 경우 00403332에서 jump됨을 알 수 있다.
00403332부분을 보면 AX가 0일 경우 실패 메시지를 출력하는 곳으로 jump한다.
00403332의 jump문을 nop로 patch한다.
Patch할 경우 key는 맞다고 출력되지만 serial은 틀렸다고 출력된다.
이 부분이 serial이 틀렸다고 출력하는 곳이다. 이 곳에서 위쪽으로 jump문들을 찾아보자.
00403424에서 serial이 틀렸을 경우 jump되지 않고 실패 메시지가 출력된다.
이 부분을 무조건 jump인 jmp로 수정한다.
Lena’s reversing for newbies
모든 Nag를 없애고 registration코드를 찾는 문제이다. 디버거에서 파일을 실행한 후 Nag Screen이 출력되었을 때 pause를 눌러 스택 프레임을 살펴보겠다.
스택 프레임을 보는 이유는 이 창을 띄운 부분의 위치를 찾기 위함이다. Return 주소가 프로그램 코드 영역인 곳이 나올 때까지 깊게 들어가다 보면 00402D03에서 이 메시지 박스를 호출했다는 걸 알 수 있다.
메시지 박스를 출력하는 00402CFE를 NOP으로 patch한다.
Patch를 하고 실행한 경우 오류가 발생한다. 스택 프레임을 조사하여 어느 코드에서 오류가 발생했는지 찾아보자.
가장 가까운 프로그램 코드 영역인 00402D5A를 수행하다가 오류가 발생하였다.
이 오류를 피하기 위해 NOP 대신 오류가 발생하는 00402D5A 코드 바로 다음의 코드인 00402D5D영역으로 jump하도록 patch한다.
Nag Screen은 오류 없이 뜨지 않게 되었다. 코드를 인증하는 부분에 아무 코드를 입력하고 Register me! 버튼을 클릭하면 위처럼 실패 메시지가 출력된다.
아까와 같은 방법으로 스택 프레임을 조사하여 어느 부분에서 메시지 박스를 출력하는지 찾아본다. 00402AED에서 메시지 박스를 출력한다.
Jump from을 따라가본다.
Jump from을 따라가다 보면 위 부분까지 오게 된다. 이 부분이 성공 여부를 결정한다. 그 위에 문자열을 비교하는 것처럼 보이는 __vbaStrCmp함수를 호출한다.
이 함수를 호출할 때 스택에 어떤 argument들을 넣는지 보니 처음에 입력한 문자열 “abcd”와 “I’mlena151” 이라는 두 문자열이 들어간다.
다시 돌아와서 Regcode에 I’mlena151을 넣어 실행해보면 성공 메시지가 출력된다.
'리버싱핵심원리' 카테고리의 다른 글
Reverse Engineering Chap 23 (0) | 2016.01.17 |
---|---|
Reverse Engineering Chap 14~15 (0) | 2016.01.14 |
Reverse Engineering Chap 16-17 (0) | 2016.01.14 |
Reverse Engineering Chap 13 (0) | 2016.01.12 |
Reverse Engineering Chap1-2 (0) | 2016.01.10 |