在整個程式的逆向分析過程中,尋找 main 函式是逆向分析過程的第一步,程式的主要邏輯從這裡展開。
這裡面涉及到兩個概念:使用者入口(User Entry Point)
和 應用程式入口(Application Entry Point)
。
使用者入口
使用者入口是開發者編寫的用於程式開始的函式。對於大多數 C/C++ 程式而言,這個入口函式通常是
main
,也可以是WinMain
(在 Windows GUI 程式中)或其他使用者定義的入口函式。
應用程式入口
應用程式入口是作業系統在載入可執行檔案時呼叫的第一個程式碼位置,當我們將程式拖入 x64dbg 後第一個斷下的地方就是應用程式入口點。這個位置通常是由編譯器或聯結器自動生成的,它負責初始化執行時環境,初始化完成後就會跳到使用者入口(
main
函式或WinMain
函式)
在逆向工程中,透過理解和識別這兩個不同的入口點,可以更好地分析程式的結構和執行流程。例如,透過定位應用程式入口,你可以看到如何設定和呼叫使用者入口函式;而透過分析使用者入口函式,可以理解程式的主要邏輯和功能。
接下來我們會分析,如何分別在 32 位 和 64 位程式的 Debug 和 Release 版本中找到 main
函式入口點,我們的實驗環境是 VS2019。
一、對使用者入口(main 函式)進行深入理解
在進行實驗之前,首先我們需要重新認識一下 main 函式,知己知彼方能百戰百勝。
我們都知道,C/C++ 程式的 main 函式其實歸根到底也是一個函式,那麼這個函式有沒有引數呢?其實是有的,大家會發現我們可以在 main 函式中填入引數,也可以不填入引數,其實都是可以編譯透過的,在 main 函式中填入引數我們可以在命令列中進行呼叫,更加的方便靈活。
其實,不管我們有沒有填入引數,在我們對 main 函式的逆向過程中都會發現,在執行 main 函式之前,都會壓入 3 個引數,那麼我們是不是可以透過這個特點來定位 main 函式的位置呢?答案是肯定的。
其實 main 函式傳遞的三個引數分別是:int argc(引數個數)
、char *argv[](引數)
和 char *envp[](環境變數)
。
前面兩個大家用的比較多,最後一個環境變數引數大家可能不是很瞭解,我們可以透過下面這段程式碼對 main 函式的引數有一個直觀的瞭解:
#include <stdio.h>
#include <Windows.h>
int main(int argc, char *argv[], char *envp[])
{
printf("引數個數:%d\r\n\n", argc);
for (int i = 0; i < argc; i++)
{
printf("Argument%d:%s\r\n", i, argv[i]);
}
printf("\r\n");
printf("環境變數:\r\n");
int i = 0;
for (char** env = envp; *env != 0; env++, i++)
{
char* curEnv = *env;
printf("Enviroment Variable%d:%s\r\n", i, curEnv);
}
system("pause");
return 0;
}
我們透過命令列進行呼叫,輸入 3 個引數:
得到的結果如下:
二、對應用程式入口進行深入理解
有過逆向經驗的朋友都知道,在執行 main 函式之前,其實是有一段用於負責初始化執行時環境的程式碼,當我們將程式拖入 x64dbg 中,會在應用程式入口斷下,那麼我們可以透過 VS2019 自己隨便編寫一個程式,此時會在程式目錄生成 exe 可執行檔案和符號檔案,正常情況下我們在分析別人程式的時候是不會帶有符號檔案的,但是如果我們自己編寫一個程式,在符號檔案的幫助下,大大降低我們的逆向難度。比如沒有符號檔案,我們無法在 x64dbg 中直接跳轉到 main 函式入口,但是有符號檔案就可以,而且很多的 call 都會標明對應的函式名,而不是一個冰冷的地址,感興趣的朋友可以去對比一下有符號檔案和沒有符號檔案逆向過程的區別。
當然我們今天要介紹的不是帶著符號檔案逆向,而是從正向開發的角度,看看在執行 main 函式之前到底進行了什麼操作,對這段程式碼有一個直觀的瞭解,才能更胸有成竹的找到 main 函式入口。
首先,我們來看一下 main 函式的呼叫棧:
我們可以看到,main 函式的呼叫棧是:mainCRTStartup()
-> __scrt_common_main()
-> __scrt_common_main_seg()
-> invoke_main()
。
對 main 函式的呼叫棧有一個大概的流程瞭解後,我們來看一下它對應的正向開發程式碼:
mainCRTStartup()
// The implementation of the common executable entry point code. There are four
// executable entry points defined by the CRT, one for each of the user-definable
// entry points:
//
// * mainCRTStartup => main
// * wmainCRTStartup => wmain
// * WinMainCRTStartup => WinMain
// * wWinMainCRTStartup => wWinMain
//
// These functions all behave the same, except for which user-definable main
// function they call and whether they accumulate and pass narrow or wide string
// arguments. This file contains the common code shared by all four of those
// entry points.
//
// The actual entry points are defined in four .cpp files alongside this .inl
// file. At most one of these .cpp files will be linked into the resulting
// executable, so we can treat this .inl file as if its contents are only linked
// into the executable once as well.
extern "C" int mainCRTStartup()
{
return __scrt_common_main();
}
__scrt_common_main()
// This is the common main implementation to which all of the CRT main functions
// delegate (for executables; DLLs are handled separately).
static __forceinline int __cdecl __scrt_common_main()
{
// The /GS security cookie must be initialized before any exception handling
// targeting the current image is registered. No function using exception
// handling can be called in the current image until after this call:
__security_init_cookie();
return __scrt_common_main_seh();
}
關於 __security_init_cookie()
函式可以參照微軟官方文件:
全域性安全 Cookie 用於使用 /GS (緩衝區安全檢查) 編譯的程式碼和使用異常處理的程式碼中的緩衝區溢位保護。在進入受溢位保護的函式時,cookie 被放在堆疊上,在退出時,堆疊上的值與全域性 cookie 進行比較。它們之間的任何差異都表明發生了緩衝區溢位,並導致程式立即終止。
通常,__security_init_cookie 在初始化時由 CRT 呼叫。如果繞過 CRT 初始化(例如,如果使用 /ENTRY 指定入口點),則必須自行呼叫__security_init_cookie。如果未呼叫 __security_init_cookie,則全域性安全 Cookie 將設定為預設值,並且緩衝區溢位保護會受到損害。由於攻擊者可以利用此預設 Cookie 值來破壞緩衝區溢位檢查,因此我們建議您在定義自己的入口點時始終呼叫 __security_init_cookie。
對 __security_init_cookie 的呼叫必須在輸入任何 overrun protected 函式之前進行;否則將檢測到虛假的緩衝區溢位。
__scrt_common_main_seh()
static __declspec(noinline) int __cdecl __scrt_common_main_seh()
{
if (!__scrt_initialize_crt(__scrt_module_type::exe))
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);
bool has_cctor = false;
__try
{
bool const is_nested = __scrt_acquire_startup_lock();
if (__scrt_current_native_startup_state == __scrt_native_startup_state::initializing)
{
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);
}
else if (__scrt_current_native_startup_state == __scrt_native_startup_state::uninitialized)
{
__scrt_current_native_startup_state = __scrt_native_startup_state::initializing;
if (_initterm_e(__xi_a, __xi_z) != 0)
return 255;
_initterm(__xc_a, __xc_z);
__scrt_current_native_startup_state = __scrt_native_startup_state::initialized;
}
else
{
has_cctor = true;
}
__scrt_release_startup_lock(is_nested);
// If this module has any dynamically initialized __declspec(thread)
// variables, then we invoke their initialization for the primary thread
// used to start the process:
_tls_callback_type const* const tls_init_callback = __scrt_get_dyn_tls_init_callback();//tls init
if (*tls_init_callback != nullptr && __scrt_is_nonwritable_in_current_image(tls_init_callback))
{
(*tls_init_callback)(nullptr, DLL_THREAD_ATTACH, nullptr);
}
// If this module has any thread-local destructors, register the
// callback function with the Unified CRT to run on exit.
_tls_callback_type const * const tls_dtor_callback = __scrt_get_dyn_tls_dtor_callback();//tls destructor
if (*tls_dtor_callback != nullptr && __scrt_is_nonwritable_in_current_image(tls_dtor_callback))
{
_register_thread_local_exe_atexit_callback(*tls_dtor_callback);
}
//
// Initialization is complete; invoke main...
//
int const main_result = invoke_main();
//
// main has returned; exit somehow...
//
if (!__scrt_is_managed_app())
exit(main_result);
if (!has_cctor)
_cexit();
// Finally, we terminate the CRT:
__scrt_uninitialize_crt(true, false);
return main_result;
}
__except (_seh_filter_exe(GetExceptionCode(), GetExceptionInformation()))
{
// Note: We should never reach this except clause.
int const main_result = GetExceptionCode();
if (!__scrt_is_managed_app())
_exit(main_result);
if (!has_cctor)
_c_exit();
return main_result;
}
}
invoke_main()
static int __cdecl invoke_main()
{
return main(__argc, __argv, _get_initial_narrow_environment());
}
三、在 32 位和 64 位程式的 Debug 版本中尋找 main 函式
通常來說,Debug 版本會最大程度保留和原始程式碼一樣的結構,因此對照上面的程式碼,結合呼叫 main 函式之前必定傳入 3 個引數這個特點,我們可以很快找到 main 函式的入口點,下面是 Debug 版本下應用程式入口的呼叫圖: