商用硬件Token设备软件实现中的Envelope漏洞分析

lucywang 漏洞 2019年4月29日发布
Favorite收藏

导语:我最近正在研究一个使用加密Token(USB加密狗)的不知名Windows软件,其中可执行文件本身经过某种形式的预处理,并被封装和加密,以阻止在没有加密狗的情况下使用。

前言

我最近正在研究一个使用加密Token(USB加密狗)的不知名Windows软件,其中可执行文件本身经过某种形式的预处理,并被封装和加密,以阻止在没有加密狗的情况下使用。目前,该软件不再适用于Windows 10(可能是由于Envelope保护逻辑中的某些漏洞)。

实际上,该软件使用这些Envelope是为了防止加密狗代码被删除,如下所示:

if(!dongle_present()){exit();}

即使被删除,也很容易被修复。

另外,使用这些Envelope还有作用。

1.隐藏硬编码的秘密;

2.隐藏未发布的功能;

3.阻止或使软件复制复杂化;

过去今年,我已经研究了几个辅助封装的硬件的二进制文件,其中并没有很多变体。考虑到封装代码已经成了一个不怎么被关注的技术,没有很多变体也是可以理解的。另外,添加至少需要通过HID进行通信的硬件组件,会使任何封装器模型复杂化,并显著增加攻击面。

然而,本文所讲的样本则非常有趣,因为它似乎不是普通的AKS/HASP SRMEnvelope或VMPEnvelope,而是来自深思洛克(SenseLock) 的更便宜的产品。

硬件

1.png

Senselock LC也被称为Clave 2,这个预算级别的硬件加密Token不像Elite EL型号那样华而不实。它没有强大的API,无法执行用户创建的自定义固件,它绝对是“入门级”的Token设备。

配置如下:

1.内置2KB闪存;

2.通过API和“更新包”远程更新此内存区域;

3. AES128位加密,带有不可配置的硬件密钥;

4.每次调用最多16个字节的输入数据;

5.HID(Driverless)支持Windows/Mac/Linux 32/64位;

另一个值得吹捧的特性(仅适用于Windows 32位)是一个Envelope实用程序,它可以用加密狗加密和封装现有的、预编译的可执行文件,并提供一些额外的安全选项,比如自定义漏洞消息或将特定的封装可执行文件绑定到特定的加密狗。

供应商提供的软件

幸运的是,这个加密狗的API可以在Senselock站点上免费获得,包括用于封装的实用程序。应该注意的是,虽然其他产品(如HASP)会混淆客户端API库和封装实用程序,但这并非总是如此。

你可以很容易地从API zip中轻松抓取一个静态库文件并解压缩它:

2.png

可以看出它们没有被删除,以后会有用的。

003.png

Envelope实用程序本身是一个相当基本的GUI,它会扩展到使用LC库静态编译的exe。

API本身也是相当简单的,使用Clave 2,你可以进行如下操作:

1.打开具有给定索引的句柄;

2.使用密码登录,密码有三个 ,分别是admin (read – write)、general (read/encrypt/decrypt)和auth;

3.做你想做的;

应该注意的是,这种API流程的工作方式意味着在大多数情况下,通用密码至少可能是硬编码的。因此,如果有人知道所需的密码,那么使用这个公开可用的API并使用适用于其他软件的加密狗创建软件就很简单了。

注: 大多数加密TokenAPI都会进行一些初始化,以便将静态和动态库绑定到特定的加密狗或加密狗系列,这是为了防止人们通过公共可用库与特定加密狗交互。

漏洞寻找

1.一开始我们对目标应用程序了解得不够多,所以更好的方法是封装一个超级基础应用程序。这样,我们就将同时拥有代码和原始二进制文件,并将其用于我们的测试。

2.我们可能会使用API库来标记供应商代码,并尝试更快地确定整个过程的工作方式。

3.我们的目的是确定封装器是如何工作的,或者使用调试器生成一个运行转储。

