使用S2E分析基于触发器的恶意软件
介绍
这篇博客是上个月我在 瑞士联邦理工学院 的可信系统实验室做的一项工作,当时我正在用S2E分析恶意软件。尽管没有特别出彩的地方,但我希望这篇文章可以帮到那些想要用符号执行/S2E来分析恶意软件行为的人。
恶意软件分析不同之处在哪?
我之前发了2篇文章,一篇是关于 CTF challenge 的,另一篇是分析 文件解析器的。这些程序有两个共通之处:
- 它们都是Linux ELF 可执行文件;
- 程序输入由用户指定 — 通过 STDIN 标准输入或者从磁盘上的文件读入。
相对来说,大多数软件:
- 针对Windows 平台(尽管有些 报告 显示Android 恶意软件的数量正在上升,但恶意软件作者的主要目标还是Windows;
- 没有一个良好定义的输入源。输入可能来源于命令行参数,但是不太常见。更可能的一些输入来源是注册表键值、网络数据等等。
由于这些原因,在S2E中分析恶意软件不太容易将命令行参数符号化,或者向程序输入符号文件。这篇文章会探索我们基于S2E开发的恶意软件分析工具,后面会有两个”研究实例“。像往常一样,如果你希望自己研究一下的话,可以在 Github上找到所有的代码。
在S2E中分析 Windows 软件
直到现在,我们只用 S2E 分析过 Linux 程序。幸运的是,S2E也支持 Windows 程序的分析,所以两者的区别在哪呢?
- 使用
image_build
命令编译Windows客户镜像的时候,需要提前准备好镜像,S2E 支持的所有版本的Windows 镜像 (列在 这里) 都可以从 MSDN上下到。如果需要的话,可以添加对其他版本的支持。在这篇文章中,我们选用的是 Windows 7 专业版 32-位。
- 在 Windows 上,没有
s2e.so
的 dll 版本。因此,我们需要换种方式来将符号化的数据注入到恶意软件中。我们可以写个 S2E 插件来实现,但有点复杂。相反,我们 通过 hook Windows API 来注入DLL,并且插入符号化的数据。
Hook Windows API
有许多不同的技术可以实现 hook Windows API,我们会使用现有的办法而非再去发明一种新的。当我刚开始这项工作时,我想重用Cuckoo Sandbox的 Monitor 来 实现对API 的hook(它就是为了分析恶意软件而设计的)。然而,后来我们决定用 EasyHook 的办法,主要是因为它在开始阶段需要更少的工作。
在开始深入研究代码前,先看下我们下面要做的:
malware-inject
:启动其他程序 (e.g. 恶意软件) ,将DLL 注入 新启动进程的地址空间;
malware-hook
: 通过 malware-inject
注入其他进程地址空间的 DLL ,这个 DLL 是 hook Windows API 的关键函数,给我们注入符号化的数据提供了一种机制。
现在让我们深入研究一些代码!
在 Visual Studio 中 打开 $S2EDIR/source/s2e/guest/windows/s2e.sln
,创建两个项目:
malware-inject
: Win32 控制台应用程序;
malware-hook
:Win32 DLL。
这两个项目都需要 EasyHook 的本地包, 通过 Nuget 安装。 注意在Github 上的 malware-hook
项目分为 GetLocalTime-hook
和 wannacry-hook
两个项目 (我们的研究实例)。
malware-inject
malware-inject
基于 EasyHook 的样例程序 injector 。在这里,我们使用 RhCreateAndInject
而非RhInjectLibrary
(将 DLL 注入已经运行的程序)。 我们使用这个函数启动处于挂起状态的应用程序,注入DLL,然后恢复挂起进程。malware-inject
还会在返回之前等待注入的进程完成,这非常有用,因为它可以防止当malware-inject
进程退出时 S2E 终止状态。
创建 inject.c
文件,把下面的代码加进去:
#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 来 实现malware-hook
,下面是我们 hook 的 DLL 的框架, 也就是 malware-hook.cpp
的内容:
#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的“自动识别恶意软件中基于触发器的行为“ 和Andreas Moser的 “探索恶意软件分析的多个执行路径”。 这两篇论文都着眼于“基于触发器的恶意软件”,即恶意软的恶意行为仅在特定场景下发生,例如,当满足特定触发条件时。举个例子,恶意软件可能只在特定日期(如 MyDoom 蠕虫那样)启动其有效负载,或者从命令和控制服务器接收特定数据。 在这两个示例中,触发源是当前日期/时间和从网络读取的数据。 其他触发源包括(如Moser的论文中所列的):
- 互联网连接;
- 互斥对象;
- 文件存在;
- 注册表项的存在;
- 从文件中读取的数据
我们如何分析基于触发器的恶意软件? Brumley的论文提出了Minesweeper,用来检测基于触发器的行为的存在,并找到执行这些行为的输入。 据我所知,Minesweeper从未公开发布过。但是,我们可以通过使用 malware-hook
DLL,在S2E中搭建一个非常相似的系统!因此,让我们继续为这两篇论文中讨论的一些触发源创建 hook 。
研究实例 1: GetLocalTime-test
Brumley 的论文探索的第一个触发源是 GetLocalTime 。GetLocalTime
的原型 如下:
void WINAPI GetLocalTime(
_Out_ LPSYSTEMTIME lpSystemTime
);
在 Minesweeper 中,用户需要指定内存中的触发输入存储的位置。因此,符号执行引擎可以在执行期间正确地分配符号变量。在GetLocalTime
的例子中,需要指定GetLocalTime
将其结果存储在调用GetLocalTime
时由堆栈值指向的16字节结构中。 幸运的是,我们不必担心这些底层的细节。相反,我们可以在传递给GetLocalTime
的变量上调用S2EMAKECONCOLIC
。下面是 malware-hook
中的实现方法:
// Function hooks
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
S2EMakeConcolic(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 et al 的论文中的例子(图 1.1)来看下是否和预期的一样:
#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-位 的Windows 7 虚拟机)。一旦编译完成(包括虚拟机),我们就能够创建一个新项目:
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 工具的权限。通过下面的命令,创建符号链接即可:
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
:
# ...
# 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
现在准备开始分析!
结果
可以看到,在分析过程中,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
中发现一行包含以下的测试用例(我重新格式化并添加了字段名称以便于阅读):
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
看起来不错,但是从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
,下面开始我们的工作。
WinINet hooks
首先,在 malware-hook.cpp
换掉我们要 hook 的 函数,像下面这样:
static LPCSTR functionsToHook[][2] = {
{ "wininet", "InternetOpenUrlA" },
{ "wininet", "InternetCloseHandle" },
{ NULL, NULL },
};
static PVOID hookFunctions[] = {
InternetOpenUrlAHook,
InternetCloseHandleHook,
};
static HOOK_TRACE_INFO hooks[] = {
{ NULL },
{ NULL },
}
然后写入实际要 hook 的函数:
/// 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 = S2EConcolicChar("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
internetHandles.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 = internetHandles.find(hInternet);
if (it == internetHandles.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);
internetHandles.erase(it);
return TRUE;
}
}
在这里,我们遵循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中看到以下库调用:
Address |
DLL |
Function |
0x4081bc |
wininet |
InternetCloseHandle |
0x4081bf |
wininet |
InternetCloseHandle |
0x409b4e |
msvcrt |
exit |
处于状态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 |
… |
|
这看起来不错:当隐藏开关被触发时,我们已成功探索了WannaCry的行为。 Rousseau的文章概述了WannaCry的执行流程,如果我们遵循状态1的库调用,我们应该看到执行流程是匹配的。
Hook 进程创建
开始写最后一个 hook,如果我们 hook 的进程又产生一个新进程会发生什么呢? 这对于“dropper”类恶意软件来说非常常见,而 WannaCry 确实通过从资源加载可执行文件(tasksche.exe
),将其写入磁盘然后运行它(通过CreateProcessA
)来实现这一点。 当发生这种情况时,我们完全不知道这个新进程正在做什么:在通过我们的hook 注入符号化的数据和用S2E跟踪其行为方面(例如通过LibraryCallMonitor
插件)。
我们可以通过 hook CreateProcessA
并使用 EasyHook API 将 malware-hook
注入此新进程来解决这个问题(失去将符号数据注入新进程的能力), 下面的代码实现了这一点:
// 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-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 那些恶意软件常用来隐藏它们行为的“触发”函数。 虽然这种方法适用于我们的两个研究实例(这些案例被认为是高度人为的),但仍有许多改进途径。 这些途径包括:
- hook 更多的 Windows API。Brumley 和 Moser 描述了本文未涉及到的许多不同的触发源(例如网络数据,注册表键等)。
- 建立更复杂的 hook。 例如 ,我们的
InternetOpenUrlA
过于简化了,它只返回了在堆上分配的虚拟句柄。 如果这个句柄稍后被传递给像 InternetReadFile
之类的函数, 我们也必须 hook 掉它们。 这基本上是大多数符号执行引擎中继承的“环境建模”问题。
- 隐藏我们正在分析的恶意软件的 hook 。一些想法包括将Cuckoo Monitor移植到S2E或在S2E插件中执行所有操作。
- 更广泛地关于恶意软件的研究。这种符号执行是否有助于恶意软件分析?基于触发器的恶意软件有多常见 - 我们可以在Cuckoo Sandbox中进行动态分析吗? Banescu在恶意软件作者使用的 代码混淆反对符号执行攻击的工作中讨论了混淆技术,如果是这样,它们如何影响我们的分析?
希望这篇文章为您提供必要的背景和工具,以便了解其中的一些改进。也许有一天我甚至会找时间亲自看看其中的一些!
原文地址:https://adrianherrera.github.io/post/malware-s2e/
本文由 看雪翻译小组 fyb 波 编译
本文由 看雪翻译小组 银雁冰 校对
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开
发者可享99元/年,续费同价!
最后于 2018-9-13 16:12
被fyb波编辑
,原因: 格式选择错误