这篇博客是上个月我在 瑞士联邦理工学院 的可信系统实验室做的一项工作,当时我正在用S2E分析恶意软件。尽管没有特别出彩的地方,但我希望这篇文章可以帮到那些想要用符号执行/S2E来分析恶意软件行为的人。
我之前发了2篇文章,一篇是关于 CTF challenge 的,另一篇是分析 文件解析器的。这些程序有两个共通之处:
相对来说,大多数软件:
由于这些原因,在S2E中分析恶意软件不太容易将命令行参数符号化,或者向程序输入符号文件。这篇文章会探索我们基于S2E开发的恶意软件分析工具,后面会有两个”研究实例“。像往常一样,如果你希望自己研究一下的话,可以在 Github上找到所有的代码。
直到现在,我们只用 S2E 分析过 Linux 程序。幸运的是,S2E也支持 Windows 程序的分析,所以两者的区别在哪呢?
有许多不同的技术可以实现 hook Windows API,我们会使用现有的办法而非再去发明一种新的。当我刚开始这项工作时,我想重用Cuckoo Sandbox的 Monitor 来 实现对API 的hook(它就是为了分析恶意软件而设计的)。然而,后来我们决定用 EasyHook 的办法,主要是因为它在开始阶段需要更少的工作。
在开始深入研究代码前,先看下我们下面要做的:
现在让我们深入研究一些代码!
在 Visual Studio 中 打开 $S2EDIR/source/s2e/guest/windows/s2e.sln
,创建两个项目:
这两个项目都需要 EasyHook 的本地包, 通过 Nuget 安装。 注意在Github 上的 malware-hook
项目分为 GetLocalTime-hook
和 wannacry-hook
两个项目 (我们的研究实例)。
malware-inject
基于 EasyHook 的样例程序 injector 。在这里,我们使用 RhCreateAndInject
而非RhInjectLibrary
(将 DLL 注入已经运行的程序)。 我们使用这个函数启动处于挂起状态的应用程序,注入DLL,然后恢复挂起进程。malware-inject
还会在返回之前等待注入的进程完成,这非常有用,因为它可以防止当malware-inject
进程退出时 S2E 终止状态。
创建 inject.c
文件,把下面的代码加进去:
当然,恶意软件完全可能会监视 API 的 hook(毕竟我们正在处理恶意软件),尽管这是一个重要问题,但在这里不做过多探讨。
既然我们已经编写了可以将 DLL 注入恶意软件的工具,那么让我们把注意力转向这个 DLL 实际要做的事情。
同样 ,我们将基于 EasyHook 的样例 程序 BeepHook 来 实现malware-hook
,下面是我们 hook 的 DLL 的框架, 也就是 malware-hook.cpp
的内容:
那么,我们要hook 什么呢? 我们可以从两篇关于这个主题的优秀论文中获得一些灵感:David Brumley的“自动识别恶意软件中基于触发器的行为“ 和Andreas Moser的 “探索恶意软件分析的多个执行路径”。 这两篇论文都着眼于“基于触发器的恶意软件”,即恶意软的恶意行为仅在特定场景下发生,例如,当满足特定触发条件时。举个例子,恶意软件可能只在特定日期(如 MyDoom 蠕虫那样)启动其有效负载,或者从命令和控制服务器接收特定数据。 在这两个示例中,触发源是当前日期/时间和从网络读取的数据。 其他触发源包括(如Moser的论文中所列的):
我们如何分析基于触发器的恶意软件? Brumley的论文提出了Minesweeper,用来检测基于触发器的行为的存在,并找到执行这些行为的输入。 据我所知,Minesweeper从未公开发布过。但是,我们可以通过使用 malware-hook
DLL,在S2E中搭建一个非常相似的系统!因此,让我们继续为这两篇论文中讨论的一些触发源创建 hook 。
Brumley 的论文探索的第一个触发源是 GetLocalTime 。GetLocalTime
的原型 如下:
在 Minesweeper 中,用户需要指定内存中的触发输入存储的位置。因此,符号执行引擎可以在执行期间正确地分配符号变量。在GetLocalTime
的例子中,需要指定GetLocalTime
将其结果存储在调用GetLocalTime
时由堆栈值指向的16字节结构中。 幸运的是,我们不必担心这些底层的细节。相反,我们可以在传递给GetLocalTime
的变量上调用S2EMAKECONCOLIC
。下面是 malware-hook
中的实现方法:
我们来实现一下 Brumley et al 的论文中的例子(图 1.1)来看下是否和预期的一样:
确保你是在x86平台上编译的这些项目(因为使用的是32-位 的Windows 7 虚拟机)。一旦编译完成(包括虚拟机),我们就能够创建一个新项目:
注意这会直接创建一个bootstrap.sh
脚本 执行 GetLocalTime-test.exe
。我们需要修改 bootstrap.sh
以便让 malware-inject.exe
执行 GetLocalTime-test.exe
。为了实现这个目的,需要有在虚拟机中访问 hook 工具的权限。通过下面的命令,创建符号链接即可:
然后编辑 bootstrap.sh
:
最后,可以在 s2e-config.lua
中禁止下面的插件(不是必要的):
现在准备开始分析!
可以看到,在分析过程中,fork 了四次。 如果 开启 --verbose-fork-info
KLEE 参数的话 (在 s2e-config.lua
中), 我们可以看到在这四个分叉点中生成的约束。下面的图片是这些点高亮的反汇编代码:
ReadLSB w16 X SystemTime
可以理解为 在符号变量SystemTime
的偏移X处 “读取 16 位 (i.e. 一个字) 。 如果我们在MSDN上查看SYSTEMTIME
的结构,可以看到这些偏移处的每个字(0x6
,0x8
,0x2
,0xA
)分别对应于wDay
,wHour
,wMonth
和wMinute
字段 - 正如预期的那样。最后,可以在debug.txt
中发现一行包含以下的测试用例(我重新格式化并添加了字段名称以便于阅读):
如果我们在GetLocalTime-test/test.c
上交叉引用它,就能看到这次造成了 DDOS,成功了!
看起来不错,但是从2007年Minesweeper 论文写完到现在,恶意软件已经发生了很大的变化。让我们看一下最新的东西-- WannaCry 勒索软件。 WannaCry 里有一个隐藏开关,可以阻止勒索软件加密目标数据。这个开关会检查 病毒是否可以访问一个奇怪的域名时,如果访问到的话,病毒终止运行(此检查可能是为了欺骗动态分析工具,工具通常被配置为对所有网络查询返回有效的、虚拟的响应)。 考虑到这一点,让我们使用S2E来探索WannaCry 在触发条件满足和不满足时的行为。我们将重点关注 Amanda Rousseau 出色的 writeup 中的样本(SHA1 哈希值 24d004a104d4d54034dbcffc2a4b19a11f39008a575aa614ea04703480b1022c)。
看下 WannaCry killswitch 的反汇编代码:
可以看到 WinINet API用于打开与 隐藏开关 URL的连接(hxxp://www[.]iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea[.]com)。调用以下函数来执行此操作:
最小限制下,我们需要hook InternetOpenUrlA,并强制 fork 以探索 0x4081a5 处的两条路径。InternetOpenA 呢? 可以在WannaCry
代码中看到InternetOpenA
返回的HINTERNET
句柄永远不会被检查,所以我们不必担心这个函数。 如果(正确地)检查了返回的句柄,我们可能就需要 hook 掉InternetOpenA
并强制它返回一些虚拟的非空值。类似地,如果我们对InternetOpenA
失败时执行的代码感兴趣,我们也可以强制使用fork获取一些符号值。但是,为了简便起见,我们只关注 InternetOpenUrlA
,下面开始我们的工作。
首先,在 malware-hook.cpp
换掉我们要 hook 的 函数,像下面这样:
然后写入实际要 hook 的函数:
在这里,我们遵循S2E的 多路径故障注入教程 中采用的方法。 returnResource
符号变量强制使用 fork,导致InternetOpenUrlA
成功的一个状态(通过返回虚拟资源)和InternetOpenUrlA
失败的另一个状态(通过返回NULL)。 我们可以返回一个虚拟资源句柄,因为InternetOpenUrlA
句柄从未实际使用过:记住,WannCry 只检查它是否为NULL。 然后,InternetCloseHandle
hook 清除已分配的内存,现在让我们在S2E中 hook 并运行WannaCry。
我们可以按照与GetLocalTime-test
相同的步骤为 WannaCry 建立S2E项目,记得在 bootstrap
脚本中为Easystook32.dll
,malware-hook.dll
和malware-inject.exe
创建符号链接, 并且 s2eget
它们 。
在运行S2E之前,请在s2e-config.lua
中启用LibraryCallMonitor
插件。 该插件监视并记录外部库函数调用,这能更好地帮助我们了解 WannaCry 正在做什么。 当您运行S2E时,您应该看到malware-hook
在地址空间中的fork(可能隐藏在LibraryCallMonitor
生成的大量调试输出中)。 如果您按照WannaCry可执行文件所做的库调用(而不是加载其地址空间中所有其他的DLL),您应该在状态0中看到以下库调用:
处于状态1 时,你应该看到:
这看起来不错:当隐藏开关被触发时,我们已成功探索了WannaCry的行为。 Rousseau的文章概述了WannaCry的执行流程,如果我们遵循状态1的库调用,我们应该看到执行流程是匹配的。
开始写最后一个 hook,如果我们 hook 的进程又产生一个新进程会发生什么呢? 这对于“dropper”类恶意软件来说非常常见,而 WannaCry 确实通过从资源加载可执行文件(tasksche.exe
),将其写入磁盘然后运行它(通过CreateProcessA
)来实现这一点。 当发生这种情况时,我们完全不知道这个新进程正在做什么:在通过我们的hook 注入符号化的数据和用S2E跟踪其行为方面(例如通过LibraryCallMonitor
插件)。
我们可以通过 hook CreateProcessA
并使用 EasyHook API 将 malware-hook
注入此新进程来解决这个问题(失去将符号数据注入新进程的能力), 下面的代码实现了这一点:
这个 hook 会 启动处于 挂起状态的新进程,并将其自身注入新进程,而malware-hook
的NativeInjectionEntryPoint
函数就负责唤醒进程。
这解决了将符号数据注入 WannaCry 产生的新进程的问题,但如何在S2E中跟踪这个新进程的行为呢?不幸的是,这需要更多的工作。一种方法是编写一个S2E插件,用于监听OSMonitor的onProcessLoad
信号。如果发现新进程来自 WannaCry 进程,我们可以将新的子进程添加到ProcessExecutionDetector
的跟踪模块。然后,LibraryCallMonitor
将开始为这个新进程发出onLibraryCall
事件,允许我们跟踪它的行为。因为我不想在这篇文章中编写S2E插件,所以我将其留作“读者练习”。
还有最后一个问题:原始的WannaCry进程在启动tasksche.exe
就结束了。这导致malware-inject
也退出了(记住它调用WaitForSingleObject
),导致bootstrap脚本终止当前状态(在我们的例子中,这是状态1,它对应于InternetOpenUrlA
返回为NULL的状态)。这有效地杀死了S2E,因为我们只有一个活动状态。有一种简单的方法可以解决这个问题:在bootstrap脚本中执行
调用之后添加一个sleep
命令(不要忘记设置适当的 sleep
时间)。但这也意味在状态0 时,我们会在 WannaCry 早早退出之后还在浪费时间等待。但是,如果您在launch-s2e.sh
中添加 sleep
并注释掉GRAPHICS = -nographic
(用来启用QEMU GUI),最终会看到下面的效果:
在这篇文章中,我们研究了使用S2E分析 Windows 恶意软件,实质上是在S2E中重新创建了 David Brumley 的Minesweeper工具。 与我们在之前的帖子中看过的程序不同,我们不得不想出一些新的技术来将符号化的数据注入到Windows程序中。 我们使用EasyHook来 hook 那些恶意软件常用来隐藏它们行为的“触发”函数。 虽然这种方法适用于我们的两个研究实例(这些案例被认为是高度人为的),但仍有许多改进途径。 这些途径包括:
希望这篇文章为您提供必要的背景和工具,以便了解其中的一些改进。也许有一天我甚至会找时间亲自看看其中的一些!
原文地址:https://adrianherrera.github.io/post/malware-s2e/
本文由 看雪翻译小组 fyb 波 编译
本文由 看雪翻译小组 银雁冰 校对
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2018-9-13 16:12
被fyb波编辑
,原因: 格式选择错误