前言
这是2026年御网杯的一道逆向题目。笔者拿来用于以题为例教学逆向exe文件:CRT 初始化流程详细分析。
这个exe文件用IDA解包后,发现其入口逻辑大概是:start → sub_140001180(CRT 初始化)→ sub_1400014FB(main)

笔者这里主要讲:sub_140001180(CRT 初始化)。

该函数是一个 CRT Startup 初始化包装函数,负责在进入真正用户逻辑之前完成运行库初始化、全局构造、TLS 回调、异常处理器设置、命令行参数准备,并最终调用真正的入口函数。
反编译:

1. 函数定位
原函数:
c
__int64 sub_140001180()
{
signed __int64 v1; // rsi
signed __int64 v2; // rax
signed int v3; // edi
int v4; // ebx
__int64 v5; // rdi
_QWORD *v6; // rax
__int64 v7; // rbp
__int64 v8; // r12
signed __int64 v9; // rdi
__int64 v10; // rbx
size_t v11; // rax
size_t v12; // rsi
void *v13; // rax
const void *v14; // rdx
_QWORD *v15; // rdi
__int64 v16; // rcx
__int64 result; // rax
_RBX = &unk_140007040;
v1 = *(_QWORD *)(__readgsqword(0x30u) + 8);
while ( 1 )
{
v2 = _InterlockedCompareExchange((volatile signed __int64 *)&unk_140007040, v1, 0i64);
if ( !v2 )
{
v3 = 0;
if ( unk_140007048 == 1 )
goto LABEL_20;
goto LABEL_6;
}
if ( v1 == v2 )
break;
((void (__fastcall *)(signed __int64))Sleep)(1000i64);
}
v3 = 1;
if ( unk_140007048 == 1 )
LABEL_20:
sub_1400028B0(31i64);
LABEL_6:
if ( unk_140007048 )
{
dword_140007008 = 1;
}
else
{
unk_140007048 = 1;
initterm(&unk_140004A60, &unk_140004A70);
}
if ( unk_140007048 == 1 )
{
initterm(&unk_140004A48, &unk_140004A58);
unk_140007048 = 2;
if ( v3 )
goto LABEL_10;
}
else if ( v3 )
{
goto LABEL_10;
}
_RAX = 0i64;
__asm { xchg rax, [rbx] }
LABEL_10:
if ( TlsCallback_0 )
TlsCallback_0(0i64, 2i64, 0i64);
sub_140001A80();
qword_1400070D0 = (__int64)SetUnhandledExceptionFilter(TopLevelExceptionFilter);
set_invalid_parameter_handler(Handler);
sub_140001890();
v4 = dword_140007028;
v5 = dword_140007028 + 1;
v6 = malloc(8 * v5);
v7 = (__int64)v6;
if ( v4 <= 0 )
{
v15 = v6;
}
else
{
v8 = qword_140007020;
v9 = 8 * v5 - 8;
v10 = 0i64;
do
{
v11 = strlen(*(const char **)(v8 + v10));
v12 = v11 + 1;
v13 = malloc(v11 + 1);
*(_QWORD *)(v7 + v10) = v13;
v14 = *(const void **)(v8 + v10);
v10 += 8i64;
memcpy(v13, v14, v12);
}
while ( v9 != v10 );
v15 = (_QWORD *)(v7 + v9);
}
*v15 = 0i64;
qword_140007020 = v7;
sub_140001690();
v16 = (unsigned int)dword_140007028;
*(_QWORD *)off_140003078 = qword_140007018;
result = sub_1400014FB(v16, qword_140007020);
dword_140007010 = result;
if ( !dword_14000700C )
exit(result);
if ( !dword_140007008 )
{
cexit();
result = (unsigned int)dword_140007010;
}
return result;
}
从整体结构看,该函数不是业务函数,而是程序启动阶段的运行库初始化函数。它具备以下典型 CRT Startup 特征:
- 使用全局锁防止 CRT 初始化重入;
- 使用全局状态变量记录 CRT 初始化阶段;
- 调用
initterm()执行初始化函数表; - 调用 TLS Callback;
- 设置
SetUnhandledExceptionFilter(); - 设置
set_invalid_parameter_handler(); - 准备
argc / argv / envp; - 调用真实入口函数;
- 根据运行模式调用
exit()或cexit()。
核心用户入口调用点:
c
result = sub_1400014FB(v16, qword_140007020);
其中:
c
v16 = (unsigned int)dword_140007028;
qword_140007020 = argv;
因此 sub_1400014FB() 很可能对应:
c
main(argc, argv)
或编译器生成的 main 包装函数。
2. 整体执行流程
该函数可以抽象为以下伪代码:
c
int CRTStartup()
{
acquire_startup_lock();
if (crt_state == INITIALIZING)
runtime_error_exit(31);
if (crt_state == UNINITIALIZED)
{
crt_state = INITIALIZING;
call_c_initializers();
call_cpp_initializers();
crt_state = INITIALIZED;
}
else
{
already_initialized = true;
}
release_startup_lock_if_needed();
call_tls_callback_if_exists();
setup_runtime_environment();
setup_exception_filter();
setup_invalid_parameter_handler();
duplicate_argv();
setup_environment_pointer();
result = main(argc, argv);
if (!managed_mode)
exit(result);
if (!already_initialized)
cexit();
return result;
}
3. 初始化锁的设置
对应代码:
c
_RBX = &unk_140007040;
v1 = *(_QWORD *)(__readgsqword(0x30u) + 8);
while ( 1 )
{
v2 = _InterlockedCompareExchange((volatile signed __int64 *)&unk_140007040, v1, 0i64);
if ( !v2 )
{
v3 = 0;
if ( unk_140007048 == 1 )
goto LABEL_20;
goto LABEL_6;
}
if ( v1 == v2 )
break;
((void (__fastcall *)(signed __int64))Sleep)(1000i64);
}
### 3.1 `unk_140007040` 的含义
`unk_140007040` 被作为 `_InterlockedCompareExchange()` 的目标地址使用:
```c
_InterlockedCompareExchange(&unk_140007040, v1, 0);
这说明它是一个全局同步变量,作用类似:
c
static volatile LONG_PTR startup_lock;
它用于保证 CRT 初始化代码同一时间只被一个线程执行。
3.2 _InterlockedCompareExchange 的含义
原子操作逻辑为:
c
old = *lock;
if (*lock == 0)
*lock = current_thread_or_fiber_id;
return old;
对应本函数:
c
v2 = _InterlockedCompareExchange(&unk_140007040, v1, 0);
含义是:
- 如果
unk_140007040 == 0,说明当前没有线程持有初始化锁; - 将
unk_140007040设置为v1; - 当前线程获得初始化锁;
- 如果返回值
v2 != 0,说明锁已经被其他执行流持有。
3.3 v1 的来源
c
v1 = *(_QWORD *)(__readgsqword(0x30u) + 8);
__readgsqword() 用于读取 x64 Windows 下 GS 段中的线程环境结构。这里取出的值被用作初始化锁的拥有者标识。
在 CRT Startup 代码中,这类值通常用于判断:
c
if (startup_lock == current_thread_id)
{
// 当前线程递归进入初始化流程
}
3.4 锁竞争逻辑
代码:
c
if ( !v2 )
{
v3 = 0;
...
}
if ( v1 == v2 )
break;
Sleep(1000);
含义如下:
| 条件 | 含义 |
|---|---|
v2 == 0 |
当前执行流成功获得 CRT 初始化锁 |
v2 == v1 |
当前执行流已经持有锁,属于递归进入 |
v2 != 0 && v2 != v1 |
其他线程正在初始化 CRT,当前线程等待 |
等待方式为:
c
Sleep(1000);
也就是每次等待 1000 毫秒后重新尝试获取锁。
3.5 v3 的含义
c
v3 = 0;
表示当前线程是首次成功获得锁,需要在后面释放锁。
后面还有:
c
v3 = 1;
表示当前线程是递归进入初始化流程,不应该重复释放锁。
因此可以推测:
c
v3 == 0 // 当前调用负责释放初始化锁
v3 == 1 // 当前调用不负责释放初始化锁
4. CRT 初始化状态机
核心状态变量:
c
unk_140007048
相关代码:
c
if ( unk_140007048 == 1 )
LABEL_20:
sub_1400028B0(31i64);
LABEL_6:
if ( unk_140007048 )
{
dword_140007008 = 1;
}
else
{
unk_140007048 = 1;
initterm(&unk_140004A60, &unk_140004A70);
}
if ( unk_140007048 == 1 )
{
initterm(&unk_140004A48, &unk_140004A58);
unk_140007048 = 2;
if ( v3 )
goto LABEL_10;
}
else if ( v3 )
{
goto LABEL_10;
}
unk_140007048 很可能对应 CRT 初始化状态:
| 值 | 含义 |
|---|---|
0 |
尚未初始化 |
1 |
正在初始化 |
2 |
初始化完成 |
4.1 检测非法重入
代码:
c
if ( unk_140007048 == 1 )
sub_1400028B0(31i64);
如果状态为 1,表示 CRT 正在初始化过程中。如果此时再次进入初始化流程,则属于异常情况。
sub_1400028B0(31) 很可能是运行库错误退出函数,类似:
c
_amsg_exit(31);
该错误通常表示 CRT 初始化阶段发生非法重入或初始化顺序错误。
5. C 初始化函数调用
代码:
c
if ( unk_140007048 )
{
dword_140007008 = 1;
}
else
{
unk_140007048 = 1;
initterm(&unk_140004A60, &unk_140004A70);
}
当 unk_140007048 == 0 时,说明 CRT 尚未初始化,于是:
c
unk_140007048 = 1;
initterm(&unk_140004A60, &unk_140004A70);
也就是将状态设置为"初始化中",然后调用一段初始化函数表。
5.1 initterm() 的作用
initterm() 是 MSVC CRT 中常见函数,用于遍历函数指针区间并逐个调用。
典型形式:
c
void initterm(_PVFV *begin, _PVFV *end)
{
while (begin < end)
{
if (*begin != NULL)
(**begin)();
++begin;
}
}
在本函数中:
c
initterm(&unk_140004A60, &unk_140004A70);
含义是调用地址区间:
c
[0x140004A60, 0x140004A70)
中的函数指针。
这一段通常对应 C 初始化表,例如 .CRT$XIA ~ .CRT$XIZ。
6. C++ 全局对象构造函数调用
代码:
c
if ( unk_140007048 == 1 )
{
initterm(&unk_140004A48, &unk_140004A58);
unk_140007048 = 2;
if ( v3 )
goto LABEL_10;
}
在 C 初始化函数执行完成后,如果状态仍为 1,继续调用第二组初始化函数:
c
initterm(&unk_140004A48, &unk_140004A58);
该区间通常用于 C++ 全局对象构造函数,例如:
c
class A {
public:
A() { ... }
};
A g_obj;
类似 g_obj 这样的全局对象,其构造函数会在进入 main() 之前执行。
执行完成后:
c
unk_140007048 = 2;
表示 CRT 初始化完成。
7. 释放 CRT 初始化锁
代码:
c
_RAX = 0i64;
__asm { xchg rax, [rbx] }
前面有:
c
_RBX = &unk_140007040;
因此该汇编等价于:
c
unk_140007040 = 0;
即释放 CRT 初始化锁。
不过释放锁前有条件控制:
c
if ( v3 )
goto LABEL_10;
如果 v3 == 1,说明当前调用是递归进入,不是锁的实际拥有者,因此不释放锁。
如果 v3 == 0,说明当前调用成功获取了锁,因此需要释放。
8. TLS Callback 调用
代码:
c
LABEL_10:
if ( TlsCallback_0 )
TlsCallback_0(0i64, 2i64, 0i64);
TLS Callback 的标准原型为:
c
void NTAPI TlsCallback(
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
);
这里传入参数:
c
TlsCallback_0(NULL, 2, NULL);
第二个参数 2 对应 Windows 中的:
c
DLL_THREAD_ATTACH
虽然 DLL_THREAD_ATTACH 通常用于 DLL 线程附加通知,但在某些 CRT 启动代码、EXE TLS 初始化或保护壳场景中,可能会手动触发 TLS 回调。
需要重点关注 TlsCallback_0,因为 TLS Callback 经常用于:
- 早于
main()执行初始化逻辑; - 设置反调试逻辑;
- 解密代码段或数据段;
- 检查运行环境;
- 初始化全局状态。
如果逆向目标是恶意样本或加壳程序,TLS Callback 是高优先级分析点。
9. 异常处理与运行库处理器设置
代码:
c
sub_140001A80();
qword_1400070D0 = (__int64)SetUnhandledExceptionFilter(TopLevelExceptionFilter);
set_invalid_parameter_handler(Handler);
sub_140001890();
9.1 sub_140001A80()
该函数位于异常处理器设置之前,可能用于:
- 初始化安全 Cookie;
- 初始化运行库内部结构;
- 初始化 I/O、堆或环境;
- 初始化反调试或异常相关上下文。
需要结合该函数内部代码进一步确认。
9.2 设置顶层异常过滤器
代码:
c
qword_1400070D0 = (__int64)SetUnhandledExceptionFilter(TopLevelExceptionFilter);
等价于:
c
old_filter = SetUnhandledExceptionFilter(TopLevelExceptionFilter);
作用是注册新的顶层异常处理函数。
当程序发生未处理异常时,例如:
c
int *p = NULL;
*p = 1;
如果没有其他异常处理捕获,系统会调用:
c
TopLevelExceptionFilter
返回值保存到:
c
qword_1400070D0
它可能用于后续恢复旧异常处理器,或者在当前异常处理器无法处理时继续调用旧处理器。
逆向时应重点分析 TopLevelExceptionFilter,因为它可能包含:
- 崩溃日志记录;
- 自定义异常恢复;
- 反调试逻辑;
- SEH/VEH 相关控制流混淆;
- 解密或跳转到隐藏代码。
9.3 设置非法参数处理器
代码:
c
set_invalid_parameter_handler(Handler);
MSVC CRT 的安全函数在遇到非法参数时,会触发 invalid parameter handler。例如:
c
strcpy_s(NULL, 10, "abc");
printf(NULL);
fopen_s(NULL, "a.txt", "r");
默认情况下,CRT 可能会弹窗、终止进程或调用默认错误处理逻辑。
此处设置自定义 Handler,说明程序希望自己接管这类异常情况。
需要重点分析:
c
Handler
它可能用于:
- 忽略 CRT 参数错误;
- 统一退出;
- 记录日志;
- 混淆异常行为;
- 配合反调试绕过默认错误路径。
10. 命令行参数 argv 的复制
代码:
c
v4 = dword_140007028;
v5 = dword_140007028 + 1;
v6 = malloc(8 * v5);
v7 = (__int64)v6;
这里 dword_140007028 大概率是:
c
argc
v5 = argc + 1,然后:
c
malloc(8 * (argc + 1));
x64 下指针大小是 8 字节,因此这是在分配:
c
char **argv_copy = malloc(sizeof(char *) * (argc + 1));
10.1 无参数或 argc <= 0 的情况
代码:
c
if ( v4 <= 0 )
{
v15 = v6;
}
如果 argc <= 0,则没有参数需要复制,直接让 v15 指向新分配的数组开头。
后面会执行:
c
*v15 = 0i64;
也就是:
c
argv_copy[0] = NULL;
10.2 有参数时逐个复制字符串
代码:
c
else
{
v8 = qword_140007020;
v9 = 8 * v5 - 8;
v10 = 0i64;
do
{
v11 = strlen(*(const char **)(v8 + v10));
v12 = v11 + 1;
v13 = malloc(v11 + 1);
*(_QWORD *)(v7 + v10) = v13;
v14 = *(const void **)(v8 + v10);
v10 += 8i64;
memcpy(v13, v14, v12);
}
while ( v9 != v10 );
v15 = (_QWORD *)(v7 + v9);
}
变量含义:
| 变量 | 含义 |
|---|---|
v8 |
原始 argv 指针数组 |
v7 |
新分配的 argv_copy 指针数组 |
v10 |
当前参数偏移,单位字节,每次加 8 |
v11 |
当前参数字符串长度 |
v12 |
当前参数字符串长度 + 1,包括 \0 |
v13 |
为当前参数新分配的字符串缓冲区 |
v14 |
原始参数字符串地址 |
v9 |
最后一个参数指针的偏移 |
循环逻辑等价于:
c
for (int i = 0; i < argc; i++)
{
size_t len = strlen(argv[i]) + 1;
argv_copy[i] = malloc(len);
memcpy(argv_copy[i], argv[i], len);
}
10.3 添加 argv 结束 NULL
代码:
c
*v15 = 0i64;
qword_140007020 = v7;
等价于:
c
argv_copy[argc] = NULL;
qword_140007020 = argv_copy;
C 标准中 argv[argc] 必须为 NULL,因此这里是在保证复制后的参数数组符合标准格式。
11. 环境变量和运行环境设置
代码:
c
sub_140001690();
v16 = (unsigned int)dword_140007028;
*(_QWORD *)off_140003078 = qword_140007018;
11.1 sub_140001690()
该函数位于参数复制之后、调用 main 之前,可能用于进一步初始化运行时环境,例如:
- 初始化环境变量;
- 初始化当前目录信息;
- 初始化 locale;
- 初始化标准 I/O;
- 整理
argv/envp指针; - 设置全局运行库变量。
11.2 设置环境变量指针
代码:
c
*(_QWORD *)off_140003078 = qword_140007018;
这里 qword_140007018 很可能是环境变量指针,例如:
c
envp
off_140003078 可能是某个 CRT 全局环境指针的地址,例如:
c
_environ
因此这句可以理解为:
c
_environ = qword_140007018;
或:
c
*__p__environ() = envp;
12. 调用真正用户入口函数
代码:
c
result = sub_1400014FB(v16, qword_140007020);
dword_140007010 = result;
其中:
c
v16 = (unsigned int)dword_140007028;
qword_140007020 = argv_copy;
因此该调用等价于:
c
result = sub_1400014FB(argc, argv_copy);
高度疑似:
c
result = main(argc, argv);
或:
c
result = invoke_main(argc, argv);
返回值保存到:
c
dword_140007010
也就是程序退出码。
逆向分析时,sub_1400014FB 是本函数之后最重要的分析目标,因为它很可能开始进入真正的用户逻辑。
13. 程序退出路径
代码:
c
if ( !dword_14000700C )
exit(result);
if ( !dword_140007008 )
{
cexit();
result = (unsigned int)dword_140007010;
}
return result;
13.1 dword_14000700C
该变量用于判断是否直接调用 exit()。
如果:
c
dword_14000700C == 0
则:
c
exit(result);
exit() 会执行完整的 CRT 退出流程,包括:
- 调用
atexit()注册函数; - 调用全局对象析构函数;
- 刷新并关闭标准 I/O;
- 终止进程。
如果:
c
dword_14000700C != 0
则不直接 exit(),而是继续执行后面的清理逻辑。
该变量可能表示:
- 是否为托管应用;
- 是否由外部宿主控制退出;
- 是否只返回退出码而不终止进程。
13.2 dword_140007008
前面代码中:
c
if ( unk_140007048 )
{
dword_140007008 = 1;
}
说明如果进入函数时 CRT 已经初始化过,则设置:
c
dword_140007008 = 1;
退出时:
c
if ( !dword_140007008 )
{
cexit();
result = (unsigned int)dword_140007010;
}
含义是:
- 如果本次调用负责 CRT 初始化,则本次调用也负责 CRT 清理;
- 如果 CRT 在进入本函数之前已经初始化,则不重复清理。
13.3 cexit() 的作用
cexit() 是 CRT 清理函数,但通常不会直接终止当前进程。它主要用于执行 CRT 清理逻辑,包括:
- 调用终止函数表;
- 调用 C++ 全局析构;
- 刷新运行库状态。
和 exit() 的区别是:
| 函数 | 是否终止进程 | 是否执行 CRT 清理 |
|---|---|---|
exit() |
是 | 是 |
cexit() |
否 | 是 |
14. 全局变量含义推测
| 变量 | 推测含义 | 依据 |
|---|---|---|
unk_140007040 |
CRT 初始化锁 | 被 _InterlockedCompareExchange 原子修改 |
unk_140007048 |
CRT 初始化状态 | 判断 0/1/2 初始化阶段 |
dword_140007008 |
CRT 是否已初始化标志 | 影响是否调用 cexit() |
dword_14000700C |
是否直接 exit() 的模式标志 |
控制 exit(result) 路径 |
dword_140007010 |
主函数返回值 / exit code | 保存 sub_1400014FB 返回值 |
dword_140007028 |
argc |
被传给主函数作为第一个参数 |
qword_140007020 |
argv |
被复制并传给主函数 |
qword_140007018 |
envp / 环境指针 |
被写入 off_140003078 指向的位置 |
qword_1400070D0 |
旧的顶层异常处理器 | 保存 SetUnhandledExceptionFilter 返回值 |
TlsCallback_0 |
TLS 回调函数 | 按 TLS Callback 形式调用 |
TopLevelExceptionFilter |
顶层异常处理函数 | 传给 SetUnhandledExceptionFilter |
Handler |
CRT 非法参数处理器 | 传给 set_invalid_parameter_handler |
sub_1400014FB |
真正用户入口 | 以 argc, argv 形式调用 |
15. 结合代码的完整伪代码还原
根据当前反编译代码,可以还原为:
c
int sub_140001180(void)
{
int nested_or_reentered;
int argc;
char **new_argv;
int result;
startup_lock_ptr = &unk_140007040;
current_owner = get_current_thread_or_fiber_id();
while (true)
{
old_owner = InterlockedCompareExchange(
&unk_140007040,
current_owner,
0
);
if (old_owner == 0)
{
nested_or_reentered = 0;
if (crt_state == INITIALIZING)
runtime_error_exit(31);
break;
}
if (old_owner == current_owner)
{
nested_or_reentered = 1;
break;
}
Sleep(1000);
}
if (crt_state != UNINITIALIZED)
{
already_initialized = 1;
}
else
{
crt_state = INITIALIZING;
initterm(c_init_begin, c_init_end);
}
if (crt_state == INITIALIZING)
{
initterm(cpp_init_begin, cpp_init_end);
crt_state = INITIALIZED;
if (!nested_or_reentered)
release_startup_lock();
}
else
{
if (!nested_or_reentered)
release_startup_lock();
}
if (TlsCallback_0 != NULL)
TlsCallback_0(NULL, DLL_THREAD_ATTACH, NULL);
sub_140001A80();
old_exception_filter = SetUnhandledExceptionFilter(
TopLevelExceptionFilter
);
set_invalid_parameter_handler(Handler);
sub_140001890();
argc = dword_140007028;
new_argv = malloc(sizeof(char *) * (argc + 1));
for (int i = 0; i < argc; i++)
{
size_t len = strlen(qword_140007020[i]) + 1;
new_argv[i] = malloc(len);
memcpy(new_argv[i], qword_140007020[i], len);
}
new_argv[argc] = NULL;
qword_140007020 = new_argv;
sub_140001690();
*off_140003078 = qword_140007018;
result = sub_1400014FB(argc, qword_140007020);
dword_140007010 = result;
if (!dword_14000700C)
exit(result);
if (!dword_140007008)
{
cexit();
result = dword_140007010;
}
return result;
}
16. 总结
sub_140001180() 是一个典型的 CRT 初始化启动函数。它的主要作用不是实现程序业务,而是为真正的用户入口函数准备运行环境。
它完成了:
- CRT 初始化锁控制;
- CRT 初始化状态管理;
- C 初始化函数调用;
- C++ 全局构造函数调用;
- TLS Callback 调用;
- 顶层异常过滤器设置;
- CRT 非法参数处理器设置;
argc / argv / envp准备;- 调用真实入口函数;
- 根据运行模式执行退出清理。
c
sub_140001180()
应被命名为类似:
c
CRTStartup
或:
c
runtime_startup_wrapper
而真正的程序逻辑入口应重点关注:
c
sub_1400014FB(argc, argv)
该函数才是后续题目分析的主要入口。比赛时,这个函数大致过一遍,找到main()函数就可以跳过去了。这里只是用于分析才长篇大论的展开去介绍。