【S2E】【翻译】用S2E分析“基于触发的”恶意软件

【翻译】用 S2E 分析“基于触发的”恶意软件

原文链接:https://adrianherrera.github.io/post/malware-s2e/

引言

本文希望帮助那些想通过符号执行或者 S2E 来分析恶意软件行为的人。

恶意软件分析有什么不同?

我的之前的博客使用 S2E 解决了 CTF 问题解析文件,他们有两个共同点:

  1. 都是 Linux ELF 文件;
  2. 程序的输入都由用户指定——stdin,或者是通过磁盘上的文件。

相反,大多数恶意软件:

  1. 是 Windows 下的程序(虽然一些报告指出 Android 恶意软件的数量在上升,但是 Windows 下的恶意软件仍然占主流);
  2. 并不精确定义输入源。输入可能来自命令行参数,但并不常见。输入更有可能来自注册表,或者来自网络等。

由于这些原因,用 S2E 分析恶意软件不能简单地将命令行参数符号化或者传递一个符号化文件。这篇博文将用两个“学习用例”介绍我们为恶意软件分析开发的基于 S2E 的工具 。相关代码在 Github 上。

在 S2E 下分析 Windows 软件

目前为止,我们只分析了 Linux 程序。幸运的是,S2E 也支持对 Windows 程序的分析。所以有什么不同点呢?

  1. 构建 Windows 镜像的时候(使用 s2e image_build),Windows ISO 必须被指定。S2E 支持的所有 Windows 版本 (在这里查看支持的版本)都可以在 MSDN 下载。这篇博客里使用的是 Windows 7 Professional 32-bit。

  2. Windows 下没有和 s2e.so 对等的链接库。因此,我们需要别的方法将符号变量插入恶意软件。我们可以编写 S2E 插件来这么做,但是会很复杂。所以我们使用 DLL 注入的方法来 hook Windows 的 API 调用,然后通过 hook 函数来传递符号变量。

    PS:复杂也许指的是指定某些内存地址,依次写入符号变量吧,但是 angr 就是这么做的,不知道最近的版本有没有改进。

Hooking Windows API

hook API 调用由很多方法。我们用一个现成的方案,而不是造个新轮子。一开始我们选择了 Cuckoo SandboxMonitor (它本身就被设计用于分析恶意代码),但我们最后选用了 EasyHook,因为入门更容易。

在开始代码之前,还有一些工程需要构建:

  • malware-inject :一个启动其他程序(比如恶意程序)的启动器,会将 DLL 注入新启动程序的地址空间;
  • malware-hook :构建一个 DLL,该 DLL 将被注入另一个程序的地址空间。这个 DLL 能够 hook Windows 的 API 调用,提供给我们注入符号变量的方式。

现在开始代码。

在 Visual Studio 中打开 $S2EDIR/source/s2e/guest/windows/s2e.sln ,然后创建两个新的项目(Project):

  • malware-inject ;
  • malware-hook

每个项目都需要 EasyHook 的原生包文件,可以通过 Nuget 下载。注意在 Github 仓库中 malware-hook 项目被分割为 GetLocalTime-hookwannacry-hook 两个项目,对应两个学习用例。

malware-inject 项目

malware-inject 项目是基于 EasyHook 实现的注入程序。然而,不同于使用 RhInjectLibrary (将 DLL 注入一个已经运行的进程),我们使用 RhCreateAndInject 。这个函数将启动一个程序并挂起,注入 DLL 之后再继续运行被挂起的程序。malware-inject 也会等到被注入的进程执行完再结束。这样做会防止 S2E 在 malware-inject 进程停滞的时候杀死 states。

创建 inject.c 然后写入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <stdio.h>
#include <string.h>

#include <Shlwapi.h>
#include <Windows.h>

#include <easyhook.h>

// We must add this header file to support writing to S2E's logs. s2e.h resides
// in the libcommon project, so the libcommon project must be added as a
// dependency to the malware-inject project
#define USER_APP
#include <s2e/s2e.h>

