【二进制分析】注入技术与 EasyHook

在研究使用 S2E 分析 wannacry 时,用到了 DLL 注入实现 hook。这里系统复习一下关于 Hook 的知识。

Hook 概述

hook 技术指的是在程序实现正常功能的情况下,实现嵌入代码的功能(一般用于获取程序的目标参数,信息等)。

Hook 的实现方式有很多种。除了 SetWindowsHookEx 这种用于截获系统中消息的 Hook,主要有以下几种 API hook 方式:

调试 Hook

调试 Hook :与调试器的原理非常相似,都是令进程运行时发生异常,通过捕获到异常进而获得对被调试程序的控制权;

内联 Hook(Inline Hook)

直接修改 API 代码。修改 API 函数的前 5 个字节,将其修改为跳转指令,进而跳转到自定义的代码上操作,劫持执行流程。

使用该技术,必须要在控制程序流之后将原有 API 函数的前几个字节修复。

另一种方式是修改 API 函数的前 7 个字节。这种 API 通常有两个特征:

  • 函数以 mov edi, edi 为首指令(该指令是无意义的);

  • 函数最上方有至少 5 字节的空余。

Untitled

这样就可以修改 mov edi, edi 为一个短跳转,在前面 5 字节存放一个长跳转,这样就不会破坏原有的 API 函数代码了。

导入表 Hook(IAT Hook)

和 inline hook 不同,导入表 Hook 只需要去修改 IAT 即可。

注入和 Hook 的关系

https://www.zhihu.com/question/28710950

在安卓上,静态的篡改方式是反编译apk,修改或添加代码后重打包,用户只要安装了这个修改过的apk,运行时攻击者的代码就会被加载到进程空间。
动态的篡改方法就是hook。如果我要篡改代码,那么我要实现的就是在程序将要执行某段逻辑的时候控制它去执行我的代码,这个行为就叫hook。

一个运行时的程序表现形式是进程,代码跟数据都放在自己的进程里面。那么问题来了,操作系统隔离了进程,我的代码在我的进程里,别人的代码在别人的进程里,别人的进程是不能跳到我的进程来执行我的代码的,这怎么办呢,所以要先想办法把代码注入到别人的进程里。之前提到的重打包也算是一种静态的注入方法,动态的注入方法在安卓上与Linux的共享库注入是类似的,这种方法网上用的最多的应该是看雪的古河发布的libinject。另外还有Xposed,它采取了一种特殊的注入方法,是动静结合的。

注入技术

参考链接:

DLL 注入指的是将 DLL 注入到某个其他进程中,令其执行 DLL 中的代码。由于 DLL 已经属于被注入进程的一部分,因此他们共享内存空间和其他进程资源。如果在 DLL 中放置监控代码,则可以监控被注入程序的执行。

根据前面提到的 Hook 技术,我们可以将 hook 相关的代码放到 DLL 中,然后注入到其他进程中。

需要注意的问题是,其他进程并不会主动的加载我们编写的 DLL,因此我们需要别的手段使其加载,这个过程就是“注入”。

EasyHook 使用的是 Inline Hook。参考官方给出的注入代码,可以使用 RhInjectLibrary() 函数进行远线程注入。也可以使用 RhCreateAndInject() 先创建一个挂起的进程, 将 DLL 注入至进程后再使其继续运行。这里我主要探讨后者。

