C++ 反汇编
配套资源下载
环境及工具
反汇编引擎工作原理(可略过)
基本数据类型的表现形式
程序的真正入口
本文档使用 MrDoc 发布
-
+
首页
程序的真正入口
**<center> 这章慢慢看,看不懂就看最后的识别 </center>** -------------- ## 程序的真正入口 - 应用程序被操作系统加载时,操作系统会**分析执行文件内的数据,并分配相关资源,读取执行文件中的代码和数据到合适的内存单元,然后才是执行入口代码**,**入口代码其实并不是 main 或 WinMain 函数**,通常是 **mainCRTStartup、wmainCRTStartup、WinMainCRTStartup 或 wWinMainCRTStartup**,具体视编译选项而定。其中 **`mainCRTStartup` 和 `wmainCRTStartup` 是控制台环境下多字节编码和 Unicode 编码的启动函数**,而 **`WinMainCRTStartup` 和 `wWinMainCRTStartup` 则是 Windows 环境下多字节编码和 Unicode 编码的启动函数**。在开发过程中,**C++也允许程序员自己指定入口**。 ## 了解启动函数 - VS C++在控制台和多字节编码环境下的启动函数为 mainCRTStartup,由系统库 KERNEL32.dll 负责调用,在 mainCRTStartup 中再调用 main 函数。我使用 VS2022 查看 main 函数之前的代码,操作如下:**在调试环境下,依次选择菜单“调试”→“窗口”→“调用堆栈”,打开出栈窗口(快捷键:Ctrl+Alt+C)**。如果显示没有的话,**右键调用堆栈窗口→“显示外部代码”**即可。 ![](/media/202404/2024-04-15_135422_0022880.7294456923496023.png) **<center>64 位控制台</center>** ![](/media/202404/2024-04-15_135731_1163250.029790311882974208.png) **<center>32 位控制台</center>** ![](/media/202404/2024-04-15_135545_8699100.5732349081562048.png) **<center>书上案例</center>** - 下面的是书上的图,可以**看到 32 和 64 位的环境下的函数是不一样的**。图中我的函数名没有像书中一样加载出来。解决方案如下:**调试环境下,依次选择菜单“调试”“窗口”“模块”**,可以看到有些模块显示“无法查找或打开 PDB 文件。”,**右键模块“加载符号”**。问题解决。 ![](/media/202404/2024-04-15_140528_3592700.7943424808822261.png)<br> ![](/media/202404/2024-04-15_140655_8676920.30186004186113913.png) 参考 [微软符号服务器下载符号总结](https://www.cnblogs.com/maifengqiang/archive/2011/10/11/2206925.html) - 关注 32 位的调用函数,程序运行时调用的8个函数,依次是 1. __RtlUserThreadStart@8 2. __RtlUserThreadStart 3. @BaseThreadInitThunk@12 4. mainCRTStartup 5. __scrt_common_main 6. __scrt_common_main_seh 7. invoke_main 8. main。 - 除了 dll 文件中被调用的函数,从 mainCRTStartup 函数开始,我们就可以看到源代码了。下面依次分析这些代码: ```c // mainCRTStartup 函数 extern "C" DWORD mainCRTStartup(LPVOID) { return __scrt_common_main(); } ``` ```c // __scrt_common_main 函数 // This is the common main implementation to which all of the CRT main functions // 翻译:这是所有 CRT 主要功能的通用主要实现 // delegate (for executables; DLLs are handled separately). // 翻译:委托(对于可执行文件;DLL 单独处理)。 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: // 翻译:在处理任何异常之前,必须初始化/GS安全cookie // 翻译:以当前图像为目标进行注册。没有使用异常的函数 // 翻译:可以在当前图像中调用handling,直到该调用之后: __security_init_cookie(); // 初始化缓冲区溢出全局变量 return __scrt_common_main_seh(); } ``` ```c // __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; // 用于初始化 C 语法中的全局数据 if (_initterm_e(__xi_a, __xi_z) != 0) return 255; // 用于初始化 C++语法中的全局数据 _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(); 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(); 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... // // 初始化完成调用 main()函数 int const main_result = invoke_main(); // // main has returned; exit somehow... // // main()函数返回执行析构函数或 atexit 注册的函数指针,并结束程序 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; } } ``` - **这里和书里面不一样,重新探究一下** ```c // invoke_main 函数 static int __cdecl invoke_main() { return main(__argc, __argv, _get_initial_narrow_environment()); } ``` - 代码贴出来了,和书上有不少区别。让我结合书上将我在 VS2022 下看到的代码重新分析一下吧: 1. **mainCRTStartup** 函数直接调用 **__scrt_common_main** 函数。 2. **__scrt_common_main** 函数中调用了 __security_init_cookie 和 **__scrt_common_main_seh** 函数。 1. __security_init_cookie 函数:初始化缓冲区溢出全局变量,用于在函数中检查缓冲区是否溢出。 3. **__scrt_common_main_seh** 函数的流程较为复杂,下面详细分析 1. __scrt_initialize_crt() 是一个运行时库的初始化函数,被用来确保 C/C++ 程序在开始执行之前已经正确地初始化了运行时环境。这个函数接受一个参数,指定当前模块的类型(exe 或者 dll)。 2. 如果 __scrt_initialize_crt() 函数返回 false,说明初始化失败,这时候会调用 __scrt_fastfail() 函数,该函数是一个内部函数,用于处理严重错误情况。在这段代码中,使用 FAST_FAIL_FATAL_APP_EXIT 参数将导致应用程序退出。 3. 接下来是控制程序的启动过程和检查当前的启动状态。通过调用 __scrt_acquire_startup_lock() 函数获取启动锁,并将返回值赋给布尔变量 is_nested。启动锁用于确保在多线程环境下只有一个线程能够执行初始化逻辑。后续根据 __scrt_current_native_startup_state 的值**进行不同的操作**: 1. __scrt_current_native_startup_state 为 __scrt_native_startup_state::initializing,表示程序正在初始化中,此时调用 __scrt_fastfail() 函数,导致应用程序退出。 2. __scrt_current_native_startup_state 为 __scrt_native_startup_state::uninitialized,表示程序尚未初始化,此时将 __scrt_current_native_startup_state 设置为 __scrt_native_startup_state::initializing。然后,调用 _initterm_e() 函数执行静态构造函数(从 __xi_a 到 __xi_z 范围内的函数),如果返回值不为 0,表示初始化失败,返回255。接着,调用 _initterm() 函数执行全局对象构造函数(从 __xc_a 到 __xc_z 范围内的函数)。最后,将 __scrt_current_native_startup_state 设置为 __scrt_native_startup_state::initialized,表示初始化完成。 3. 如果以上条件都不满足,则将 has_cctor 设置为 true 4. __scrt_release_startup_lock() 函数释放启动锁。在多线程环境下,当一个线程完成了初始化逻辑时,可以调用该函数释放启动锁,以便其他线程能够继续执行初始化过程。 5. 接下来,检查是否存在动态初始化的 __declspec(thread) 变量,并获取其初始化回调函数的指针 tls_init_callback。如果回调函数不为空且可被写入(即地址位于当前模块中),则调用此回调函数进行初始化。 6. 后续检查是否存在线程局部析构函数,并获取其销毁回调函数的指针 tls_dtor_callback。如果回调函数不为空且可被写入,则将此回调函数注册为在程序退出时执行。 7. 调用 **invoke_main()** 函数来执行 main 函数。 8. 根据程序的类型和是否存在静态构造函数,采取不同的退出方式: 1. 如果程序不是托管应用程序(即非CLR应用程序),则调用 exit(main_result) 来退出程序。 2. 如果没有静态构造函数(has_cctor 为假),则调用 _cexit() 函数来清理静态对象。 9. 调用 __scrt_uninitialize_crt(true, false) 来终止运行时库的相关操作。 4. **invoke_main** 函数:该函数获取 main 函数所需的 3 个参数信息之后,当调用 main 函数时,便可以将_argc、_argv、env 这 3 个全局变量作为参数,传递到 main 函数中。 - 下面对部分函数进行介绍: - __security_init_cookie 函数:初始化缓冲区溢出全局变量,用于在函数中检查缓冲区是否溢出。 - _initterm_e 函数:用于全局数据和浮点寄存器的初始化,该函数由两个参数组成,类型为“_PIFV *”,这是一个函数指针数组,其中保留了每个初始化函数的地址。初始化函数的类型为_PIFV,其定义原型如下所示。 ```c typedef int (__cdecl* _PIFV)(void); ``` 如果初始化失败,返回非 0 值,程序终止运行。一般而言,_initterm_e 初始化的都是 C 语言支持库中所需的数据。参数 _xi_a 为函数指针数组的起始地址,_xi_z 为结束地址,具体如代码所示: ```c extern "C" int __cdecl _initterm_e(_PIFV* const first, _PIFV* const last) { for (_PIFV* it = first; it != last; ++it) { if (*it == nullptr) continue; int const result = (**it)(); if (result != 0) return result; } return 0; } ``` - _initterm 函数:C++全局对象和 IO 流等的初始化都是通过这个函数实现的,可以利用_initterm 函数进行数据链初始化。这个函数由两个参数组成,类型为“_PVFV *”,这也是一个函数指针数组,其中保留了每个初始化函数的地址。初始化函数的类型为_PVFV,其定义原型如下所示。 ```c typedef void (_cdecl *_PVFV)(void); ``` 也就是说,这个初始化函数是无参数也无返回值的。大家知道,C++规定全局对象和静态对象必须在 main 函数前构造,在 main 函数返回后析构。所以,这里的_PVFV 函数指针数组就是用来代理调用构造函数的,具体如代码所示。 ```c extern "C" void __cdecl _initterm(_PVFV* const first, _PVFV* const last) { for (_PVFV* it = first; it != last; ++it) { if (*it == nullptr) continue; (**it)(); } } ``` C++所需数据的初始化操作会在如代码所示的_initterm 函数调用时执行,一般都是全局对象或静态对象初始化函数。 - __scrt_get_dyn_tls_init_callback 函数:获取线程局部存储(TLS)变量的回调函数,用于初始化使用__declspec(thread)定义的变量。 - __scrt_get_dyn_tls_dtor_callback 函数:获取线程局部存储变量的析构回调函数,用于注册析构回调函数。 - invoke_main 函数:该函数获取 main 函数所需的 3 个参数信息之后,当调用 main 函数时,便可以将_argc、_argv、env 这3个全局变量作为参数,传递到main函数中。 - exit 函数:执行析构函数或 atexit 注册的函数指针,并结束程序。 ## main 函数的识别 - 控制台程序 main 函数有 3 个参数,分别是命令行参数个数、命令行参数信息和环境变量信息,而且 main 函数是启动函数中唯一具有 3 个参数的函数。同理,WinMain 也是启动函数中唯一具有 4 个参数的函数。main 函数返回后需要调用 exit 函数,结束程序根据 main 函数调用的特征,**找到入口代码第一次调用 exit 函数处,离 exit 最近的且有 3 个参数的函数通常就是 main 函数(有 4 个参数的函数通常就是 WinMain 函数)。** - 利用 exit 函数定位,**如果 IDA 无法识别出 exit 函数,就需要加载 sig 文件重新识别**。
别卷了
2024年4月16日 09:41
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码