#define S2E_MSG_LEN 512
#define MAX_PATH_LEN 256

static INT s2eVersion = 0;

static void Message(LPCSTR fmt, ...) {
CHAR message[S2E_MSG_LEN];
va_list args;

va_start(args, fmt);
vsnprintf(message, S2E_MSG_LEN, fmt, args);
va_end(args);

if (s2eVersion) {
S2EMessageFmt("[malware-inject] %s", message);
} else {
printf("[malware-inject] %s", message);
}
}

static void GetFullPath(LPCWSTR path, PWCHAR fullPath) {
if (!path) {
Message("Path has not been provided\n");
exit(1);
}

if (!PathFileExistsW(path)) {
Message("Invalid path %S has been provided\n", path);
exit(1);
}

if (!GetFullPathNameW(path, MAX_PATH_LEN, fullPath, NULL)) {
Message("Unable to get full path of %S\n", path);
exit(1);
}
}

int main() {
INT argc;
LPWSTR *argv = CommandLineToArgvW(GetCommandLineW(), &argc);

if (argc < 5) {
printf("Usage: %S [options..]\n"
" --dll <dll> Path to DLL to inject into the application\n"
" --app <target> Path to application to start\n"
" --timeout <time> Timeout value in milliseconds "
"(infinite if not provided)\n", argv[0]);
exit(1);
}

// Used by the Message function to decide where to write output to
s2eVersion = S2EGetVersion();

LPWSTR dllPath = NULL;
WCHAR fullDllPath[MAX_PATH_LEN];

LPWSTR appPath = NULL;
WCHAR fullAppPath[MAX_PATH_LEN];

DWORD timeout = INFINITE;

for (int i = 1; i < argc; ++i) {
if (wcscmp(argv[i], L"--dll") == 0) {
dllPath = argv[++i];
continue;
}

if (wcscmp(argv[i], L"--app") == 0) {
appPath = argv[++i];
continue;
}

if (wcscmp(argv[i], L"--timeout") == 0) {
timeout = wcstoul(argv[++i], NULL, 10);
continue;
}

Message("Unsupported argument: %s\n", argv[i]);
exit(1);
}

// Check that the given paths are valid
GetFullPath(dllPath, fullDllPath);
GetFullPath(appPath, fullAppPath);

// Start the target application (in a suspended state) and inject the given
// DLL
ULONG pid;
NTSTATUS result = RhCreateAndInject(appPath, L"", CREATE_SUSPENDED,
EASYHOOK_INJECT_DEFAULT,
#if defined(_M_IX86)
dllPath, NULL,
#elif defined(_M_X64)
NULL, dllPath,
#else
#error "Platform not supported"
#endif
NULL, 0, &pid);

if (FAILED(result)) {
Message("RhCreateAndInject failed: %S\n", RtlGetLastErrorString());
exit(1);
}

Message("Successfully injected %S into %S (PID=0x%x)\n", fullDllPath,
fullAppPath, pid);

DWORD exitCode = 1;

// Get a handle to the newly-created process and wait for it to terminate.
// Once the process has terminated, get its return code and return that as
// our return code
HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION,
FALSE, pid);
if (hProcess) {
WaitForSingleObject(hProcess, timeout);
GetExitCodeProcess(hProcess, &exitCode);
CloseHandle(hProcess);
} else {
Message("Unable to open process 0x%x: 0x%X\n", pid, GetLastError());
}

return exitCode;
}

当然,恶意软件完全有可能正在监视 API hook 的操作。虽然这是一个重要问题,但是我们不打算在这篇博客中处理。

?????????????

现在已经完成了向恶意软件注入 DLL 的方法了,接下来解决的是注入的 DLL 具体要做什么。

malware-hook 工程

相似地,我们用 EasyHook 的样例工程 BeepHook DLL 来编写 malware-hook,下面是我们的 hook DLL 的框架,放在 malware-hook.cpp 下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <Windows.h>
#include <strsafe.h>

