우연히 삽질 후 알게 된 메인함수의 뒷 이야기..?

2018. 3. 14. 21:180x03 Reversing Theory

728x90

집에 와서 디버깅을 조금 해보다가 재밌는것을 알게 되었습니다.


제목은 우연히 삽질 후 알게 된 메인함수의 뒷 이야기입니다. 


많은 이들이 메인함수가 끝났을 때 return 0을 습관처럼 사용합니다. 

무심결에 return 0을 사용하는데 그 근본을 알고 싶어 이번에 우연찮게 시도해보게 되었습니다.


테스트에 사용되었던 코드입니다.

#include <stdio.h>

#pragma warning(disable:4716)

char format[] = "ㅋ";


int i = 0;

__declspec(naked)int main(int argc, char*argv[])

{

__asm

{

push ebp

mov ebp,esp

mov ecx, i

compare:

cmp ecx,0x4

jl  _loop

je _out

_loop:

mov eax, offset format

push eax

call printf

xor ecx, ecx

add i, 0x1

mov ecx, i

jmp compare

_out:

pop eax

mov esp,ebp

pop ebp

retn


}

}



디버거가 메인의 에필로그 까지 디버깅을 하고 retn을 빠져 나갔을 때 이러한 코드를 만나게 됩니다.

static int __cdecl invoke_main() throw()

    {

        return main(__argc, __argv, _get_initial_narrow_environment());

    }

적힌 메인 코드의 반환 값은 0이기 때문에 invoke_main에도 0이 들어가게 될 것입니다.


throw라고 적혀있는 것으로 보아 예외처리에도 한 몫 할 것 같다는 생각이 듭니다. 


다시 코드에 대한 얘기로 돌아가보겠습니다.


그 후 만나게 되는 코드입니다.

        int const main_result = invoke_main();

invoke_main에서 main함수의 반환 값 0을 반환받았기 때문에 main_result 역시 EAX 레지스터에 의거하여 0을 반환 값으로 이용할 겁니다.



그 이후에 scrt를 만나서 이러한 조건문도 만나게 되는데요.

if(!__scrt_is_managed_app()) 

{

exit(main_result);

}


해당 조건문도 한번 들어가보겠습니다.


extern "C" bool __cdecl __scrt_is_managed_app()

{

    PIMAGE_DOS_HEADER const dos_header = reinterpret_cast<PIMAGE_DOS_HEADER>(GetModuleHandleW(nullptr));

    if (dos_header == nullptr)

        return false;


    if (dos_header->e_magic != IMAGE_DOS_SIGNATURE)

        return false;


    PIMAGE_NT_HEADERS const pe_header = reinterpret_cast<PIMAGE_NT_HEADERS>(

        reinterpret_cast<BYTE*>(dos_header) + dos_header->e_lfanew);


    if (pe_header->Signature != IMAGE_NT_SIGNATURE)

        return false;


    if (pe_header->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC)

        return false;


    // prefast assumes we are overrunning __ImageBase

    #pragma warning(push)

    #pragma warning(disable: 26000)


    if (pe_header->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR)

        return false;


    #pragma warning(pop)


    if (pe_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress == 0)

        return false;


    return true;

}


 이곳이 __scrt_is_managed_app()인데요.

가만히 들여다보면 PE의 시그니처도 살펴보기도 하는걸로 보아 4D 5A가 아닐 때 여기서 인지를 한다고도 

재밌게 추측을 해볼 수 있네요

그 다음 헤더부분의 rva와 size를 엔트리 디스크립터와 비교를 하게되고 정확하겐 모르겠다만 직역을 쭉 해보면 


이 함수의 특징은 우선 GetModuleHandle에 의해 현재 프로세스의 핸들을 반환-> 이것으로 베이스 주소 추출가능

(베이스주소 추출가능 이유는 PIMAGE_DOS_HEADER 구조체 dos_header이기 때문이겠죠.

PIMAGE_DOS_HEADER는 제가 알기로 파일 시그니처를 체크하는 놈이 있어요. 아니나 다를까 두번째 비교문에서 시그니처

체크를 하고 있군요.


구조체에서 -> 라는 의미는 그 안의 참조값을 들여다 보겠다는 건데요.

즉 구조체의 포인터입니다. 

pe_header안에 Signature가 있어요. 

잠시 PEVIEW를 참조한 결과 

typedef struct IMAGE_NT_HEADERS

{

   Signature

   IMAGE_FILE_HEADER

   IMAGE_OPTIONAL_HEADER 

}pe_header; 이렇게 쓰이는 구조입니다. 


아니나 다를까 위 소스코드 구조체 형태를 보면 Signature, OptionalHeader가 있지요

OptionalFileHeader는 지금 보이지 않는 듯 하군요. 


pe_header->OptionalHeader.Magic 이런건 어떻게 해석을 하냐면

IMAGE_NT_HEADERS의 별칭 pe_header구조체의 포인터 OptionalHeader에 접근해서

그 안의 멤버 Magic을 들여다 봐라 이런식으로 해석을 하면 됩니다.


PE 구조 그 자체를 모두 다 설명할 순 없어요. 저도 아직 모든 섹션과 데이터의 Description을 

다 알진 못하기 때문에


지금 모든 조건문에 만족하지 못하다가 이 조건문에서 만족을 하게 됩니다.

f (pe_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].VirtualAddress == 0)

        return false;


이 부분은 이해가 덜 되어있어서 이해 되는 대로 포스팅에 추가하도록 하겠습니다.

 

if (!__scrt_is_managed_app()) // 여기서 false 성립 

 exit(main_result); // exit(0)이 되어서 정상종료가 됨.


리버싱에 조금이라도 깊게 관심이 있다면 아무 exe 열어서 PEVIEW로 저 구조체 형태를 따라가보시면

놀라운 공부가 될 거라고 생각해서 글을 써보게 되었습니다