这种技术叫做劫持进程创建注入(和 PROCESS HOLLOWING 有点类似)。来看 CreateProcess 的函数原型:https://blog.csdn.net/flowwaterdog/article/details/116564999

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcess (
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes。
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID IpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

dwCreationFlags 参数可以赋值为 CREATE_SUSPENDED ,此时该进程的主线程将以挂起的方式启动。想要恢复主线程的运行,需要调用 ResumeThread() 函数。因此,劫持进程加载就是在主线程被挂起到恢复的这段时间内实现注入代码的。

例:EasyHook — RhCreateAndInject

这里用 EasyHook 作为例子,探讨一下如何实现这种先挂起,再注入,最后恢复执行的注入方式。如何使用 EasyHook 可以参考官方文档。总的来说,RhCreateAndInject 是远线程注入。

RhCreateAndInject 函数定义如下:(EasyHookDll/RemoteHook/thread.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
EASYHOOK_NT_EXPORT RhCreateAndInject(
WCHAR* InEXEPath,
WCHAR* InCommandLine,
ULONG InProcessCreationFlags,
ULONG InInjectionOptions, // 默认是 EASYHOOK_INJECT_DEFAULT
WCHAR* InLibraryPath_x86,
WCHAR* InLibraryPath_x64,
PVOID InPassThruBuffer, // 一个可选的缓冲区,它会被传递到被注入DLL的入口
ULONG InPassThruSize, // InPassThruBuffer 缓冲区的 size
ULONG* OutProcessId) // 新创建的进程pid
/*
创建一个挂起的进程,并立即注入指定的DLL。 注入操作发生在进程的任何初始化之前。从进行注入开始,到注入完成,进程不会执行任何代码......就像您的DLL入口点是该进程中执行的第一段代码。您可以通过调用 RhWakeUpProcess() 令原始进程继续执行。
*/

该函数主要调用 RtlCreateSuspendedProcess 创建被挂起的进程,然后调用 RhInjectLibrary 注入 DLL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    // all other parameters are validate by called APIs...
FORCE(RtlCreateSuspendedProcess(InEXEPath, InCommandLine, InProcessCreationFlags, &ProcessId, &ThreadId));
// RtlCreateSuspendedProcess 就是朴素地调用了 CreateProcessW ,并设置参数 nCustomFlags | CREATE_SUSPENDED 。这里不再细说。

// inject library
FORCE(RhInjectLibrary(
ProcessId,
ThreadId,
InInjectionOptions,
InLibraryPath_x86,
InLibraryPath_x64,
InPassThruBuffer,
InPassThruSize));

*OutProcessId = ProcessId;

RETURN;

RhInjectLibrary 则是采用了远线程注入的方式实现。其部分源码如下:(这里只选取 32 位的相关代码)

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
EASYHOOK_NT_EXPORT RhInjectLibrary(...)
{
... ...
WCHAR PATH[MAX_PATH + 1];
... ...
CHAR* EasyHookEntry = "_HookCompleteInjection@4";
... ...
/* 1. 获得被注入进程的句柄 */
if((hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, InTargetPID)) == NULL){
// 错误处理 ...
... ...
}
/* 2. 准备好要向目标进程写入的内容 */
if(!GetFullPathNameW(InLibraryPath_x86, MAX_PATH, UserLibrary, NULL))
THROW(STATUS_INVALID_PARAMETER_4, L"Unable to get full path to the given 32-bit library.");
... ...
// import strings...
RtlGetWorkingDirectory(PATH, MAX_PATH - 1);
RtlGetCurrentModulePath(EasyHookPath, MAX_PATH); // 获取 EasyHook32.dll 路径

// allocate remote information block
EasyHookPathSize = (RtlUnicodeLength(EasyHookPath) + 1) * 2;
EasyHookEntrySize = (RtlAnsiLength(EasyHookEntry) + 1);
PATHSize = (RtlUnicodeLength(PATH) + 1 + 1) * 2;
UserLibrarySize = (RtlUnicodeLength(UserLibrary) + 1 + 1) * 2; // 被注入 DLL 的路径
... ...
// 申请一段空间用于存放这些信息
// Info 结构体保存了和目标进程有关的内容
if((Info = (LPREMOTE_INFO)RtlAllocateMemory(TRUE, RemoteInfoSize)) == NULL){
// 错误处理 ...
... ...
}
// 确保如果我们注入了一个挂起的进程,我们可以检索远程函数地址
FORCE(NtForceLdrInitializeThunk(hProc));

// 检索目标进程中的函数地址
Info->LoadLibraryW = (PVOID)GetRemoteFuncAddress(InTargetPID, hProc, "kernel32.dll", "LoadLibraryW");
Info->FreeLibrary = (PVOID)GetRemoteFuncAddress(InTargetPID, hProc, "kernel32.dll", "FreeLibrary");
Info->GetProcAddress = (PVOID)GetRemoteFuncAddress(InTargetPID, hProc, "kernel32.dll", "GetProcAddress");
Info->VirtualFree = (PVOID)GetRemoteFuncAddress(InTargetPID, hProc, "kernel32.dll", "VirtualFree");
Info->VirtualProtect = (PVOID)GetRemoteFuncAddress(InTargetPID, hProc, "kernel32.dll", "VirtualProtect");
Info->ExitThread = (PVOID)GetRemoteFuncAddress(InTargetPID, hProc, "kernel32.dll", "ExitThread");
Info->GetLastError = (PVOID)GetRemoteFuncAddress(InTargetPID, hProc, "kernel32.dll", "GetLastError");

Info->WakeUpThreadID = InWakeUpTID;
Info->IsManaged = InInjectionOptions & EASYHOOK_INJECT_MANAGED;

/* 3. 在目标进程中申请空间,并写入 Info 的内容 */
// allocate memory in target process
CodeSize = GetInjectionSize();

if((RemoteInjectCode = (BYTE*)VirtualAllocEx(hProc, NULL, CodeSize + RemoteInfoSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) == NULL)
THROW(STATUS_NO_MEMORY, L"Unable to allocate memory in target process.");

// save strings
Offset = (BYTE*)(Info + 1);
... ...
// 注入一段汇编语言,Injection_ASM_x86,主要工作是找到 EasyHook32.dll 中的 HookCompleteInjection 函数,它就是 EasyHookEntry,可能是用于在目标进程中初始化 EasyHook 相关的功能。
if(!WriteProcessMemory(hProc, RemoteInjectCode, GetInjectionPtr(), CodeSize, &BytesWritten) || (BytesWritten != CodeSize))
THROW(STATUS_INTERNAL_ERROR, L"Unable to write into target process memory.");
... ...
RemoteInfo = (LPREMOTE_INFO)(RemoteInjectCode + CodeSize);
... ...
// 将 Info 结构体的内容写入目标进程中
if(!WriteProcessMemory(hProc, RemoteInfo, Info, RemoteInfoSize, &BytesWritten) || (BytesWritten != RemoteInfoSize))
THROW(STATUS_INTERNAL_ERROR, L"Unable to write into target process memory.");
... ...

/* 4. 执行远线程注入 */
// 调用 NtCreateThreadEx 或 CreateRemoteThread 来创建远线程。
if(!RTL_SUCCESS(NtCreateThreadEx(hProc, (LPTHREAD_START_ROUTINE)RemoteInjectCode, RemoteInfo, FALSE, &hRemoteThread)))
{
// create remote thread and wait for injection completion
// RemoteInjectCode 就是负责初始化 EasyHook 的代码。猜想它会负责调用 LoadLibrary 加载目标 DLL
if((hRemoteThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)RemoteInjectCode, RemoteInfo, 0, NULL)) == NULL)
THROW(STATUS_ACCESS_DENIED, L"Unable to create remote thread.");
}
/* 5. 等待远线程运行完毕 */
Handles[1] = hSignal;
Handles[0] = hRemoteThread;

Code = WaitForMultipleObjects(2, Handles, FALSE, INFINITE);
... ...
}

总结一下,流程主体过程分为以下几个步骤:

  1. 打开被注入的进程,获得其句柄(OpenProcess );

  2. 准备好要向目标进程写入的内容:

    1. EasyHook 路径,目标 DLL 路径,目标进程的一些函数地址等;

    2. Injection_ASM_x86 的代码,创建远线程时就会执行这段代码。猜想它会调用 LoadLibrary 导入目标 DLL。

  3. 在目标程序申请空间 (VirtualAllocEx,附带 rwx 属性);

  4. 创建远线程,执行 Injection_ASM_x86 函数 (NtCreateThreadEx 或 CreateRemoteThread);

  5. 等待远线程运行完毕 (WaitForMultipleObjects)