4.如果我们真的想要额外的点,这是可能的,我们可以静态地重构原始的exe,而不需要实际运行程序。

4.1该封装器是否采用了额外的进程、nanomites……;

4.2如果封装器使用被盗字节,那么静态构建将更加困难;

测试应用程序

一个相当基础的C语言程序将非常适合测试:

#include <Windows.h>#include <stdio.h>int main() {
	HMODULE h = LoadLibraryA("user32.dll");	printf("This is a test program: %p\n", h);

	HANDLE hfile = CreateFileA("C:\\test.txt",
		GENERIC_READ,
		FILE_SHARE_READ,		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,		NULL);	CloseHandle(hfile);	return 0;
}

编译后的结果大约是9KB (JFC Visual Studio),用Envelope处理它会得到一个后缀为“_shell”的exe(大小> 100KB)

5.png

将它加载到IDA中分解IAT,这样我们的可执行文件中有一个新部分:

6.png

毫无疑问,这也是我们新的测试点。

首先要做的是打开我们的静态库,寻找常量和各种类似的函数,并检查xreferences,以尝试标记其中的一些内容。

结果如下:

7.png

现在,在原始入口点之后输入一些二级函数,这样我们将开始看到一些熟悉的内容,例如:

8.png

以及

9.png

现在,我们就获得了一些VM检测代码。

实际上,通过封装器入口点中的一些函数,你会看到很多来自以下链接的复制粘贴代码:https://www.cnblogs.com/zhangdongsheng/archive/2012/07/02/2573416.html

通常情况下,封装者会混淆这种逻辑,以使识别或修改变得更加困难。在这种情况下,如果你修改此部分中的任何字节,应用程序都将关闭(无论如何都不会正常运行)。

10.png

在最初的拆封过程中有一些校验和被用来阻止篡改,不过它们看起来更像是检查一个硬编码的值,该值不用于任何其他用途。因此,可以很容易地更改结果值或完全删除检查而不产生任何后果。

除此之外,它是寻找进程名称的非常典型的检查(例如ollydbg,idaq,softice等)。

需要注意的一件事是,因为我们标记了LC API函数,所以我们还可以看到这些API调用的执行位置。这样,我们可以看到调用LC_passwd的位置,这意味着,我们也知道了硬编码后的加密狗密码的位置。

11.png

现在,我们就可以开始解析封装器的流程了。

解析封装过程

在对大多数内容进行标记分析之后,解析封装器的过程就非常简单。所以我会简要的说一下,因为有很多示例文件,比如动态绑定解析、反调试检查和反vm或反篡改检查,它们在大多数封装的二进制文件和一些日常管理工作中都很常见。解析过程如下:

* Iterate each section of the exe that is marked as executable (e.g. a CODE section)
  -> Log into the Dongle,
  -> Send the hardcoded exe key (16 bytes) to the dongle's "encrypt" fuction.
  -> Retrieve the 16 byte key result.
  -> Use the key result in a Rijndael function to decrypt the given section in place.

* Go to a hardcoded offset in the .lcsh section

* Decrypt the a blob of data scraped from the original Import Address Table (in place)
  using the same method of getting a key and Rijndael and all that stuff from the code
  sections.

* Read the original IDT in plaintext from a hardcoded offset in the .lcsh section.

* Use the newly decrypted original IAT and IDT to write various offsets to
  relocation points and additional locations to reflect the IAT's new location.

* Jump to OEP (this address is also hardcoded into .lcsh).

1.可以看出,这个封装器做的任何事情都不会造成损失或破坏。因此,我们决定静态地重构一个可运行的非封装可执行文件。

2.我们应该能够复制所有这些步骤,值得注意的是,搞清楚整个过程不需要动态分析。因为没有垃圾代码、阻止反汇编的宏和混淆策略,所以静态地搞清楚这些步骤非常简单。

3.既然我们理解了这个过程,那么我们应该能够构建一个可以解压缩给定可执行文件的脚本。不过其中会受到一些限制:

