模块 79 - 移除 CRT 库与恶意软件编译技巧
恶意软件开发课程 - 移除 CRT 库与恶意软件编译技巧
模块 79 - 移除 CRT 库与恶意软件编译技巧#
移除 CRT 库与恶意软件编译技巧#
简介#
在恶意软件开发中,仅仅编写功能代码是不够的,如何编译代码同样至关重要。不同的编译选项会直接影响二进制文件的体积、熵值以及导入表(IAT)的特征。本模块将深入探讨如何通过修改 Visual Studio 的默认编译器设置,彻底移除 C 运行时库 (CRT),从而产出高度精简且更具规避性的二进制文件。
💡 初学者提示:什么是 CRT?为什么要移除它?
类比理解: 想象你正准备来一场极限生存挑战(恶意软件运行):
- 默认情况:你背着一个巨大的旅行箱,里面装满了全套厨具、换地毯的工具、甚至还有备用发电机(这就是 CRT 库,它包含了成百上千个你可能永远不用的通用功能)。虽然方便,但你走得很慢,且目标巨大(体积大、IAT 特征多),很容易被发现。
- 精简方案(移除 CRT):你决定只带一把多功能军刀、一根火柴和一点干粮。虽然所有事情(如内存拷贝、字符串计算)你都要亲手劳作(手动实现底层函数),但你变得极其轻盈,且能完美消失在树林中。
1. Release 与 Debug 选项的区别#
在 Visual Studio 中,“Release”和“Debug”配置决定了程序的编译和执行方式。
- 性能: Release 模式开启了大量代码优化,运行速度比 Debug 快。
- 调试支持: Debug 模式禁用优化,且会生成调试符号文件 (
.pdb)。这允许调试器显示变量名、函数名和源代码行号。 - 部署与兼容性: Release 版本通常具有更好的兼容性。而 Debug 版本通常依赖于特定版本的调试辅助 DLL,这些 DLL 往往只安装在开发机器上。
- 异常处理: Debug 模式下发生崩溃时,VS 能提供详细的错误弹窗;而 Release 模式下可能会直接闪退或导致不可预知的行为。
2. 默认编译器设置存在的问题#
尽管 Release 选项优于 Debug,但其默认设置仍存在以下弊端:
- 兼容性风险: 如果目标机器缺少对应的 Visual Studio 运行时环境,可能会出现“找不到 DLL”的错误。
- 臃肿的 IAT: 即使是一个简单的“Hello World”,默认编译器也会链接大量未使用的 CRT 函数(如各种堆管理、本地化、错误处理函数),这为防御端提供了大量的静态特征。
- 文件体积: 默认优化的生成文件通常比实际代码大得多。
- 残留字符串: 默认设置会在二进制文件中留下大量的调试路径、库名称等明文辅助字符串。
3. C 运行时库 (CRT) 详解#
CRT (Microsoft C Run-Time Library) 是提供标准 C/C++ 基础功能的函数集,包括:
- 内存管理:
malloc,memset,free - 字符串操作:
strcpy,strlen - I/O 函数:
printf,scanf
这些函数通常位于 vcruntimeXXX.dll 或 api-ms-win-crt-*.dll 中。
解决兼容性:/MT 还是 /MD?#
默认情况下,VS 使用 多线程 DLL (/MD)。此时 CRT 是动态链接的。为了提高兼容性,可以改为 多线程 (/MT)。
- /MT 的效果: 编译器会将用到的 CRT 函数(如
printf)的代码直接嵌入到你的.exe中,不再依赖外部 CRT DLL。 - 代价: 这会使生成的二进制文件体积显著增大(因为要包含库代码),且 IAT 中会出现更多底层的 WinAPI。
4. 终极方案:彻底脱离 CRT 依赖#
为了生成真正的“轻量级”恶意软件,理想方案是既不动态链接也不静态链接 CRT,而是完全实现我们自己的底层函数。这需要对 Visual Studio 进行一系列手术:
A. 核心编译器调整#
- 禁用 C++ 异常: 设置“启用 C++ 异常”为“否”。因为移除 CRT 后,异常传播机制将失效。
- 禁用全程序优化: 设置“全程序优化”为“否”,以获得对堆栈和代码生成的完全控制。
- 禁用调试信息与清单: 设置“生成调试信息”为“否”,并关闭清单文件的生成。
B. 链接器调整 (关键)#
- 忽略所有默认库: 设置“忽略所有默认库”为 是 (/NODEFAULTLIB)。这会切断与 CRT 及一切系统默认库的联系。
- 设置入口点 (Entry Point): 由于我们不再使用 CRT 引导,默认的
mainCRTStartup将不复存在。你需要手动将入口点指向你的主函数(例如main)。 - 禁用安全检查: 设置“安全检查”为“禁用安全检查 (/Gs-)”。这会移除对
__security_check_cookie的依赖(即移除堆栈保护金丝雀,请在代码中自行注意缓冲区溢出)。 - 禁用 SDL 检查: 为了避免因关闭安全检查引发的警告,需同时关闭 SDL 检查。
5. 手动实现 CRT 必要函数#
当你切断了与 CRT 的联系后,像 printf 或 memset 这样的函数将无法直接使用。你需要自己重写它们。
替代 printf:PRINTA 宏#
我们利用 Windows 的原生能力 wsprintfA 进行格式化,并使用 WriteConsoleA 输出到控制台。
#define PRINTA( STR, ... ) \
if (1) { \
LPSTR buf = (LPSTR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 ); \
if ( buf != NULL ) { \
int len = wsprintfA( buf, STR, __VA_ARGS__ ); \
WriteConsoleA( GetStdHandle( STD_OUTPUT_HANDLE ), buf, len, NULL, NULL ); \
HeapFree( GetProcessHeap(), 0, buf ); \
} \
}
// 使用示例
int main() {
PRINTA("Hello World ! 无需 CRT 也可运行。\n");
return 0;
}c替代 memset (内联函数)#
某些编译器宏(如 ZeroMemory)内部仍会寻找 memset。你可以通过 intrinsic 关键字强制让编译器使用你自定义的版本:
#include <Windows.h>
extern void* __cdecl memset(void*, int, size_t);
#pragma intrinsic(memset)
#pragma function(memset)
void* __cdecl memset(void* Destination, int Value, size_t Size) {
unsigned char* p = (unsigned char*)Destination;
while (Size > 0) {
*p = (unsigned char)Value;
p++;
Size--;
}
return Destination;
}c6. 其他配置建议#
- 隐藏控制台窗口: 为了避免弹出的黑框引起怀疑,建议将项目设置中的“子系统 (SubSystem)”改为 Windows (/SUBSYSTEM:WINDOWS)。这样程序启动后将不会附着或创建控制台。
7. 实验结论与效果#
完成上述调整后,你会发现:
- 极简的体积: 一个带 Payload 的加载器可能从 112kb 缩减到仅仅 3kb。
- 洁净的 IAT: 导入表中只会留下你自己显式调用的 API,没有任何冗余的库函数,极大地增加了沙箱静态得分的难度。
- 更好的免杀性: 实测表明,移除 CRT 后的样本在 VirusTotal 上的检出率显著降低。
🎯 总结#
在本模块中,我们学习了:
- 编译器的精细控制: 摆脱 Visual Studio 的默认负担,打造干净的二进制外壳。
- 底层自立: 学会了不依赖标准库,利用原生 WinAPI 重新实现核心运行时功能。
- 静态足迹净化: 深刻理解了 IAT 臃肿对反病毒审计的影响,并掌握了手动精简技术。
📚 下一步学习
下一个模块将探讨 Module 80 - IAT Camouflage。我们将学习如何通过注入“假”的导入函数来进一步迷惑分析人员。