在整个程序的逆向分析过程中,寻找 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 版本下应用程序入口的调用图: