EasyHook 中的注入方法。
函数原型
// EasyHook 中的命名比较有意思,Rh 代表的就是Remote Hook,此函数就是远程钩子的一个子过程----注入,前面的宏代表它是导出函数。 EASYHOOK_NT_EXPORT RhInjectLibrary( ULONG InTargetPID, ULONG InWakeUpTID,//如果当前函数是通过RhCreateAndInject 调用过来的,这个指示主线程的ID。 //之后可以通过RhWakeUpProcess 唤醒进程。否则传0. ULONG InInjectionOptions, WCHAR* InLibraryPath_x86, WCHAR* InLibraryPath_x64, PVOID InPassThruBuffer, ULONG InPassThruSize)
详细介绍
函数前面的部分就是一些参数检查以及针对X86 和 X64 的不同而准备的变量的不同初始化。之后检查是否进行跨WOW64 注入,是的话报错并退出。另外,代码需要使用PROCESS_ALL_ACCESS 权限调用OpenProcess 。经过上面的检查之后后面才是真正的的注入过程。要了解注入的过程就必须需要了解REMOTE_INFO 结构体以及其与注入代码在目标进程地址空间的位置关系。
REMOTE_INFO 结构
#define WRAP_ULONG64(Decl)\ union\ {\ ULONG64 UNUSED;\ Decl;\ }\ typedef struct _REMOTE_INFO_ { // will be the same for all processes WRAP_ULONG64(wchar_t* UserLibrary); // fixed 0 WRAP_ULONG64(wchar_t* EasyHookPath); // fixed 8 WRAP_ULONG64(wchar_t* PATH); // fixed 16 WRAP_ULONG64(char* EasyHookEntry); // fixed 24 WRAP_ULONG64(void* RemoteEntryPoint); // fixed 32 WRAP_ULONG64(void* LoadLibraryW); // fixed; 40 WRAP_ULONG64(void* FreeLibrary); // fixed; 48 WRAP_ULONG64(void* GetProcAddress); // fixed; 56 WRAP_ULONG64(void* VirtualFree); // fixed; 64 WRAP_ULONG64(void* VirtualProtect); // fixed; 72 WRAP_ULONG64(void* ExitThread); // fixed; 80 WRAP_ULONG64(void* GetLastError); // fixed; 88 BOOL IsManaged; // 指示是否为托管代码 HANDLE hRemoteSignal; // 用于指示是否注入成功 DWORD HostProcess; DWORD Size; BYTE* UserData; // 自定义参数 DWORD UserDataSize; // 自定义参数的大小 ULONG WakeUpThreadID; }REMOTE_INFO, *LPREMOTE_INFO;
该结构体中前面的值为函数指针,按照8 字节对齐。另外,这些函数指针都是通过GetRemoveFuncAddress 函数得到的。在填充完远程函数地址之后,代码通过GetInjectionSize 函数根据汇编代码中尾部的特征码判断汇编代码的大小,之后一次性在目标进程中申请“代码大小 + REMOTE_INFO 结构大小 + 字符串 ‘HookCompleteInjection’- X86或者’_HookCompleteInjection’-X64 长度+1 + 当前工作目录长度 + 当前模块位置长度 + 目标DLL 路径长度”。
目标进程的内存使用详情
我们可以看到,这段注入代码的原型及实现符合线程函数的规范,而且其参数就是REMOGE_INFO 结构的指针。在目标进程中申请内存并写入数据(包含代码)之后,我们应该想办法执行我们的代码,或者称为远程线程函数,这里有两种方式:
使用远程线程函数的方式注入
参考我之前介绍远程线程注入的文章http://blog.csdn.net/qq_18218335/article/details/75246816
使用线程劫持的方法注入
X64
http://blog.csdn.net/qq_18218335/article/details/75308957
Win32–http://blog.csdn.net/qq_18218335/article/details/75268251
这里我们介绍的方法的思想与之前是相同的,不过EasyHook 中使用的方法更加的稳定,考虑的更加周到。
EasyHook 中线程劫持的实现
函数原型
EASYHOOK_NT_EXPORT RhCreateStealthRemoteThread(
ULONG InTargetPID,
LPTHREAD_START_ROUTINE InRemoteRoutine,
PVOID InRemoteParam,
HANDLE* OutRemoteThread)
使用到的数据结构
typedef struct _STEALTH_CONTEXT_ { union { struct { /*00*/ WRAP_ULONG64(PVOID CreateThread); /*08*/ WRAP_ULONG64(PVOID RemoteThreadStart); /*16*/ WRAP_ULONG64(PVOID RemoteThreadParam); /*24*/ WRAP_ULONG64(PVOID WaitForSingleObject); /*32*/ WRAP_ULONG64(HANDLE hCompletionEvent); /*40*/ WRAP_ULONG64(PVOID CloseHandle); /*48*/ union { WRAP_ULONG64(HANDLE hRemoteThread); WRAP_ULONG64(HANDLE hSyncEvent); }; /*56*/ WRAP_ULONG64(PVOID SetEvent); }; ULONGLONG __Unused__[8]; }; ULONGLONG Rax; ULONGLONG Rcx; ULONGLONG Rdx; ULONGLONG Rbp; ULONGLONG Rsp; ULONGLONG Rsi; ULONGLONG Rdi; ULONGLONG Rbx; ULONGLONG Rip; ULONGLONG RFlags; ULONGLONG R8; ULONGLONG R9; ULONGLONG R10; ULONGLONG R11; ULONGLONG R12; ULONGLONG R13; ULONGLONG R14; ULONGLONG R15; }STEALTH_CONTEXT, *PSTEALTH_CONTEXT;
劫持线程通用的做法,找到目标进程的一个活动的子线程,然后视图暂停其执行,成功后通过GetThreadContext 得到其ThreadContext,然后在目标进程中申请并写入代码,设置RIP或者EIP,然后恢复线程执行。EasyHook 的不同在于以下几点:
<1> 在恢复线程执行之前就保存了ThreadContext 中的寄存器的值,而不是在恢复了线程执行之后在通过代码将寄存器的值保存在栈中,这样做的好处就是靠谱、稳定。在介绍自己实现的线程劫持的实现的时候,我发现,在线程暂停后得到的ThreadContext 的值和之后恢复线程执行后执行第一个指令之前,部分寄存器的值会发生改变,虽然后来的运行结果正确,目标进程也没有发生崩溃的情况,但是我们应该按照EasyHook 的方式来进行ThreadContext 的保存与恢复。
<2> 劫持后执行的代码为创建新线程,在新线程中完成我们的任务,EasyHook 中的注入代码称为StealthRemoteThread 原因就在这里,我们通过目标进程已存在的线程创建新线程以执行注入的过程,以达到隐藏注入行为的目的。
<3> 在主线程中等待注入的完成,并判断注入是否成功,这是我当时没有注意到的一点。
ebx/rbx 是 STEALTH_CONTEXT 结构体指针
StealthStub_ASM_x86
public StealthStub_ASM_x86@0 StealthStub_ASM_x86@0 PROC ; Create thread... push 0 push 0 push dword ptr [ebx + 16] ; save stealth context push dword ptr [ebx + 8] ; RemoteThreadStart push 0 push 0 call dword ptr [ebx + 0] ; CreateThread(0, NULL, RemoteThreadStart, RemoteThreadParam, 0, NULL); ; signal thread creation... push dword ptr [ebx + 48] mov dword ptr [ebx + 48], eax call dword ptr [ebx + 56] ; SetEvent(hSyncEvent); ; wait for completion push -1 push dword ptr [ebx + 32] call dword ptr [ebx + 24] ; WaitForSingleObject(hCompletionEvent, INFINITE) ; close handle push dword ptr [ebx + 32] call dword ptr [ebx + 40] ; CloseHandle(hCompletionEvent); ; close handle push dword ptr [ebx + 48] call dword ptr [ebx + 40] ; CloseHandle(hSyncEvent); ; restore context mov eax, [ebx + 64 + 8 * 0] mov ecx, [ebx + 64 + 8 * 1] mov edx, [ebx + 64 + 8 * 2] mov ebp, [ebx + 64 + 8 * 3] mov esp, [ebx + 64 + 8 * 4] mov esi, [ebx + 64 + 8 * 5] mov edi, [ebx + 64 + 8 * 6] push dword ptr[ebx + 64 + 8 * 9] ; push EFlags push dword ptr[ebx + 64 + 8 * 8] ; save old EIP mov ebx, [ebx + 64 + 8 * 7] add esp, 4 popfd ; continue execution... jmp dword ptr [esp - 8] ; outro signature, to automatically determine code size db 78h db 56h db 34h db 12h StealthStub_ASM_x86@0 ENDP
注释打的已经很清楚了,首先创建注入的线程,即之前介绍的远程注入线程,参数还是原来的参数。然后将线程句柄保存在结构体中,触发事件,之后等待主线程得到新创建线程的句柄之后关闭两个事件。返回原EIP 之前,恢复各类寄存器,之后直接跳转到原EIP 处继续运行。
Injection_ASM_x86
public Injection_ASM_x86@0 Injection_ASM_x86@0 PROC ; no registers to save, because this is the thread main function ; save first param (address of hook injection information) mov esi, dword ptr [esp + 4] ; call LoadLibraryW(Inject->EasyHookPath); push dword ptr [esi + 8] call dword ptr [esi + 40] ; LoadLibraryW@4 mov ebp, eax test eax, eax je HookInject_FAILURE_A ; call GetProcAddress(eax, Inject->EasyHookEntry); push dword ptr [esi + 24] push ebp call dword ptr [esi + 56] ; GetProcAddress@8 test eax, eax je HookInject_FAILURE_B ; call EasyHookEntry(Inject); push esi call eax push eax ; save error code ; call FreeLibrary(ebp) push ebp call dword ptr [esi + 48] ; FreeLibrary@4 test eax, eax je HookInject_FAILURE_C jmp HookInject_EXIT HookInject_FAILURE_A: call dword ptr [esi + 88] ; GetLastError or eax, 40000000h jmp HookInject_FAILURE_E HookInject_FAILURE_B: call dword ptr [esi + 88] ; GetLastError or eax, 10000000h jmp HookInject_FAILURE_E HookInject_FAILURE_C: call dword ptr [esi + 88] ; GetLastError or eax, 30000000h jmp HookInject_FAILURE_E HookInject_FAILURE_E: push eax ; save error value HookInject_EXIT: push 0 push 0 push 0; // shadow space for executable stack part... ; call VirtualProtect(Outro, 4, PAGE_EXECUTE_READWRITE, &OldProtect) lea ebx, dword ptr [esp + 8] ; we'll write to shadow space push ebx push 40h push 12 push ebx call dword ptr [esi + 72] ; VirtualProtect@16 test eax, eax jne HookInject_EXECUTABLE ; failed to make stack executable call dword ptr [esi + 88] ; GetLastError or eax, 20000000h add esp, 16 ret HookInject_EXECUTABLE: ; save outro to executable stack mov dword ptr [esp], 0448BD3FFh ; call ebx [VirtualFree()] mov dword ptr [esp + 4], 05C8B0C24h ; mov eax, [esp + 12] mov dword ptr [esp + 8], 0E3FF1024h ; mov ebx, [esp + 16] ; jmp ebx [exit thread] ; save params for VirtualFree(Inject->RemoteEntryPoint, 0, MEM_RELEASE); mov ebx, [esi + 64] ; VirtualFree() push 08000h push 0 push dword ptr [esi + 16] lea eax, dword ptr [esp + 12] jmp eax ; outro signature, to automatically determine code size db 78h db 56h db 34h db 12h Injection_ASM_x86@0 ENDP
远程线程首先调用LoadLibraryW 函数加载目标DLL,然后调用GetProcAddress函数得到规定必须实现的DLL 的导出函数,得到后调用该函数,并传入用户指定的参数及参数长度。调用完成后,函数调用FreeLibrary 函数释放目标DLL。之后的动作比较厉害了,开辟三个字节的栈区,修改该栈区的保护属性为可读写执行,然后拷贝指令,并执行,该指令功能为:释放注入所需要的内存,然后退出线程。这个代码写的就非常完善了,运行完了就将所申请的内存自己释放了。而且释放内存的代码在栈区,又不用担心在释放内存后执行执行会造成非法访问。棒!!!
X64 注入的代码及注释
public StealthStub_ASM_x64 int 3 StealthStub_ASM_x64 PROC sub rsp, 8 * 4 mov qword ptr[rsp + 40], 0 mov qword ptr[rsp + 32], 0 mov r9, qword ptr [rbx + 16] ; RemoteThreadParam mov r8, qword ptr [rbx + 8] ; RemoteThreadStart mov rdx, 0 mov rcx, 0 call qword ptr[rbx] ; CreateThread cmp rax, 0 ; signal completion mov rcx, qword ptr [rbx + 48] mov qword ptr [rbx + 48], rax call qword ptr [rbx + 56] ; SetEvent(hSyncEvent); ; wait for completion mov rdx, -1 mov rcx, qword ptr [ebx + 32] call qword ptr [ebx + 24] ; WaitForSingleObject(hCompletionEvent, INFINITE) ; close handle mov rcx, qword ptr [rbx + 32] call qword ptr [rbx + 40] ; CloseHandle(hCompletionEvent); ; close handle mov rcx, qword ptr [rbx + 48] call qword ptr [rbx + 40] ; CloseHandle(hSyncEvent); ; restore context mov rax, [rbx + 64 + 8 * 0] mov rcx, [rbx + 64 + 8 * 1] mov rdx, [rbx + 64 + 8 * 2] mov rbp, [rbx + 64 + 8 * 3] mov rsp, [rbx + 64 + 8 * 4] mov rsi, [rbx + 64 + 8 * 5] mov rdi, [rbx + 64 + 8 * 6] mov r8, [rbx + 64 + 8 * 10] mov r9, [rbx + 64 + 8 * 11] mov r10, [rbx + 64 + 8 * 12] mov r11, [rbx + 64 + 8 * 13] mov r12, [rbx + 64 + 8 * 14] mov r13, [rbx + 64 + 8 * 15] mov r14, [rbx + 64 + 8 * 16] mov r15, [rbx + 64 + 8 * 17] push qword ptr[rbx + 64 + 8 * 9] ; push EFlags push qword ptr[rbx + 64 + 8 * 8] ; save old EIP mov rbx, [rbx + 64 + 8 * 7] add rsp, 8 popfq ; continue execution... jmp qword ptr [rsp - 16] ; outro signature, to automatically determine code size db 78h db 56h db 34h db 12h StealthStub_ASM_x64 ENDP public Injection_ASM_x64 Injection_ASM_x64 PROC ; no registers to save, because this is the thread main function mov r14, rcx ; r14 当前存储的为LPREMOTE_INFO sub rsp, 40 ; space for register parameter stack, should be 32 bytes... no idea why it only works with 40 ; call LoadLibraryW(Inject->EasyHookPath); mov rcx, qword ptr [r14 + 8] call qword ptr [r14 + 40] ; LoadLibraryW mov r13, rax test rax, rax je HookInject_FAILURE_A ; call GetProcAddress(hModule, Inject->EntryPoint) mov rdx, qword ptr [r14 + 24] mov rcx, rax call qword ptr [r14 + 56] ; GetProcAddress test rax, rax je HookInject_FAILURE_B ; call EasyHookEntry(Inject); mov rcx, r14 call rax mov r15, rax ; save error code to non-volatile register ; call FreeLibrary(hEasyHookLib) mov rcx, r13 call qword ptr [r14 + 48] ; FreeLibrary test rax, rax je HookInject_FAILURE_C jmp HookInject_EXIT HookInject_FAILURE_A: call qword ptr [r14 + 88] ; GetLastError or rax, 40000000h jmp HookInject_FAILURE_E HookInject_FAILURE_B: call qword ptr [r14 + 88] ; GetLastError or rax, 10000000h jmp HookInject_FAILURE_E HookInject_FAILURE_C: call qword ptr [r14 + 88] ; GetLastError or rax, 30000000h jmp HookInject_FAILURE_E HookInject_FAILURE_E: mov r15, rax ; save error value HookInject_EXIT: ; call VirtualProtect(Outro, 8, PAGE_EXECUTE_READWRITE, &OldProtect) ; 这里的Outro 也就是 &OldProtect,即此线程函数开始时申请的一个局部变量 lea rbx, qword ptr [rsp + 8] ; writes into register parameter stack mov r9, rbx mov r8, 40h mov rdx, 8 mov rcx, rbx call qword ptr [r14 + 72] ; VirtualProtect test rax, rax jne HookInject_EXECUTABLE ; failed to make stack executable call qword ptr [r14 + 88] ; GetLastError or rax, 01000000h mov rcx, rax call qword ptr [r14 + 80] ; ExitThread HookInject_EXECUTABLE: ; save outro to executable stack mov rbx, [r14 + 64] ; VirtualFree() mov rbp, [r14 + 80] ; ExitThread() mov rax, 000D5FFCF8B49D3FFh ; 类似于shellcode,后面会跳转到&OldProtect 处执行这三个指令 ; r15 为 EsayHookEntry 的返回值代码 ; call rbx ; mov rcx, r15 ; call rbp mov qword ptr [rsp + 8], rax ; save params for VirtualFree(Inject->RemoteEntryPoint, 0, MEM_RELEASE); ; 这里直接把RemoteEntryPoint 参数删除了..... mov r8, 8000h mov rdx, 0h mov rcx, qword ptr [r14 + 32] lea rax, qword ptr [rsp + 8] sub rsp, 48 jmp rax ; outro signature, to automatically determine code size db 78h db 56h db 34h db 12h Injection_ASM_x64 ENDP
引用
EasyHook 中部分函数的实现分析----注入