#include <easyhook.h>

#define USER_APP
extern "C" {
#include <s2e/s2e.h>
}

#define S2E_MSG_LEN 512

static INT s2eVersion = 0;

static void Message(LPCSTR fmt, ...) {
CHAR message[S2E_MSG_LEN];
va_list args;

va_start(args, fmt);
vsnprintf(message, S2E_MSG_LEN, fmt, args);
va_end(args);

if (s2eVersion) {
S2EMessageFmt("[0x%x|malware-hook] %s", GetCurrentProcessId(),
message);
} else {
printf("[0x%x|malware-hook] %s", GetCurrentProcessId(), message);
}
}

// EasyHook will be looking for this export to support DLL injection. If not
// found then DLL injection will fail
extern "C" void __declspec(dllexport) __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO *);

void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO *inRemoteInfo) {
// Unused
(void*) inRemoteInfo;

// Used by the Message function to decide where to write output to
s2eVersion = S2EGetVersion();

// TODO initialize hooks

// The process was started in a suspended state. Wake it up...
RhWakeUpProcess();
}

所以,该怎么 hook?让我们从两篇绝佳的文章中获得灵感:David Brumley’s “Automatically Identifying Trigger-based Behaviour in Malware” 和 Andreas Moser’s “Exploring Multiple Execution Paths for Malware Analysis”。这两篇文章都是针对“基于触发的恶意软件”,意思是恶意操作只在特定的条件下触发。比如,恶意软件可能在某个指定日期才启动它的 payload,或者在命令行接收到特定数据或者从控制服务器接到指令时才执行恶意行为。在上面两个例子里,触发源就是当前的时间,以及从网络中获取的数据。其他可能的触发源包括(在 Moser 的文章中提到):

  • Internet connectivity;
  • Mutex objects;
  • Existence of files;
  • Existence of Registry entries; and
  • Data read from a file.

我们如何对基于触发的恶意软件进行分析呢?Brumley 的文章提出了 Minesweeper,一个检测触发行为并找到对应行为触发输入的工具。然而直到目前为止,Minesweeper 都没有发布。但我们可以用 malware-hook DLL 在 S2E 内构建一个类似功能的系统。让我们继续为某些触发源创建 hooks 。

学习用例 1:GetLocalTime-test

Brumeley 提到的第一个触发源是 GetLocalTime 。定义如下:

1
2
3
void WINAPI GetLocalTime(
_Out_ LPSYSTEMTIME lpSystemTime
);

在 Minesweeper 中,用户需要指定内存中触发输入保存在哪里,这样符号执行引擎才能够将符号变量合理插入程序。在 GetLocalTime 这个例子中,GetLocalTime 调用的时候将时间信息保存在一个 16 字节的结构体,指针保存在栈中。 幸运的是,我们不必担心底层细节,只需要调用 S2EMakeSymbolic 来将 lpSystemTime 的内容设置为符号化的即可。在 malware-hook 中我们是这样做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static void WINAPI GetLocalTimeHook(LPSYSTEMTIME lpSystemTime) {
Message("Intercepted GetLocalTime\n");

// Call the original GetLocalTime to get a concrete value
GetLocalTime(lpSystemTime);

// Make the value concolic
S2EMakeSymbolic(lpSystemTime, sizeof(*lpSystemTime), "SystemTime");
}

// The names of the functions to hook (and the library they belong to)
static LPCSTR functionsToHook[][2] = {
{ "kernel32", "GetLocalTime"} ,
{ NULL, NULL },
};

// The function hooks that we will install
static PVOID hookFunctions[] = {
GetLocalTimeHook,
};

// The actual hooks
static HOOK_TRACE_INFO hooks[] = {
{ NULL },
};

