本文为 Maldev Academy 中的 Module51~Module57 小节的笔记,通过代码实现 IAT 隐藏和混淆。(完整学习内容请自行前往跳转链接查看)。
Module51~Module57 主要讲解入如下内容:
- String Hashing
- IAT Hiding & Obfuscation - Introduction
- IAT Hiding & Obfuscation - Custom GetProcAddress
- IAT Hiding & Obfuscation - Custom GetModuleHandle
- IAT Hiding & Obfuscation - API Hashing
- IAT Hiding & Obfuscation - Custom Pseudo Handles
- IAT Hiding & Obfuscation - Compile Time API Hashing
这部分在学习过程中暂未遇到太大的阻力,所以就不展开大篇文章详细介绍了。这里就只概括下这部分是如何实现的吧。详细完整内容请前往 Maldev Academy 进行学习。
1、直接调用目标函数
问题:当我们在编写程序时,如果直接调用 VirtualAlloc() 等系统函数,就会在程序的 Import Address Table (IAT) 导入地址表 中发现相关函数被使用的特征。为了消除这些特征,我们需要使用下面的一些技术手段来进行规避。
2、运行时加载目标函数
解决问题:我们可用使用 GetModuleHanle()/LoadLibrary() + GetProcAddress() 的方式,在程序运行时动态加载要调用的函数,比如 VirtualAlloc()。
实现效果:IAT 导入地址表 中不再直接出现 VirtualAlloc。
新问题:当我们使用 strings 等工具检索可执行程序中的字符串时,会发现GetModuleHanle、GetProcAddress、VirtualAlloc等字符串出现。
3、自定义加载函数
解决问题:我们可以通过手动编写代码实现 GetModuleHanle()、GetProcAddress() 相同的功能,然后就可以通过自定义函数 GetModuleHanle() + 自定义函数 GetProcAddress() 实现调用 VirtualAlloc()。
实现效果:当我们使用 strings 等工具检索可执行程序中的字符串时,不再出现GetModuleHanle、GetProcAddress字符串。(理想情况下,但不排除程序中别的地方自动调用的可能)
新问题:VirtualAlloc字符串依然存在。
4、自定义加载函数 + Runtime API Hash
解决问题:我们可以对 API 函数名进行字符串 Hash,然后对比 API Hash 值,从而获取到目标函数 VirtualAlloc() 的地址。
首先,在程序代码中使用变量保存计算好的 VirtualAlloc() 函数的 Hash 值,接着在程序运行时,遍历加载模块的 导出表(Export Table) 中的每个函数名,并对 API 函数名字符串计算 Hash 值,将计算的 Hash 值依次与目标函数 VirtualAlloc() 的 Hash 值进行对比,如果匹配成功,便可获得 VirtualAlloc() 的地址,相应的进行调用即可。
实现效果:当我们使用 strings 等工具检索可执行程序中的字符串时,不再出现GetModuleHanle、GetProcAddress、VirtualAlloc字符串。(理想情况下,但不排除程序中别的地方自动调用的可能)
新问题:这里提到的 API Hash 方式属于 Runtime API Hash。因为我们在程序代码中提前写死了目标函数VirtualAlloc() 的 Hash 值,那么该值就可能被作为一个 IOC 指标来使用。
5、自定义加载函数 + Runtime API Hash + Compile Time API Hash
解决问题:Compile-Time API Hashing (编译时Hash) 在编译阶段就可以把目标函数名转换成哈希,避免了对目标函数硬编码的问题。
我们使用 C++ 语法中的 constexpr 关键字,让编译器在编译时而不是运行时执行对目标函数 Hash 值的计算,在加入基于当前时间的随机函数之后,可以确保每次编译项目生成程序后,目标函数的 Hash 值都是不同的。通过 Compile-Time API Hashing,我们可以实现不硬编码目标函数的 Hash 值,然后和之前一样,再使用 Runtime API Hash 方法,在程序运行时,遍历加载模块的 导出表(Export Table) 中的每个函数名,并对 API 函数名字符串计算 Hash 值,将计算的 Hash 值依次与目标函数 VirtualAlloc() 的 Hash 值进行对比,如果匹配成功,便可获得 VirtualAlloc() 的地址,相应的进行调用即可。
注意:因为 导出表(Export Table) 解析必须在运行时进行,所以,我们无法提前知道这个进程加载了哪个版本的 kernel32.dll,函数在 导出表 中的顺序,以及函数地址在内存中的位置。因此,必须运行时“遍历” 导出表 并对名称哈希进行比较,这是无法避免的。
当然,应该还有更多的 IAT 隐藏和混淆方法,目前课程主模块中仅介绍了这部分内容,这里就只做这些内容概括。