[参考]mainCRTStartup分析
mainCRTStartup分析
mainCRTStartup
程序由mainCRTStartup
开始执行。这里的启动函数可能为下述四种之一。本文分析mainCRTStartup
#pragma comment( linker, "/subsystem:windows /entry:WinMainCRTStartup" )
#pragma comment( linker, "/subsystem:windows /entry:mainCRTStartup" )
#pragma comment( linker, "/subsystem:console /entry:mainCRTStartup" )
#pragma comment( linker, "/subsystem:console /entry:WinMainCRTStartup" )
其通用启动源码如下
__int64 __fastcall mainCRTStartup(void *__formal)
{
_security_init_cookie();
return _scrt_common_main_seh();
}
_security_init_cookie
启动后,首先执行安全存根校验_security_init_cookie
,其代码如下
void __cdecl _security_init_cookie()
{
uintptr_t v0; // rax
unsigned __int64 v1; // [rsp+20h] [rbp-10h] BYREF
_FILETIME SystemTimeAsFileTime; // [rsp+40h] [rbp+10h] BYREF
LARGE_INTEGER PerformanceCount; // [rsp+48h] [rbp+18h] BYREF
v0 = _security_cookie;
if ( _security_cookie == 0x2B992DDFA232i64 )
{
SystemTimeAsFileTime = 0i64;
GetSystemTimeAsFileTime(&SystemTimeAsFileTime);
v1 = (unsigned __int64)SystemTimeAsFileTime;
v1 ^= GetCurrentThreadId();
v1 ^= GetCurrentProcessId();
QueryPerformanceCounter(&PerformanceCount);
v0 = ((unsigned __int64)&v1 ^ v1 ^ PerformanceCount.QuadPart ^ ((unsigned __int64)PerformanceCount.LowPart << 32)) & 0xFFFFFFFFFFFFi64;
if ( v0 == 0x2B992DDFA232i64 )
v0 = 0x2B992DDFA233i64;
_security_cookie = v0;
}
_security_cookie_complement = ~v0;
}
代码中,所谓_security_cookie
是指/GS (Buffer Security Check)技术,此技术自VS2003就被引入。简单地说,该技术在所有函数的返回地址前面加上一个数据,每次返回时检查这个数据是否被篡改,如果是则进入溢出异常处理程序,阻止了返回地址覆盖rce。
上面的代码中,v0
的值由.data
段的第一个dword
确定,并在后续的一系列异或操作之后被写回.data
段。其绕过参考内存保护机制及绕过方案——通过覆盖虚函数表绕过/GS机制 - zhang293 - 博客园 (cnblogs.com)
_scrt_common_main_seh
_security_init_cookie
结束后,进入到_scrt_common_main_seh
函数。此函数主要完成下述工作:
- 初始化运行时环境:初始化 C/C++ 运行时环境,包括全局变量、静态对象的构造函数调用等。
- 调用程序的入口函数:调用程序的真正入口函数,通常是
main
函数或wmain
函数,从这里开始执行程序的主要逻辑。 - 异常处理:设置结构化异常处理(SEH)机制,用于捕获和处理可能发生的异常。如果程序执行过程中发生异常,SEH 会将控制流转移到相应的异常处理代码。
_scrt_initialize_crt
首先,此函数调用_scrt_initialize_crt
,其源代码如下
// src\vcruntime\vcstartup_internal.h
// 这些函数在启动和终止过程中被调用,用于初始化或取消初始化 CRT。
// 当 CRT 静态链接时,它们实际上执行完整的 CRT 初始化,调用每个 CRT 部分来进行初始化。
// 当使用 CRT DLLs 时,CRT DLLs 在加载时自行初始化。
// 然而,仍然需要进行一些初始化操作,将模块特定的 VCRuntime DLL 绑定到全局的 AppCRT DLL。
extern "C" bool __cdecl __scrt_initialize_crt(__scrt_module_type module_type);
// src\vcruntime\utility.cpp
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// CRT Initialization
//
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
static bool is_initialized_as_dll;
extern "C" bool __cdecl __scrt_initialize_crt(__scrt_module_type const module_type)
{
if (module_type == __scrt_module_type::dll)
{
is_initialized_as_dll = true;
}
__isa_available_init();
// 通知CRT自下而上进行初始化
if (!__vcrt_initialize())
{
return false;
}
if (!__acrt_initialize())
{
__vcrt_uninitialize(false);
return false;
}
return true;
}
读到这里,我们要考虑一个问题。如果编译产生的文件引入了其他依赖(例如dll
文件)那么这些文件的初始化函数应当如何执行呢,尤其是,按照什么顺序执行。
在静态链接的情况下,所有的依赖关系在编译和链接时就被解析并固定下来。每个模块在链接时会直接包含所依赖的模块的代码和数据,形成一个整体的可执行文件。因此,在静态链接的情况下,依赖关系是明确的,不会有动态的变化。
在动态链接的情况下,模块在运行时通过加载外部的 DLL 文件来解决依赖关系。在加载模块时,操作系统会根据模块的导入表(Import Table)查找并加载所依赖的 DLL。导入表中记录了模块所需的函数和符号以及它们所在的 DLL。加载器会按照导入表的顺序逐个加载依赖的 DLL,确保被依赖的模块在使用时可用。
而上述代码中,就按照此依赖关系逐个初始化这些模块。
_scrt_fastfail
如果crt初始化失败,则进行错误处理(即_scrt_fastfail
)其代码如下
void __fastcall _scrt_fastfail(unsigned int code)
{
unsigned __int64 v2; // rbx
_IMAGE_RUNTIME_FUNCTION_ENTRY *v3; // rax
bool v4; // bl
struct _EXCEPTION_POINTERS ExceptionInfo; // [rsp+40h] [rbp-C0h] BYREF
int v6[4]; // [rsp+50h] [rbp-B0h] BYREF
unsigned __int64 v7; // [rsp+60h] [rbp-A0h]
_CONTEXT ContextRecord; // [rsp+F0h] [rbp-10h] BYREF
unsigned __int64 retaddr; // [rsp+5C8h] [rbp+4C8h]
__int64 v10; // [rsp+5D0h] [rbp+4D0h] BYREF
unsigned __int64 ImageBase; // [rsp+5D8h] [rbp+4D8h] BYREF
unsigned __int64 EstablisherFrame; // [rsp+5E0h] [rbp+4E0h] BYREF
PVOID HandlerData; // [rsp+5E8h] [rbp+4E8h] BYREF
if ( IsProcessorFeaturePresent(0x17u) )
__fastfail(code);
_crt_debugger_hook(3);
memset_0(&ContextRecord, 0, sizeof(ContextRecord));
RtlCaptureContext(&ContextRecord);
v2 = ContextRecord.Rip;
v3 = RtlLookupFunctionEntry(ContextRecord.Rip, &ImageBase, 0i64);
if ( v3 )
RtlVirtualUnwind(0, ImageBase, v2, v3, &ContextRecord, &HandlerData, &EstablisherFrame, 0i64);
ContextRecord.Rip = retaddr;
ContextRecord.Rsp = (unsigned __int64)&v10;
memset_0(v6, 0, 0x98ui64);
v7 = retaddr;
v6[0] = 1073741845;
v6[1] = 1;
ExceptionInfo.ExceptionRecord = (_EXCEPTION_RECORD *)v6;
v4 = IsDebuggerPresent();
ExceptionInfo.ContextRecord = &ContextRecord;
SetUnhandledExceptionFilter(0i64);
if ( !UnhandledExceptionFilter(&ExceptionInfo) && !v4 )
_crt_debugger_hook(3);
}
详细的源代码见VC-LTL/_msvcrt.cpp at ce0a53460cfb5a6967787f21a5c5cb1f172f024c · BDZNH/VC-LTL (github.com),
这段代码的逆向结果也很有意思,IsProcessorFeaturePresent(0x17u)
的参数为PF_FASTFAIL_AVAILABLE
,即如果支持__fastfail
操作,那么此函数立即执行快速失败。否则就进行一系列尝试进行异常处理的过程。
异常处理过程如下
首先通知调试器出现非法参数异常(参见Mingw/x86_64-w64-mingw32/include/crtdbg.h at master · go-vgo/Mingw · GitHub)
根据上面这个文档给出的参考,函数_crt_debugger_hook
的参数定义如下
#define _CRT_WARN 0
#define _CRT_ERROR 1
#define _CRT_ASSERT 2
#define _CRT_ERRCNT 3
因此首先通知调试器出现异常上下文(ERRCNT),接着进行VirtualUnwinding过程,具体而言,虚拟展开是一种在异常处理过程中恢复调用堆栈状态的机制。当发生异常时,操作系统需要找到合适的异常处理程序来处理异常,并且需要将调用堆栈恢复到异常发生前的状态。虚拟展开是指在这个过程中,根据函数表中的信息逐级恢复调用堆栈上的每个栈帧。
在这个函数中,可以明确的看到几次通知调试器的操作,也就正好对应上异常处理的几次时机。
_scrt_acquire_startup_lock
接下来,代码获取启动锁。启动锁是一种用于多线程同步的机制,用于确保在多线程环境下只有一个线程可以执行关键的启动代码。在应用程序启动过程中,存在一些需要在多个线程之间共享的全局资源或状态,这些资源需要在多线程访问时进行同步,以避免竞态条件和不确定的行为。获取此锁的方法如下
-
检查当前组件(dll)是否正在被使用
-
如果是,则根据
NtCurrentTeb()->Reserved1[1]
的值获取_scrt_native_startup_lock
-
轮询等待这个资源(这里使用了死循环的方法,一直等到这个资源被成功的由原子操作
_InterlockedCompareExchange64
获取到。
函数原型如下LONG64 InterlockedCompareExchange64( [in, out] LONG64 volatile *Destination, [in] LONG64 ExChange, [in] LONG64 Comperand );
对指定值执行原子比较和交换操作。 该函数根据比较结果将两个指定的 64 位值进行比较,并与另一个 64 位值交换。
initterm_e_0
接下来进入crt的初始化环节。变量_scrt_current_native_startup_state
控制初始化的执行流。
初始化时,此变量被设置为initializing
,即1
if ( initterm_e_0(_xi_a, _xi_z) )
return 255i64;
initterm_0(_xc_a, _xc_z);
此处代码执行初始化和清理流程。
initterm_e_0(_xi_a, _xi_z)
:这个函数接受两个参数_xi_a
和_xi_z
,它们表示一个对象或变量的初始化函数表的起始地址和结束地址。该函数的目的是按顺序调用这些初始化函数,对相关的对象或变量进行初始化。如果调用成功并且所有初始化函数都返回零(表示成功),则条件判断为假。- 如果条件判断为假,即初始化函数执行成功且返回值为零,那么执行下一步。否则返回
255
,表示初始化失败。 initterm_0(_xc_a, _xc_z)
:这个函数接受两个参数_xc_a
和_xc_z
,它们表示一个对象或变量的清理函数表的起始地址和结束地址。该函数的目的是按逆序调用这些清理函数,对相关的对象或变量进行清理或释放。这里并没有检查函数的返回值。
参考_initterm、_initterm_e | Microsoft Learn,
我编写的程序如下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int func()
{
OutputDebugStringA("This is global initialization function!");
int d = GetFileAttributesA("D:\\Hello");
return sizeof(__int64) * d;
}
int c = func();
int main(int argc, char ** argv) {
printf("%d",c);
return 0;
}
其中,_xi_a
和_xi_z
的布局如下
.rdata:0000000140002208 ; int (__fastcall *_xi_a[])()
.rdata:0000000140002208 00 00 00 00 00 00 00 00 __xi_a dq 0 ; DATA XREF: __scrt_common_main_seh+54↑o
.rdata:0000000140002210 ; int (__fastcall *pre_c_initializer)()
.rdata:0000000140002210 F0 10 00 40 01 00 00 00 pre_c_initializer dq offset pre_c_initialization
.rdata:0000000140002218 ; int (__fastcall *post_pgo_initializer)()
.rdata:0000000140002218 A8 11 00 40 01 00 00 00 post_pgo_initializer dq offset post_pgo_initialization
.rdata:0000000140002220 ; int (__fastcall *_xi_z[])()
.rdata:0000000140002220 00 00 00 00 00 00 00 00 __xi_z dq 0 ; DATA XREF: __scrt_common_main_seh+4D↑o
initterm_e_0
会按照顺序调用其中的所有函数,这里的所有初始化代码都会执行。同样的,_xc_a, _xc_z
如下
.rdata:00000001400021E8 ; void (__fastcall *_xc_a[])()
.rdata:00000001400021E8 00 00 00 00 00 00 00 00 __xc_a dq 0 ; DATA XREF: __scrt_common_main_seh+75↑o
.rdata:00000001400021F0 ; void (__fastcall *pre_cpp_initializer)()
.rdata:00000001400021F0 B8 11 00 40 01 00 00 00 pre_cpp_initializer dq offset pre_cpp_initialization
.rdata:00000001400021F8 ; void (__fastcall *c_initializer_)()
.rdata:00000001400021F8 00 10 00 40 01 00 00 00 c$initializer$ dq offset _dynamic_initializer_for__c__
.rdata:0000000140002200 ; void (__fastcall *_xc_z[])()
.rdata:0000000140002200 00 00 00 00 00 00 00 00 __xc_z dq 0 ; DATA XREF: __scrt_common_main_seh:loc_140001242↑o
注意其中的dynamic_initializer_for__c__
函数,这是动态初始化函数。有关于CRT初始化的过程,可以详细看看CRT 初始化 | Microsoft Learn,这里给出了一个代码示例,解决了全局变量的初始化过程。
CRT 定义两个指针:
.CRT$XCA
中的__xc_a
.CRT$XCZ
中的__xc_z
除
__xc_a
和__xc_z
之外,两个组都没有定义任何其他符号。现在,链接器在读取各个
.CRT
子节($
后面的节)时,会将这些子节组合在一个节中,并按字母顺序排列。 这表示用户定义的全局初始值设定项(Microsoft C++ 编译器将其置于.CRT$XCU
中)始终出现在.CRT$XCA
之后和.CRT$XCZ
之前。
可以用下述代码来保证某些初始化函数在最先或者最后执行
为了帮助防止代码出现问题,从 Visual Studio 2019 版本 16.11 开始,我们默认添加了两个新警告:C5247 和 C5248。 启用这些警告以检测创建自己的初始值设定项时出现的问题。
#pragma section(".CRT$XCT", read)
// 'i1' is guaranteed to be called before any compiler generated C++ dynamic initializer
__declspec(allocate(".CRT$XCT")) type i1 = f;
#pragma section(".CRT$XCV", read)
// 'i2' is guaranteed to be called after any compiler generated C++ dynamic initializer
__declspec(allocate(".CRT$XCV")) type i2 = f;
TLS
上述初始化过程结束后,进入TLS过程。函数scrt_get_dyn_tls_init_callback
得到一个指向TLS回调表的指针,以调用第一个条目。动态线程本地存储 (D-TLS) 是一种在多线程程序中,每个线程都可以拥有自己的独立变量的机制。这些变量对于每个线程来说是唯一的,每个线程都可以独立地访问和修改它们,而不会互相干扰。
TLS有关的过程为
v3 = _scrt_get_dyn_tls_init_callback();
v5 = (void (__fastcall **)(_QWORD, __int64))v3;
if ( *v3 && _scrt_is_nonwritable_in_current_image(v3) )
(*v5)(0i64, 2i64);
v6 = (_tls_callback_type *)_scrt_get_dyn_tls_dtor_callback(v4);
v7 = v6;
if ( *v6 && _scrt_is_nonwritable_in_current_image(v6) )
register_thread_local_exe_atexit_callback_0(*v7);
_scrt_get_dyn_tls_init_callback()
函数用于获取动态线程本地存储的初始化回调函数,并将其结果赋值给变量v3
。- 将
v3
转换为函数指针类型void (__fastcall **)(_QWORD, __int64)
,赋值给变量v5
。这一步是方便当前的线程调用 - 如果
*v3
(即获取的初始化回调函数)存在,并且该函数所在的内存区域在当前映像中为非可写,则调用(*v5)(0i64, 2i64)
,即执行初始化回调函数进行动态线程本地存储的初始化。 _scrt_get_dyn_tls_dtor_callback(v4)
函数用于获取动态线程本地存储的析构回调函数,并将其结果赋值给变量v6
。- 将
v6
转换为_tls_callback_type*
类型,赋值给变量v7
。 - 如果
*v6
(即获取的析构回调函数)存在,并且该函数所在的内存区域在当前映像中为非可写,则调用register_thread_local_exe_atexit_callback_0(*v7)
,即注册线程本地存储的析构回调函数。
TLS运行结束之后,就进入主线程主程序的准备阶段。
main
v8 = get_initial_narrow_environment_0();
v9 = *_p___argv_0();
v10 = _p___argc_0();
v0 = main(*v10, (const char **)v9, (const char **)v8);
这里可以明确的看到main
函数的原型。其中,第三个参数为环境变量。
主程序返回后,将检查当前程序是否为托管应用程序,并根据不同的情况进行不同的退出。
if ( !_scrt_is_managed_app() )
LABEL_20:
exit_0(v0);
if ( !v1 )
cexit_0();
最后清理crt。
_scrt_uninitialize_crt
作者发布、转载的任何文章中所涉及的技术、思路、工具仅供以安全目的的学习交流,并严格遵守《中华人民共和国网络安全法》、《中华人民共和国数据安全法》等网络安全法律法规。
任何人不得将技术用于非法用途、盈利用途。否则作者不对未许可的用途承担任何后果。
本文遵守CC BY-NC-SA 3.0协议,您可以在任何媒介以任何形式复制、发行本作品,或者修改、转换或以本作品为基础进行创作
您必须给出适当的署名,提供指向本文的链接,同时标明是否(对原文)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示作者为您或您的使用背书。
同时,本文不得用于商业目的。混合、转换、基于本作品进行创作,必须基于同一协议(CC BY-NC-SA 3.0)分发。
如有问题, 可发送邮件咨询.