// This function was defined previously
void __stdcall NativeInjectionEntryPoint(REMOTE_ENTRY_INFO *inRemoteInfo) {
// ...

// Replace the previous TODO with the following code to install the
// GetLocalTime hook
for (unsigned i = 0; functionsToHook[i][0] != NULL; ++i) {
LPCSTR moduleName = functionsToHook[i][0];
LPCSTR functionName = functionsToHook[i][1];

// Install the hook
NTSTATUS result = LhInstallHook(
GetProcAddress(GetModuleHandleA(moduleName), functionName),
hookFunctions[i],
NULL,
&hooks[i]);

if (FAILED(result)) {
Message("Failed to hook %s.%s: %S\n", moduleName, functionName,
RtlGetLastErrorString());
} else {
Message("Successfully hooked %s.%s\n", moduleName, functionName);
}

// Ensure that all threads _except_ the injector thread will be hooked
ULONG ACLEntries[1] = { 0 };
LhSetExclusiveACL(ACLEntries, 1, &hooks[i]);
}

// ...
}

我们从 Brumley 的论文中摘取一个测试程序来检测我们的 hook 能否正常执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <Windows.h>

void ddos (LPCSTR target) {
// DDOS code goes here :)
}

int main() {
SYSTEMTIME systime;
LPCSTR site = "www.usenix.org";

GetLocalTime(&systime);

if (9 == systime.wDay) {
if (10 == systime.wHour) {
if (11 == systime.wMonth) {
if (6 == systime.wMinute) {
ddos(site);
}
}
}
}

return 0;
}

确保你在 x86 平台下正确编译了每个工程(因为我们使用 32-bit 的 Windows 7 虚拟机)。一旦所有工程都构建好了(包括镜像),我们就可以创建一个新的 S2E 工程:

1
s2e new_project -i windows-7sp1pro-i386 /path/to/malware-s2e/GetLocalTime-test/Debug/GetLocalTime-test.exe

工程创建时会生成 bootstrap.sh ,它会直接运行 GetLocalTime-test.exe。我们必须修改 bootstrap.sh 来用 malware-inject.exe 来执行 GetLocalTime-test.exe 。我们需要在虚拟机内部访问我们编译的 hook 工具程序。我们可以通过在 s2e-env 环境下执行下列代码来在我们的工程目录下创建必要的软链接:

1
2
3
4
5
cd $S2EDIR/projects/GetLocalTime-test
HOOK_FILES="EasyHook32.dll malware-hook.dll malware-inject.exe"
for FILE in $HOOK_FILES; do
ln -s $S2EDIR/source/s2e/guest/windows/Debug/$FILE $FILE
done

然后修改 bootstrap.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ...

# The target does not get executed directly - we execute it via malware-inject
function execute_target {
local TARGET
TARGET="$1"

./malware-inject.exe --dll "./malware-hook.dll" --app ${TARGET}
}

# ...

# We also need to download the files required for hooking

# Download the target file to analyze
${S2EGET} "GetLocalTime-test.exe"

${S2EGET} "EasyHook32.dll"
${S2EGET} "malware-hook.dll"
${S2EGET} "malware-inject.exe"

# ...

最后,我们可以在 s2e-config.lua 中禁用下面的插件,因为不需要:

  • WebServiceInterface
  • KeyValueStore
  • MultiSearcher
  • CUPASearcher
  • StaticFunctionModels

结果

我们看到 S2E 在分析过程中 fork 了四次。如果我们在 KLEE 参数中启用了 --verbose-fork-info (在 s2e-config.lua 中),就能看到在这 4 个分裂点各自产生的约束。分裂点的位置在下图的高亮处:

GetLocalTime-test fork points

ReadLSB w16 X SystemTime 可以理解为 “读取符号变量 SystemTime 中偏移为 X 处的 16bit 数据(即一个 WORD)”。如果我们在 MSDN 中查看 SYSTEMTIME 结构体,我们能看到,每个 word 在对应的偏移处(0x6, 0x8, 0x2, 0xa)都对应于 wDay,wHour,wMonth,wMinute 的字段。最后,我们应该能够在 debug.txt 中找到一行信息,包含下面的测试用例(为修改了这行的格式,添加了字段名称来增加可读性):