3.1我们需要加密那个exe密钥,Envelope生成器会随机创建一个128位的值用于加密狗。因此,所有的exe都是不同的。因此,如果发布了更新的可执行文件,则还得依赖加密狗。

3.2我们可能希望选择性地指定一个预先录制的加密密钥,这样就不总是需要加密狗了,并在使用加密狗时,使用某种打印输出来查看生成的加密密钥。

3.3我们有一个dll ,甚至有一个64位dll,但由于API存在漏洞,所以供应商建议使用32位版本。因此,我们将使用带有Python Ctypes的32位供应商API dll,,并在我们的解封器中放入合法的加密狗交互。

除了加密狗的硬编码密钥和密码之外,它们所在的一般区域还有一些其他有趣的值。

13.png

在CFFExplorer中打开我们的原始exe,此时会出现一些值。

14.png

此时,我们就可以开始构建这个area中存在的内容了,并正确解压所涉及的重要可执行文件,比如:

1.OEP(即程序的入口点,可以用OD载入,不分析代码);

2.电子狗密码;

3.预加密的EXE密钥;

4.原始映像基底;

5.加密的IAT偏移和大小;

6.IDT偏移量和大小;

7. 初始重定位目录;

编写一个解封器

注意:其中包含了代码。

为了使整个编写过程更容易,我们将使用2个Py3模块:

1.pefile; 

2.py3rijndael; 

PEfile模块非常适合处理pe文件或自动执行这类任务,我们在示例中使用的是Rijndael的初始py3模块,因为这是封装器用来加密和解密代码块的。

第1步:加载Senselock数据块

首先是加载PE文件并从数据块中转储有趣的值,类似于:

15.png

注意,如果加密狗可用,我们可以使用针对“LC.dll”(pylc)创建的最小绑定派生出EXE密钥。密码和开发人员ID可以直接从可执行文件中读取。因此,不需要额外的配置。

第2步:解密所有代码段

接下来,我们必须迭代每个部分,并确定它是否可执行。我们可以通过检查“IMAGE_SCN_MEM_EXECUTE”标志的每个部分来实现这一点。如果是这样,我们使用Rijndael和EXE密钥解密该部分。

对比我们的封装和非封装版本,我们可以看到.text部分发生了变化:

16.png

事实上,与原版相比,我们似乎取得了进步!

016.png

即使在IDA中加载它并转到指定的OEP,我们也可以看到事情正在恢复正常。

18.png

第3步:解密IAT信息

接下来,我们需要解密该IAT块以查看我们正在使用的内容。内置结构是以下这样的:

· bytes – encrypted_iat_information

· bytes – original_idt

19.png

值得注意的是,这不仅仅是原始IAT的副本。Thunk条目已被删除并存储在自定义表中,在重构IAT之前需要对该表进行解析。

注意:以下步骤的难度将取决于PE内部结构和测试者导入方面的经验,在此,我建议你阅读一下该内容。http://www.reverse-engineering.info/SystemInformation/iat.html#sec3.2

第4步:解析IAT Blob

接下来的任务实际上是加载我们解密的IAT blob,并将其视为一个内置了几个字符串表的多链表。

20.png

特别值得注意的是,该表还考虑了按正常方式(IMAGE_ORDINAL_FLAG32)显示的导入序号。

在处理结束时,我们应该有一个看起来像这样的列表:

KERNEL32.dll
       CreateFileA
       LoadLibraryA
       CloseHandle
       IsDebuggerPresent
       InitializeSListHead
       GetSystemTimeAsFileTime
       GetCurrentThreadId
       GetCurrentProcessId
       QueryPerformanceCounter
       IsProcessorFeaturePresent
       TerminateProcess
       GetCurrentProcess
       SetUnhandledExceptionFilter
       UnhandledExceptionFilter
       GetModuleHandleW
VCRUNTIME140.dll
       memset
       _except_handler4_common
