Sh4n3e

Reversing[2] 본문

System Hacking/Reverse Engineering

Reversing[2]

sh4n3e 2015. 10. 6. 20:25

오늘은 간단하게 프로그램의 구조와 프로그램내에 존재하는 Stack영역에 대해서 볼 생각입니다.


이번 시간이 끝나게되면, 프로그램의 구조를 좀더 자세히 쪼개어 PE(Portable Excutable) File에 대해서 한번 알아볼 것입니다. 


우리가 사용하는 Windows 32bit Application Program의 경우 PE구조를 지니고 있습니다. 이 PE구조를 크게 보면 아래의 그림과 같을 것 입니다.



이러한 콘텐츠를 좀더 잘게 잘라서 생각해보면, TEXT영역, DATA영역, RESOURCE DATA영역 3가지로 나눠볼 수 있습니다. TEXT영역의 경우 실제 변환된 코드가 들어가 있으며, DATA영역은 String정보, RESOURCE DATA영역의 경우 리소스에 관련된 기타 정보들을 담고 있습니다. PE구조에 대한 자세한 내용은 다음시간에 하도록 하겠습니다.


PE구조를 하기 앞서, 프로그램과 메모리의 관계와 그 메모리 내의 Stack이 어떻게 사용되는지에 대해서 알아보도록 하겠습니다. 


위의 그림을 보시면 파일시스템(즉, HDD, SSD 영역)내에 Binary형태로 저장되어있는 프로그램을 실행하면 프로그램은 메모리영역에 올라가서 실행되게 됩니다. 그 중에 프로그램의 동작의 상태를 저장하고, 동작을 보조하는 부분이 Stack이라는 영역입니다. 그리고 사용자가 자유롭게 메모리를 할당하거나 해제할 수 있는 영역(즉, malloc와 같은 함수)이 바로 heap 영역입니다.


그럼 stack영역에 대해서 조금더 자세히 알아보겠습니다. 스택영역에서 사용하는 레지스터는 대표적으로 3가지의 레지스터를 사용합니다. 현재의 위치를 가리키기 위한 ESP(Extended Stack Pointer), 현재의 스택의 Base가 되는 부분을 가리키기 위한 EBP(Extentded Base Pointer), 다음에 실행할 명령의 Offset을 가리키기 위한 EIP(Extended Instruction Pointer)가 있습니다. 여기서 E가 붙은 이유는 16bit에서 32bit로 확장되면서 32bit로 사용됨을 알려주기 위함입니다. 

좀 더 간략하게 알아볼 수 있도록 소스코드와 그림을 비교하며 보도록 하겠습니다.


void func1(){

int i,cnt=0; // 지역변수

for(i=0;i<10;i++){

cnt++;

}

}

int main(){

func1(); // func1() 호출

return 0;

}

우선적으로 stack의 용어에 대한 정의를 하자면, 한 쪽 끝에서만 데이터를 넣거나 뺄 수 있는 선형 구조 입니다. 자료를 넣는 것을 PUSH라고 하며, 자료를 꺼내는 것을 POP이라고 합니다.

위의 그림은 메모리 상에서 스택을 어떻게 사용하는지에 대한 모습을 보여주고 있습니다. EBP는 쉽게 말하면 현재 함수 영역의 경계선이라고 생각하면되고, ESP는 사용중인 stack의 현재 위치를 나타낸다고 보면 되겠습니다. 

func1의 보면 RET라는 부분이 보이는데, 이것은 Return Address의 부분으로 다시 main의 영역으로 넘어가기 위한 부분입니다. 메인 프로그램이 실행되는 도중에 func1이라는 함수가 호출이 되면 새로운 func1 함수에게 새로운 스택을 할당하게 됩니다. 이 때 func1 함수의 실행이 끝나면 다시 메인 함수로 돌아오기 위해서 RET가 존재하는 것입니다. SFP는 이전 main함수의 Base Pointer를 저장하고 있다고 생각하시면 되겠습니다.

간단하게 이런 함수의 영역을 보여주는 어셈블러 코드는 다음과 같습니다. 윗 부분은 함수의 프롤로그, 아랫 부분은 함수의 에필로그로 보시면 되겠습니다.


PUSH EBP

MOV EBP, ESP

...

MOV ESP, EBP

POP EBP

RETN


이런 함수의 호출에는 규약이 따로 존재하게 됩니다. 함 수 호출 규약은 프로그램이 함수에게 파라미터를 전달하고 결과값을 다시 받는 일련의 표준화된 방법입니다. 즉, 함수 호출을 위해서 밟는 절차를 정해둔 것이라고 보시면 됩니다. 함수 호출 규약에는 총 5가지 규약이 존재합니다.(stdcall, cdecl, thiscall, fastcall, naked) 하지만 여기서는 자주 사용되는 호출 규약인 cdecl과 stdcall에 대해서 간략하게 설명하고 넘어가겠습니다.


함수를 호출함에 있어서 함수를 호출하는 놈(Caller, 콜러)과 함수를 호출받은 놈(Callee, 콜리)이 존재합니다. cdecl과 stdcall은 이 Caller과 Callee의 역할의 차이에서 구분됩니다.

우선 cdecl은 C컴파일러 혹은 C++ 컴파일러에서 전역 함수나 전역 스태틱 함수, 스태틱 멤버 함수 호출에 기본적으로 사용되는 규약으로 멤버 함수의 오른쪽 인자부터 왼쪽 인자 순으로 스택에 집어 넣고, 함수 호출이 종료된 뒤 Caller가 스택에 집어 넣은 인자를 제거합니다. 또한 리턴 값이 있는 경우 레지스터 eax를 통해서 리턴 값을 돌려받게 됩니다. 이렇게 정리하는 부분은 보통 call 명령어 다음에 add esp, X를 이용하여 스택을 정리합니다.

다음으로 stdcall은 윈도우 플랫폼에서 시스템 API를 호출할 때 사용하는 규약입니다. cdecl과 마찬가지로 멤버 함수의 오른쪽 인자부터 왼쪽 인자 순으로 스택에 집어 넣고, 함수 호출이 종료되면 Callee가 인자를 스택에서 제거하며, 리턴 값이 있는 경우 레지스터 eax를 통해서 리턴 값을 돌려받게 됩니다. 이때는 Callee가 스택에서 인자를 제거해야되기 때문에 가변 개수의 인자를 전달할 수 없으며 retn X를 이용해 스택을 정리합니다.


이번 시간은 여기까지 마무리하고, 다음시간에는 PE에 대해서 다뤄보도록 하겠습니다.






'System Hacking > Reverse Engineering' 카테고리의 다른 글

Debugger 단축키  (0) 2017.06.16
DLL 작성 및 로딩 방법  (0) 2015.12.24
Reversing[4] - PE(2)  (0) 2015.10.08
Reversing[3] - PE(1)  (0) 2015.10.07
Reversing[1]  (0) 2015.10.06
Comments