1
2
3
4
5
6
7
8
TestCaseGenerator:  v0_SystemTime_0 = {0x0, 0x0, /* wYear */
0xb, 0x0, /* wMonth */
0x0, 0x0, /* wDayOfWeek */
0x9, 0x0, /* wDay */
0xa, 0x0, /* wHour */
0x6, 0x0, /* wMinute */
0x0, 0x0, /* wSecond */
0x0, 0x0} /* wMilliseconds */

如果我们将其与 GetLocalTime-test/test.c 进行交叉引用,我们可以看到现在是启动 DDOS 的时候了。 成功!

学习用例2:WannaCry

前一个例子很棒。但是恶意软件从 Minesweeper 这篇文章发布以来(2007 年)也发展了许多。让我们来看一个更近的例子——WannaCry 勒索软件。众所周知,WannaCry 包含一个“killswitch”,可以阻止勒索软件加密目标数据。 这个 killswitch 条件是,检查一个胡扯的 URL 是否真的指向了一个网页。如果这个 URL 可以访问,那么 WannaCry 就会中止运行(这个检查可能用于欺骗动态分析工具,因为动态分析工具通常会配置为对网络请求返回有效响应)。在已知这些信息的情况下,让我们用 S2E 来分析 WannaCry 的行为,判断触发条件在合适满足。我们会针对 Amanda Rousseau 的精彩 Writeup 来讨论。(SHA1 hash 24d004a104d4d54034dbcffc2a4b19a11f39008a575aa614ea04703480b1022c)

反汇编代码

简单看一下 WannaCry 的 killswitch 代码:

WannaCry killswitch