api-ms-win-crt-stdio-l1-1-0.dll
...

我们现在将“表”(LOL)列表,并在处理导入描述符表(IDT)之后将其返回。

第5步:恢复原始IDT

22.png

如前所述,原始IDT完好无损,并遵循加密的IAT信息blob。事实上,IDT仍然保留其最初位于IAT头部时的RVA(relative Virtual Address,相对虚拟地址偏移),它没有被修改并遵循典型的结构。

_fields_ = [('p_original_first_thunk', ctypes.c_int32),
            ('time_datestamp', ctypes.c_uint32),
            ('forwarder_chain', ctypes.c_int32),
            ('p_name', ctypes.c_int32),
            ('p_first_thunk', ctypes.c_int32)]

我们还知道,在这个示例中,包含8个条目(7个真实条目和1个空白)来表示表的结尾,每个长度为20字节。

这几步下来,重构就算完成了。如果你想尝试查找IDT过去的位置,可以通过以下步骤:

1.寻找最小的OFT值;

2.减去0x14 * number_of_idt_entries;

24.png

在大多数情况下,这应该是IDT的原始位置。由此我们知道,封装器不能在原始PE映像中准确地重建该表,它在lcsh中“in place”运行它,并更新所有重定位偏移量来进行匹配。

第6步:恢复DLL字符串位置!

因为知道IDT的位置和名称RVA,这意味着我们也知道这些dll名称字符串的位置。

作为奖励,让我们把它们放回原来的位置,以便让那些RVA值保持有效,而不是浪费额外的空间!

25.png

如上所述,编译过程将dll字符串内联到THUNK条目中,它们位于_IMAGE_IMPORT_BY_NAME结构的旁边,而且它们也是无序的。当然,他们中的一些也有多个尾随null( trailing null),至于是什么原因,我们也不清楚。

第7步:创建一个新的Thunk数组位置

为此,我们需要遍历前面创建的导入列表,找到函数名称,然后按名称结构创建导入。应该注意,由于序号不需要_IMAGE_IMPORT_BY_NAME结构,所以没有序号信息。

但是,对于每次导入,我们将添加偏移量,我们将把它写入列表供以后参考,因为这些偏移量将在以后修复IAT时需要用到。

26.png

第8步:调整重定位

现在要进入最后的调整阶段了,为此我们必须创建一个值表,通过RVA指向_IMAGE_IMPORT_BY_NAME结构,或者通过迭代导入列表将序号排列好。

通过IDT,我们可以在两个位置:FirstThunk和OriginalFirstThunk写入这些偏移量。

第9步

1.现在,我们必须将导入部分偏移量更改为前面编写IDT标头的值,同时更改导入表的总体大小。

2.我们必须将EntryPoint调整为真正的OEP;

3.我们应该重命名.lcsh部分让我们知道它已被修改;

27.png

从上图可以看出,构建结果非常完美。

28.png

由于名称表的导入偏移量是不同的,并且我们最后添加了一些额外的内容,所以一些PE标头偏移量会和原来的架构有所不同。除此之外,未封装的exe的大小为10074字节(原始值为9216字节,封装后为102912字节)。

现在,我们可以清理该架构,检查所封装的“代码”目录即可。但是,你需要从供应商SDK(Clave2 Basic_v2.2.2.2.zip)下载LC.dll文件。

总结

1.供应商将客户端库锁定到特定客户是有原因的;

2.有时候,一些混淆或反汇编策略可能会对分析封装工作产生巨大影响;

3. 如果可执行文件使用加密狗,如加密的应用程序文件或通过利用加密狗事务进行网络通信,那本文所讲的方法的效果就会差很多;

4.与封装器混合的硬件加密模块很少能构建出更复杂的安全模型;

本文翻译自:https://github.com/batteryshark/writeups/tree/master/20190417如若转载,请注明原文地址: https://www.4hou.com/vulnerable/17666.html
点赞 5
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论