首页
论坛
专栏
课程
1

[翻译] 使用S2E分析基于触发器的恶意软件

fyb波 2018-9-13 16:06 463

使用S2E分析基于触发器的恶意软件

介绍

这篇博客是上个月我在 瑞士联邦理工学院可信系统实验室做的一项工作,当时我正在用S2E分析恶意软件。尽管没有特别出彩的地方,但我希望这篇文章可以帮到那些想要用符号执行/S2E来分析恶意软件行为的人。

恶意软件分析不同之处在哪?

我之前发了2篇文章,一篇是关于 CTF challenge 的,另一篇是分析 文件解析器的。这些程序有两个共通之处:

  1. 它们都是Linux ELF 可执行文件;
  2. 程序输入由用户指定 — 通过 STDIN 标准输入或者从磁盘上的文件读入。

相对来说,大多数软件:

  1. 针对Windows 平台(尽管有些 报告 显示Android 恶意软件的数量正在上升,但恶意软件作者的主要目标还是Windows;
  2. 没有一个良好定义的输入源。输入可能来源于命令行参数,但是不太常见。更可能的一些输入来源是注册表键值、网络数据等等。

由于这些原因,在S2E中分析恶意软件不太容易将命令行参数符号化,或者向程序输入符号文件。这篇文章会探索我们基于S2E开发的恶意软件分析工具,后面会有两个”研究实例“。像往常一样,如果你希望自己研究一下的话,可以在 Github上找到所有的代码。

在S2E中分析 Windows 软件

直到现在,我们只用 S2E 分析过 Linux 程序。幸运的是,S2E也支持 Windows 程序的分析,所以两者的区别在哪呢?

  1. 使用 image_build 命令编译Windows客户镜像的时候,需要提前准备好镜像,S2E 支持的所有版本的Windows 镜像 (列在 这里) 都可以从 MSDN上下到。如果需要的话,可以添加对其他版本的支持。在这篇文章中,我们选用的是 Windows 7 专业版 32-位。
  2. 在 Windows 上,没有s2e.so 的 dll 版本。因此,我们需要换种方式来将符号化的数据注入到恶意软件中。我们可以写个 S2E 插件来实现,但有点复杂。相反,我们 通过 hook Windows API 来注入DLL,并且插入符号化的数据。

Hook Windows API

有许多不同的技术可以实现 hook Windows API,我们会使用现有的办法而非再去发明一种新的。当我刚开始这项工作时,我想重用Cuckoo SandboxMonitor 来 实现对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-hookwannacry-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 的论文探索的第一个触发源是 GetLocalTimeGetLocalTime 的原型 如下:

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-infoKLEE 参数的话 (在 s2e-config.lua 中), 我们可以看到在这四个分叉点中生成的约束。下面的图片是这些点高亮的反汇编代码:

 

GetLocalTime-test fork points

 

ReadLSB w16 X SystemTime可以理解为 在符号变量SystemTime的偏移X处 “读取 16 位 (i.e. 一个字) 。 如果我们在MSDN上查看SYSTEMTIME的结构,可以看到这些偏移处的每个字(0x60x80x20xA)分别对应于wDaywHourwMonthwMinute字段 - 正如预期的那样。最后,可以在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)。调用以下函数来执行此操作:

  • InternetOpenA: 初始化 WinINet 系统。如果成功的话,返回 HINTERNET 句柄,失败的话返回 NULL
  • InternetOpenUrlA: 使用 InternetOpenA返回的句柄打开给定URL指定的资源。如果成功的话,返回 HINTERNET 句柄,失败的话返回 NULL
  • InternetCloseHandle:关闭 InternetOpenAInternetOpenUrlA 打开的句柄。

最小限制下,我们需要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.dllmalware-hook.dllmalware-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-hookNativeInjectionEntryPoint 函数就负责唤醒进程。

 

这解决了将符号数据注入 WannaCry 产生的新进程的问题,但如何在S2E中跟踪这个新进程的行为呢?不幸的是,这需要更多的工作。一种方法是编写一个S2E插件,用于监听OSMonitoronProcessLoad信号。如果发现新进程来自 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),最终会看到下面的效果:

 

WannaCry infection

结论和后续步骤

在这篇文章中,我们研究了使用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 波 编译

 

本文由 看雪翻译小组 银雁冰 校对



快讯:[看雪招聘]十八年来,看雪平台输出了大量安全人才,影响三代安全人才!

最后于 2018-9-13 16:12 被fyb波编辑 ,原因: 格式选择错误
本主题帖已收到 1 次赞赏,累计¥1.00
最新回复 (2)
fyb波 2018-9-13 16:14
2
另外,作者去年写的那篇关于文件解析器的 译文链接在这 https://bbs.pediy.com/thread-222349.htm
最后于 2018-9-13 16:15 被fyb波编辑 ,原因:
开花的水管 2018-9-13 17:50
3
感谢分享
返回