我们可以看到 WinINet API 用于打开一个 killswitch 规定的 URL 链接(hxxp://www[.]iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea[.]com)。下面的函数是:

  • InternetOpenA: 初始化 WinINet 系统,返回一个 HINTERNET 句柄,NULL 表示失败;
  • InternetOpenUrlA: 使用 InternetOpenA 返回的句柄打开一个给定的 URL。返回一个 HINTERNET 句柄,NULL 表示失败;
  • InternetCloseHandle: 关闭由 InternetOpenAInternetOpenUrlA 打开的句柄。

至少,我们必须 hook InternetOpenUrlA ,并且强制 fork 来探索 0x4081a5 的两条路径。至于 InternetOpenA 呢?我们可以看到在 WannaCry 代码中, InternetOpenA 返回的 HINTERNET 句柄从未被检查。所以我们不必担心这个函数。类似的,如果我们对 InternetOpenUrlA 返回失败的分支感兴趣,我们也可以强制 S2E 在某些符号变量上 fork。然而,简单期间我们只关注 InternetOpenUrlA

WinINet hooks

首先,替换 malware-hook.cpp 中编写的 hook 函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static LPCSTR functionsToHook[][2] = {
{ "wininet", "InternetOpenUrlA" },
{ "wininet", "InternetCloseHandle" },
{ NULL, NULL },
};

static PVOID hookFunctions[] = {
InternetOpenUrlAHook,
InternetCloseHandleHook,
};

static HOOK_TRACE_INFO hooks[] = {
{ NULL },
{ NULL },
}

然后编写实际的 hook 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/// Keep track of dummy Internet handles that we've created
static std::set<HINTERNET> dummyHandles;

static HINTERNET WINAPI InternetOpenUrlAHook(
HINTERNET hInternet,
LPCSTR lpszUrl,
LPCSTR lpszHeaders,
DWORD dwHeadersLength,
DWORD dwFlags,
DWORD_PTR dwContext
) {
Message("Intercepted InternetOpenUrlA(%p, %s, %s, 0x%x, 0x%x, %p)\n",
hInternet, lpszUrl, lpszHeaders, dwHeadersLength, dwFlags, dwContext);

// Force a fork via a symbolic variable. Since both branches are feasible,
// both paths are taken
UINT8 returnResource = S2ESymbolicChar("hInternet", 1);
if (returnResource) {
// Explore the program when InternetOpenUrlA "succeeds" by returning a
// dummy resource handle. Because we know that the resource handle is
// never used, we don't have to do anything fancy to create it.
// However, we will need to keep track of it so we can free it when the
// handle is closed.
HINTERNET resourceHandle = (HINTERNET) malloc(sizeof(HINTERNET));

// Record the dummy handle so we can clean up afterwards
dummyHandles.insert(resourceHandle);

return resourceHandle;
} else {
// Explore the program when InternetOpenUrlA "fails"
return NULL;
}
}

static BOOL WINAPI InternetCloseHandleHook(HINTERNET hInternet) {
Message("Intercepted InternetCloseHandle(%p)\n", hInternet);

std::set<HINTERNET>::iterator it = dummyHandles.find(hInternet);

if (it == dummyHandles.end()) {
// The handle is not one of our dummy handles, so call the original
// InternetCloseHandle function
return InternetCloseHandle(hInternet);
} else {
// The handle is a dummy handle. Free it
free(*it);
dummyHandles.erase(it);

return TRUE;
}
}

这里,我们跟随 S2E multi-path fault injection tutorial 的指示。符号变量 returnResource 会触发强制 fork,导致其中一条路径 InternetOpenUrlA 的返回值是成功的,另一条路径返回失败。我们可以返回一个虚拟资源句柄,因为这个句柄并没有被真正使用,WannaCry 只是检查它是不是 NULL 而已。InternetCloseHandle hook 之后清理了分配的内存。接下来我们进行 hook,并在 S2E 中运行 WannaCry。

最初的结果

我们可以按照 [GetLocalTime-test](#学习用例 1:GetLocalTime-test) 的流程建立一个 WannaCry 的 S2E 工程。

在运行 S2E 之前,在 s2e-config.lua 中启用 LibraryCallMonitor 插件,这个插件可以监视并记录外部链接库函数的调用,可以让我们更好地了解 WannaCry 正在做什么。当运行 S2E 时,可以看到 fork 发生在 malware-hooks 的地址空间里(可能隐藏在 LibraryCallMonitor 输出的大量日志中),如果你只查看 WannaCry 本体发出的外部哭函数调用(而不是库函数对库函数对调用),将能够看到在 state 0 时,有下面的调用:

Address DLL Function
0x4081bc wininet InternetCloseHandle
0x4081bf wininet InternetCloseHandle
0x409b4e msvcrt exit

在 state 1 时,能看到:

Address DLL Function
0x4081a7 wininet InternetCloseHandle
0x4081ab wininet InternetCloseHandle
0x40809f kernel32 GetModuleFileNameA
0x4080a5 msvcrt __p___argc
0x407c56 msvcrt sprintf
0x407c68 advapi32 OpenSCManagerA
0x407c9b advapi32 CreateServiceA
0x407cb2 advapi32 StartServiceA
0x407d74 kernel32 FindResourceA
0x407d86 kernel32 LoadResource
0x407d95 kernel32 LockResource
0x407da9 kernel32 SizeofResource
0x407ee8 kernel32 CreateProcessA

这看起来很不错:我们成功地分析了 killswitch 触发时和没触发时 WannaCry 的行为。Rousseau 的 writeup 描述了 WannaCry 的执行流程,如果我们跟随 state1 的库函数调用,我们可以看到这和执行流程是匹配的。

Hook 进程的创建

让我们来编写最后一个 hook。如果我们 hook 的进程产生了一个新进程,会发生什么?这对于 dropper 类型的恶意软件很常见。事实上,WannaCry 是通过从资源加载可执行文件 (tasksche.exe),将其写入磁盘然后运行它(通过 CreateProcessA)来实现的。 在这种情况下,我们将对新启动的进程一无所知,无论是通过我们的钩子注入符号数据还是使用 S2E 跟踪其行为(例如通过 LibraryCallMonitor 插件捕获调用信息)。

我们可以通过挂钩 CreateProcessA 并使用 EasyHook API 将 malware-hook 注入这个新进程来解决前者(失去将符号数据注入新进程的能力)。 以下代码实现了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Don't forget to add CreateProcessA to the functionsToHook, hookFunctions and
// hooks arrays

BOOL WINAPI CreateProcessAHook(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
) {
Message("Intercepted CreateProcessA(%s, %s, %p, %p, %d, %d, %p, %s, %p, %p)",
lpApplicationName, lpCommandLine, lpProcessAttributes,
lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment,
lpCurrentDirectory, lpStartupInfo, lpProcessInformation);

// Get this DLL's path
HMODULE hDll = NULL;
DWORD hModFlags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT;
if (!GetModuleHandleEx(hModFlags, (LPCTSTR)&Message, &hDll)) {
Message("Failed to retrive DLL handle: 0x%X\n", GetLastError());
goto default_create_process;
}

WCHAR dllPath[MAX_PATH_LEN];
if (!GetModuleFileNameW(hDll, dllPath, MAX_PATH_LEN)) {
Message("Failed to retrive DLL path: 0x%X\n", GetLastError());
goto default_create_process;
}

// Create the new process, but force it to be created in a suspended state
if (!CreateProcessA(lpApplicationName, lpCommandLine, lpProcessAttributes,
lpThreadAttributes, bInheritHandles, dwCreationFlags | CREATE_SUSPENDED,
lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation)) {
Message("Failed to create suspended process: 0x%X\n", GetLastError());
goto default_create_process;
}

// Inject ourselves into the new, suspended process.
// NativeInjectionEntryPoint will call RhWakeupProcess, which will kick
// ourselves out of the suspended state
NTSTATUS result = RhInjectLibrary(lpProcessInformation->dwProcessId,
lpProcessInformation->dwThreadId, EASYHOOK_INJECT_DEFAULT,
#if defined(_M_IX86)
dllPath, NULL,
#elif defined(_M_X64)
NULL, dllPath,
#else
#error "Platform not supported"
#endif
NULL, 0);

if (FAILED(result)) {
Message("RhInjectLibrary failed: %S\n", RtlGetLastErrorString());
goto default_create_process;
}

Message("Successfully injected %S into %s %s (PID=0x%x)\n", dllPath,
lpApplicationName, lpCommandLine, lpProcessInformation->dwProcessId);

return TRUE;

default_create_process:
return CreateProcessA(lpApplicationName, lpCommandLine, lpProcessAttributes,
lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment,
lpCurrentDirectory, lpStartupInfo, lpProcessInformation);
}

这个 hook 将会启动一个新进程并挂起,然后将自己注入到新进程中。malware-hookNativeInjectionEntryPoint 函数负责将挂起的进程启动。

这样能够解决符号变量无法传递的问题。那么如何让 S2E 追踪新进程的执行呢?不幸的是,这需要额外的工作。一种解决方案是编写一个 S2E 插件监听 OSMonitor 的 onProcessLoad 信号,如果发现一个进程由 WannaCry 启动,那么就将这个新的进程添加到 ProcessExecutionDetector 的追踪列表中。LibraryCallMonitor 随后会对新进程发出 onLibraryCall 事件,让我们能够追踪新进程的行为。因为这篇博客中我希望避免编写 S2E 插件来实现,所以我把这个作为“留给读者的练习”。

求求你把这个插件写出来吧

最后一个问题:原始的 WannaCry 进程将会在它启动 tasksche.exe 之后停止,这导致 malware-inject 也会停止运行(因为它调用了 waitForSingleObject ),随即导致 bootstrap.sh 杀死当前状态。不幸的是,这意味着 S2E 会在 WannaCry 进行有趣的操作(例如加密我们的数据)之前终止。对于这个问题,一个解决方案是:在 bootstrap.sh 的 execute 函数中添加一个 sleep 语句(别忘记设置一个合适的睡眠时间)。更好的解决方案是等待 tasksche.exe (以及其他子进程)终止。我们可以添加一个函数来实现这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Set a sensible timeout value (in milliseconds). Can also be INFINITE
#define CHILD_PROCESS_TIMEOUT 10 * 1000

/// Keep track of child proceses (such as tasksche.exe)
static std::set<DWORD> childPids;

static BOOL WaitForChildProcesses(DWORD timeout) {
bool retCode = TRUE;

if (childPids.size() > 0) {
// Convert the set of PIDS to a list of handles with the appropriate
// permissions
std::vector<HANDLE> childHandles;
for (DWORD pid : childPids) {
Message("Getting handle to process 0x%x\n", pid);
HANDLE childHandle = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_INFORMATION,
FALSE, pid);
if (childHandle) {
childHandles.push_back(childHandle);
} else {
Message("Unable to open child process 0x%x: 0x%X\n", pid,
GetLastError());
return FALSE;
}
}

// Wait for the processes to terminate
Message("Waiting %d ms for %d children processes to terminate...\n",
timeout, childHandles.size());
DWORD waitRes = WaitForMultipleObjects(childHandles.size(),
childHandles.data(), TRUE, timeout);
switch (waitRes) {
case WAIT_FAILED:
Message("Failed to wait for child processes: 0x%X\n", GetLastError());
retCode = FALSE;
break;
case WAIT_TIMEOUT:
Message("Timeout - not all child processes may have terminated\n");
break;
}

// Close all handles
for (HANDLE handle : childHandles) {
CloseHandle(handle);
}
}

return retCode;
}

WaitForChildProcesses 应该在被 hook 的 WannaCry 函数结束时被调用。我们可以通过添加 DLLMain 并检查 fdwReason 是否为 DLL_PROCESS_DETACH 来实现:

1
2
3
4
5
6
7
8
9
10
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
switch (fdwReason) {
// Don't exit until all child processes have terminated (or a timeout is
// reached)
case DLL_PROCESS_DETACH:
return WaitForChildProcesses(CHILD_PROCESS_TIMEOUT);
}

return TRUE;
}

