强对抗环境下的安全编程(二)
——稳定的API隐蔽调用
在上一次的分享中,简单描述了虚拟机加密中一个薄弱的环节——C++对象数据结构,在虚拟机加密中,还有另外一个很重要的脆弱环节就是系统API调用。
系统API直接调用的七宗罪
虚拟机加密最初的设计是为了保护程序中的计算逻辑,一个注册码生成和验证算法,如果用虚拟机加密,会得到很好的效果。但虚拟机加密并非很适合“防止程序原理被分析”,无论程序逻辑如何加密,最终还是要调用系统API, 通过系统API调用的次序和参数进行监视,也是分析程序方案原理和细节的一个关键技术方法。
直接调用系统API, 概括来讲的七宗罪即:
1 安全方案每个模块的导入表泄露这个模块的功能类型;
2 对API的调用次序的记录为分析模块功能带来最快速的体验(扫描?通讯?…);
3 对API的调用参数的记录为分析模块功能细节带来重要参考;
4 API调用过程中参数压栈、过程调用、返回值处理等过程泄露无handle分发过程类型的虚拟机部分handle实现细节;
5 API调用次序记录的技术方法使虚拟机加密的函数重排特性实际效果大幅减弱;
6 通过对比两个先后版本的安全方案模块的API调用细节,可以快速分析两个版本中的修改细节;
7 直接调用系统API直接受到系统模块内存代码Hook的影响和攻击;
由于这个分享的读者是设定于具有一定软件安全分析逆向和对抗经验的读者,对于上面每种威胁的细节不再赘述。
我们需要一种稳定且隐蔽的API调用方式
我们需要一种隐蔽的API调用方式,对于上面提到的“七宗罪”的第一条,首先被想到的是利用运行期GetProcAddress的方法获取API函数的地址,但是考虑到其他的条目,这并不是一个比较彻底解决问题的方式,综合考虑,我们需要的API调用方式必须具备的特性有:
1 不直接通过导入表导入系统模块和系统API;
2 充分发挥虚拟机加密各种技术特性的优势;
3 在稳定性上和直接调用系统API没有差别;
4 在性能上尽量接近直接调用系统API;
5 调用的过程尽量隐蔽;
有了这些需求,让我们来一起清除路障实现它,既然GetProcAddress有诸多不完美,那么自实现LoadLibrary呢,类似下面的代码很多同学都曾经写过,解析DLL模块的PE结构然后按照页对其将每一个节映射到内存,修复重定位的地址最后调用DllEntry,一个最常见的实现自加载DLL的过程,这样的过程通常用于自加载第三方模块进行内存对比。
但是,这样的代码如果用于自加载系统模块如kernel32, user32或ntdll, 会发生什么事情,看user32中下面的代码:
图中黄色部分是位于运行期数据段的一个全局变量,每个系统模块中都有一些全局变量是需要系统进程实例内共享的,如果用通常的自加载第三方模块的方式自加载系统模块,将会带来很大程度的不稳定因素,甚至完全无法使用。
如何解决这样的问题,逐个识别重定位是否指向运行期数据?其实有一种非常稳定简便的方法,在MSDN Library中对Windows的系统函数CreateFileMapping的flProtect参数有以下描述:
HANDLE CreateFileMapping(
HANDLE hFile,
LPSECURITY_ATTRIBUTES lpAttributes,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
LPCTSTR lpName
);
flProtect
SEC_IMAGE
Sets the file that is specified for section file mapping to be an executable image file.
用SEC_IMAGE参数创建文件映射将设定文件为可执行文件按照区段进行映射,这样操作系统DLL将直接映射出来一个已经按照页对齐进行映射并且没有对重定位数据进行额外操作的文件映射。这个文件映射所对应内存中的代码引用的系统模块全局数据将于已经被加载到默认加载地址的全局数据共享。
解决了重定位的问题,下面就是获取函数地址了,GetProcAddress的过程看似简单,但仍然见过很多并不标准的自实现过程(比如DoubleEx木马中的自实现GetProcAddress),这样的过程简单应用还可以应对,但对于在千万级客户端环境上实现大量导出函数和导出数据的获取,对稳定性有非常高的要求,随本文一同发出的代码中CStealthAPI::GetProcAddrFromMappedModule函数,是zetta结合Microsoft PE/COFF文件格式定义结合方案需求重新实现的相对标准的过程(这里所说方案需求主要指,模块名和导出名不使用明文,纯运算过程不调用其他API等),已经在DNF全服客户端上长期验证通过。
接下来,为了实现前面提到的5个关键特性,尽量做到字符串的加密和散开保存,尽量缩减明文字符串在内存中呈现的时间,也是提升隐蔽API调用方案安全性的关键。例如在刚才提到的GetProcAddrFromMappedModule函数中,就使用了不再任何时刻呈现明文字符串的方式。
集中一个时刻获取API地址,加密保存到数组,是一个提升虚拟机加密效果的关键技巧,虚拟机加密的最强悍点在对数据的处理过程。
下面就是调用,在CStealthAPI这个类中,使用的调用方式是通过宏和多个函数实现各种参数的兼容,并且在调用过程中用数组下标进行引用,这样就使得整个的调用过程前都可以是纯运算过程,发挥虚拟机加密的效果。
下面看一个在实际方案中的调用实例
上面的实例就是账号安全方案PolicyProbe中使用的CStealthAPI的代码,这个类的主要目的是实现稳定和高性能的隐蔽API调用,类似特性的API隐蔽调用方案(实际方案中包括更多对抗特性)已经在关键客户端对抗方案SafeLogin/TEdit中经历了一年以上对抗的磨砺,甚至曾经和木马集中于这个环节的攻防。
随这篇分享附带的CStealth类是实际方案中使用的一个简化版,它具备的特点有:
1 隐蔽加载系统模块,没有文件句柄和文件映射句柄。
2 运行期隐蔽构建模块和导出函数加密字符串,任何时刻均不出现明文导出函数名。
3 加密保存模块加载地址和导出函数地址。
4 任何时刻不出现任何明文字符串的且最标准的自实现GetProcAddress过程。
5 运行期通过指令直接构建随机密钥,每次不同。
6 和显式加载的系统模块共用全局数据,稳定性尽可能接近原始调用。
7 调用函数时刻使用数组下标直接引用到加密函数地址,性能尽可能接近原始调用。
8 兼容各种参数个数和返回值类型。
9 稳定性和性能均兼容VMP和TVMP虚拟机保护。
其他的技术细节就不再文档中描述,大家可以参考代码。到这里,我们已经解决了稳定隐蔽和性能的问题,但是,对于七宗罪中的第七条,如何防御对搜索代码进行Hook监视的方式,将在后续关于动态防篡改中描述,这样的特性已经应用于账号安全方案SafeLogin/TEdit中成功对抗了DoubleEx木马的“监视检测、谁检测就篡改谁”的思路。随本文附带的源代码CStealthAPI用于要求稳定隐蔽高性能的环境,实际方案中的加强版用于同时可能遭遇篡改的环境。