[参考]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,其代码如下

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函数。此函数主要完成下述工作:

  1. 初始化运行时环境:初始化 C/C++ 运行时环境,包括全局变量、静态对象的构造函数调用等。
  2. 调用程序的入口函数:调用程序的真正入口函数,通常是 main 函数或 wmain 函数,从这里开始执行程序的主要逻辑。
  3. 异常处理:设置结构化异常处理(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

接下来,代码获取启动锁。启动锁是一种用于多线程同步的机制,用于确保在多线程环境下只有一个线程可以执行关键的启动代码。在应用程序启动过程中,存在一些需要在多个线程之间共享的全局资源或状态,这些资源需要在多线程访问时进行同步,以避免竞态条件和不确定的行为。获取此锁的方法如下

  1. 检查当前组件(dll)是否正在被使用

  2. 如果是,则根据NtCurrentTeb()->Reserved1[1]的值获取_scrt_native_startup_lock

  3. 轮询等待这个资源(这里使用了死循环的方法,一直等到这个资源被成功的由原子操作_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);

此处代码执行初始化和清理流程。

  1. initterm_e_0(_xi_a, _xi_z):这个函数接受两个参数 _xi_a_xi_z,它们表示一个对象或变量的初始化函数表的起始地址和结束地址。该函数的目的是按顺序调用这些初始化函数,对相关的对象或变量进行初始化。如果调用成功并且所有初始化函数都返回零(表示成功),则条件判断为假。
  2. 如果条件判断为假,即初始化函数执行成功且返回值为零,那么执行下一步。否则返回255,表示初始化失败。
  3. 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 开始,我们默认添加了两个新警告:C5247C5248。 启用这些警告以检测创建自己的初始值设定项时出现的问题。

#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);
  1. _scrt_get_dyn_tls_init_callback() 函数用于获取动态线程本地存储的初始化回调函数,并将其结果赋值给变量 v3
  2. v3 转换为函数指针类型 void (__fastcall **)(_QWORD, __int64),赋值给变量 v5。这一步是方便当前的线程调用
  3. 如果 *v3(即获取的初始化回调函数)存在,并且该函数所在的内存区域在当前映像中为非可写,则调用 (*v5)(0i64, 2i64),即执行初始化回调函数进行动态线程本地存储的初始化。
  4. _scrt_get_dyn_tls_dtor_callback(v4) 函数用于获取动态线程本地存储的析构回调函数,并将其结果赋值给变量 v6
  5. v6 转换为 _tls_callback_type* 类型,赋值给变量 v7
  6. 如果 *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
posted @ 2023-07-04 22:39  二氢茉莉酮酸甲酯  阅读(401)  评论(1编辑  收藏  举报