最后,别忘记添加下面的代码到 CreateProcessAHook 来追踪子进程。子进程应该只在成功 hook 时被保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// This function was defined previously
static BOOL WINAPI CreateProcessAHook(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
) {
// ...

// Save the newly-created process' PID
childPids.insert(lpProcessInformation->dwProcessId);

return TRUE;

// ...
}

如果你在 bootstrap.sh 中注释掉了 GRAPHICS=-nographic ,那么你最后能够看到下面的图片:

WannaCry infection

总结和下一步计划

在这篇博客,我们研究了如何用 S2E 分析 Windows 恶意程序,最重要的是在 S2E 下实现了 Minesweeper 工具。和之前实现的程序不同,我们必须提出一些新的技术来向 Windows 程序注入符号化数据。我们使用 EasyHook 来 hook 那些经常被恶意软件用来隐藏行为的触发函数。即使目前这些方案对这两个学习用例而言很成功,仍然又一些地方需要改进,包括:

  • hook 更多的 Windows API:Brumley 和 Moser 描述了很多不同的触发源(网络,注册表等),博客并没有覆盖这些。
  • 构建更复杂的 hook:比如, InternetOpenUrlA hook 其实过于简单了。它只返回了一个伪造的,分配在 heap 的地址。如果这个句柄后面被使用了,我们就必须也 hook 使用它的函数。这本质上是大多数符号执行引擎中继承的“环境建模”问题。
  • 对恶意软件隐蔽 hook 行为:有一些想法,比如将 Cuckoo Monitor 移植到 S2E,或者在 S2E 插件上实现这个功能。
  • 对恶意软件更广泛的研究:这种符号执行对恶意软件分析有帮助吗? 基于触发器的恶意软件有多普遍——我们能在 Cuckoo Sandbox 中进行动态分析吗? 恶意软件作者是否使用了 Banescu 在 Code Obfuscation Against Symbolic Execution Attacks 的工作中讨论的混淆技术,如果是,它们如何影响我们的分析?