catalogue
1. 引言 2. 使用注册表注入DLL 3. 使用Windows挂钩来注入DLL 4. 使用远程线程来注入DLL 5. 使用木马DLL来注入DLL 6. 把DLL作为调试器来注入 7. 使用createprocess来注入代码 8. api拦截 9. Detours - Inline Hook
1. 引言
应用程序需要跨越进程边界来访问另一个进程的地址空间的情况如下
1. 我们想要从另一个进程创建的窗口派生子类窗口 2. 我们需要一些手段来辅助调试,例如我们需要确定另一个进程正在使用哪些DLL 3. 我们想对另一个进程安装Hook
我们接下来要研究将一个DLL注入到另一个进程的地址空间中,一旦DLL代码进入另一个地址空间,那么我们就可以在那个进程中实现安全防御逻辑。这里需要注意是的,DLL注入是一个一个前提条件,因为Windows的内存管理体系是不允许另一个进程直接修改当前进程的API行为的,而WriteProcessMemory又只能修改小范围的跨进程内存地址,这种情况下,我们要实现运行时中跨进程API Hook(有别于Linux LD_PRELOAD那种需要重新启动进程才能生效的全局Glibc库API劫持思路),就必须进行DLL注入
0x1: Detect and Plug GDI Leaks in Your Code with Two Powerful Tools for Windows XP
Relevant Link:
http://wotseb.bokee.com/6568089.html http://blog.naver.com/hypermin/70011196503
2. 使用注册表注入DLL
windows整个系统的配置都保存在这个注册表中,我们可以通过调整其中的设置来改变系统的行为
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
1. AppInit_Dlls: 该键的值可能会包含一个DLL的文件名或一组DLL的文件名(通过空格或逗号分隔)(由于空格是用来分隔文件名的,所以我们必须避免在文件名中包含空格)。第一个DLL的文件名可以包含路径,但其他DLL包含的路径则会被忽略,出于这个原因,我们最好是将自己的DLL放到windows的系统目录中,这样就不必指定路径了 2. LoadAppInit_Dlls: 为了能让系统使用AppInit_Dlls这个注册表项,需要创建一个LoadAppInit_Dlls,类型为DWORD的注册表项,并将它的值设置为1
当User32.dll被映射到一个新的进程时,会收到DLL_PROCESS_ATTACH通知,当User32.dll对它进行处理的时候,会取得上述注册表键的值,并调用LoadLibary来载入这个字符串中指定的每个DLL。当系统载入每个DLL的时候,会调用它们的DllMain函数,并将参数fdwReason的值设置为DLL_PROCESS_ATTACH,这样每个DLL都能够对自己进行初始化
0x1: 该方法的风险点
1. 由于被注入的DLL是在进程的生命周期的早期(Loader)被载入的,因此我们在调用函数的时候应该谨慎,调用Kernel32.dll中的函数应该没有问题,但是调用其他DLL中的函数可能会导致失败,甚至可能会导致蓝屏 2. User32.dll不会检查每个DLL的载入或初始化是否成功
0x2: 该方案的缺点
1. 我们的DLL只会被映射到那些使用了User32.dll的进程中,所有基于GUI的应用程序都使用了User32.dll,但大多数基于CUI的应用程序都不会使用它。因此,如果想要将DLL注入到编译器或者链接器或者命令行程序,这种方法就不可行 2. 我们的DLL会被映射到每个基于GUI的应用程序中,但我们可能只想把DLL注入到一个或少数几个应用程序中。我们的DLL被映射到越多的进程中,它导致"容器"进程崩溃的可能性也就越大 3. 我们注入的DLL会在应用程序终止之前,一直存在于进程的地址空间中,最好是只在需要的时候才注入我们的DLL
3. 使用Windows挂钩来注入DLL
我们可以用挂钩(SetWindowsHookEx)来将一个DLL注入到进程的地址空间中。注意,当系统把挂钩过滤函数(hook filter function)所在的DLL注入或映射到地址空间中时,会映射整个DLL,而不仅仅只是挂钩过滤函数,这意味着该DLL内的所有函数存在于被注入的进程中,能够被被注入进程中的任何线程调用
0x1: 该方案的优缺点
1. 和利用注册表来注入DLL的方法相比,这种方法允许我们在不需要该DLL的时候从进程的地址空间中撤销对它的映射,只需要调用UnhookWindowsHookEx就可以达到目的。当一个线程调用UnhookWindowsHookEx的时候,系统会遍历自己内部的一个已经注入过该DLL的进程列表,并将该DLL的锁计数器递减。当锁计数器减到0的时候,系统会自动从进程的地址空间中撤销对该DLL的映射 2. 系统为了防止内存访问违规,在被注入进程指定Hook函数的时候,会对注入DLL的锁计数器加1,因为如果不这么做,则被注入进程在执行Hook函数的时候,系统的另一个进程可能会调用UnhookWindowsHookEx,从而引起内存访问违规 3. 所有这一切意味着我们不能在调用了Hook函数,且函数还在运行时把挂钩清楚,在Hook函数执行的整个声明周期,这个挂钩必须一直有效
这种方式可以理解为借用了windows自己原生的机制来进行DLL注入
4. 使用远程线程来注入DLL
远程线程(remote thread)提供了最高的灵活性,从根本上来说,DLL注入技术要求目标进程中的一个线程调用LoadLibrary来载入我们自己的DLL。由于我们不能轻易地控制别人进程中的线程,因此这种方法要求我们在目标进程中创建一个新的线程
HANDLE WINAPI CreateRemoteThread( _In_ HANDLE hProcess, _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ SIZE_T dwStackSize, _In_ LPTHREAD_START_ROUTINE lpStartAddress, _In_ LPVOID lpParameter, _In_ DWORD dwCreationFlags, _Out_ LPDWORD lpThreadId ); 1. hProcess: 表示新创建的线程归哪个进程所有 A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without these rights on certain platforms 2. lpStartAddress: 代表新建远程线程的入口函数地址 注意,这个函数地址应该在远程进程的地址空间中,而不是在我们自己进程的地址空间。因为我们只是在远程进程中新建了一个线程,我们自己的DLL这个时候还没有被载入远程进程中,我们这个时候是孤身深入地方阵地的,没有携带任何武器,只能使用地方阵地上已有的东西制造登录平台,来实现后续的DLL注入(即利用LoadLibrary)
这里需要注意的是,如果在调用CreateRemoteThread的时候直接引用LoadLibraryW,该引用会被解析为我们模块的导入段中的LoadLibraryW转换函数的地址。如果把这个转换函数的地址作为远程线程的起始地址传入,其结果很可能是访问违规,为了强制让代码略过转换函数并直接调用LoadLibraryW函数,我们必须通过调用GetProcAddress来得到LoadLibraryW的准确地址
对CreateRemoteThread的调用假定在本地进程(local process)和远程进程中,Kernel32.dll被映射到地址空间中的同一内存地址。每个应用程序都需要Kernel32.dll,且每个进程中都会将Kernel32.dll映射到同一个地址,即使这个地址在系统重启之后可能会改变,因此,我们可以按照如下的方式来调用
PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32.dll")), "LoadLibrary"); HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\Mylib.dll", 0, NULL);
但是这里还有一个问题,还是内存地址空间隔离的问题,我们传入的这个L"C:\\Mylib.dll"在编译时会被翻译为当前本地进程的内存地址,但是对于远程进程来说,这个地址可能是无效的,这可能导致访问违规,进而导致远程进程崩溃。为了解决这个问题,我们需要把DLL的路径字符串存放到远程进程的地址空间去,然后在调用CreateRemoteThread传入
LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect ); 让我们在远程进程中分配一块内存,一旦为字符串分配了一块内存,我们还需要向这个内存块中写入字符串内容 BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten );
这里再一次说明,CreateRemoteThread里传入的所有信息,都必须是在远程进程中有效的地址,这就相当于我们深入敌阵之前已经探查好了地形,当深入敌阵的那一瞬间,我们是按照事先探查好的地形(对应于远程进程中的有效内存地址)来进行后续的行动(即LoadLibraryW)
梳理一下总的流程
1. 用VirtualAllocEx函数在远程进程的地址空间中分配一块内存 2. 用WriteProcessMemory函数把DLL的路径名字符串复制到第一步分配的内存中 3. 用GetProcAddress函数来得到LoadLibraryW函数(在Kernel32.dll)在远程进行中的实际地址 4. 用CreateRemoteThread函数在远程进程中创建一个线程,让新线程调用正确的LoadLibraryW函数并在参数中传入第一步分配的内存地址。这时,DLL已经被注入到远程进程的地址空间,DLL的DllMain函数会收到DLL_PROCESS_ATTACH通知并且可以执行我们自定义的代码逻辑。当DllMain返回的时候,远程线程会从LoadLibraryW调用返回到BaseThreadStart函数。BaseThreadStart然后调用ExitThread,使远程线程终止 5. 现在远程进程中有一块内存,它是我们在第一步分配的,DLL也还在远程进程的内存空间中,为了对它们进行清理,我们需要在远程线程退出之后执行后续步骤 6. 用VirtualFreeEx来释放第一步分配的内存 7. 用GetProcAddress来得到FreeLibrary函数(在Kernel32.dll)中的实际地址 8. 用CreateRemoteThread函数在远程进程中创建一个线程,让该线程调用FreeLibrary函数并在参数中传入远程DLL的HMODULE
0x1: 方案风险点
1. CreateRemoteThread的第一个参数是远程进程的句柄HANDLE,我们需要调用OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessId);,并请求合适的访问权限,方案兼容性可能就出在这个访问权限。如果OpenProcess返回NULL,那说明应用程序所在的安全上下文(security context)不允许它打开目标进程的句柄。一些进程是本地系统帐号(local system account)运行的,例如WinLogon、SvcHost和Csrss,登录的用户是无法对这些进程进行修改的
0x2: python-dll-injection
#!/usr/bin/python # Win32 DLL injector from Grey Hat Python # Minor formatting cleanups done... import sys from ctypes import * print "DLL Injector implementation in Python" print "Taken from Grey Hat Python" if (len(sys.argv) != 3): print "Usage: %s <PID> <Path To DLL>" %(sys.argv[0]) print "Eg: %s 1111 C:\\test\messagebox.dll" %(sys.argv[0]) sys.exit(0) PAGE_READWRITE = 0x04 PROCESS_ALL_ACCESS = ( 0x00F0000 | 0x00100000 | 0xFFF ) VIRTUAL_MEM = ( 0x1000 | 0x2000 ) kernel32 = windll.kernel32 pid = sys.argv[1] dll_path = sys.argv[2] dll_len = len(dll_path) # Get handle to process being injected... h_process = kernel32.OpenProcess( PROCESS_ALL_ACCESS, False, int(pid) ) if not h_process: print "[!] Couldn‘t get handle to PID: %s" %(pid) print "[!] Are you sure %s is a valid PID?" %(pid) sys.exit(0) # Allocate space for DLL path arg_address = kernel32.VirtualAllocEx(h_process, 0, dll_len, VIRTUAL_MEM, PAGE_READWRITE) # Write DLL path to allocated space written = c_int(0) kernel32.WriteProcessMemory(h_process, arg_address, dll_path, dll_len, byref(written)) # Resolve LoadLibraryA Address h_kernel32 = kernel32.GetModuleHandleA("kernel32.dll") h_loadlib = kernel32.GetProcAddress(h_kernel32, "LoadLibraryA") # Now we createRemoteThread with entrypoiny set to LoadLibraryA and pointer to DLL path as param thread_id = c_ulong(0) if not kernel32.CreateRemoteThread(h_process, None, 0, h_loadlib, arg_address, 0, byref(thread_id)): print "[!] Failed to inject DLL, exit..." sys.exit(0) print "[+] Remote Thread with ID 0x%08x created." %(thread_id.value)
需要明白的是,CreateRemoteThread+DLL注入只是让我们有机会定向地让一个目标远程执行我们自定义的代码逻辑。到了这一步还未完成API Hook,因为进程注入只有One Shoot一次机会,如果我们希望持久地控制目标进程的行为,就需要在注入的DLL的DllMain中实现API Hook的代码逻辑
Relevant Link:
https://github.com/infodox/python-dll-injection
5. 使用木马DLL来注入DLL
注入DLL的另一种方式是,把我们知道的进程必然会载入的一个DLL替换掉,我们替换的DLL内部,我们导出原来的DLL所导出的所有符号(保证调用方无感知)
0x1: 方案的风险点
1. 这种方法不能自动适应被替换DLL版本的变化
6. 把DLL作为调试器来注入
调试器可以在被调试进程中执行许多特殊的操作。系统载入一个被调试程序(debugger)的时候,会在被调试程序的地址空间准备完毕之后,但被调试程序的主线程尚未开始执行任何代码之前,自动通知调试器。这是,调试器可以强制将一些代码注入到被调试程序的地址空间中(例如使用writeprocessmemory),然后让被调试程序的主线程去执行这些代码。
这种方法要求我们对被调试线程的context结果进行操作,这也意味着我们必须编写与cpu有关的代码。在默认情况下,如果调试器终止,那么windows会自动终止被调试程序,但是,调试器可以通过debugsetprocesskillonexit并传入false来改变默认的行为
7. 使用createprocess来注入代码
如果要注入代码的进程是由我们的进程生成的(spawn),那么,我们的进程(父进程)可以在创建新进程的时候将它挂起。这种方法允许我们改变子进程的状态,同时又不影响它的执行,因为它根本还没有开始执行。
父进程会得到子进程主线程的句柄,通过这个句柄,可以对线程执行的代码进行修改,由于我们可以设置线程的指令指针,让它执行内存映射文件中的代码
1. 让进程生成一个被挂起的子进程 2. 从.exe模块的文件头中取得主线程的起始内存地址 3. 将位于该内存地址处的机器指令保存起来 4. 强制将一些手工编写的机器指令写入到该内存地址处,这些指令应该调用loadlibrary来载入一个dll 5. 让子进程的主线程恢复运行,从而让这些指令得到执行 6. 把保存起来的原始指令恢复到起始地址处 7. 让进程从起始地址继续执行,就好像什么都没有发生过一样
0x1: 方案优缺点
1. 首先,它在应用程序开始执行之前得到地址空间 2. 其次,由于我们的应用程序不是调试器,因此我们可以非常容易地对应用程序和注入的dll进行调试 3. 最后,这种方法适用于cui、gui程序 4. 但是这种方法也有缺点。只有当我们的代码在父进程中的时候,我们才能用这种方法来注入dll 5. 同时,这种方法还和cpu相关,我们必须为不同的cpu平台做相应的修改
8. api拦截
0x1: 通过覆盖代码来拦截api(inline hook)
1. 在内存中对要拦截的函数(假设是Kernel32.dll中的ExitProcess)进行定位,从而得到它的的内存地址 2. 把这个函数起始的几个字节保存在我们自己的内存中 3. 用CPU的一条JUMP指令来覆盖这个函数起始的几个字节,这条JUMP指令用来跳转到我们的替代函数的内存地址。当然,我们的替代函数的函数签名(参数)必须与要拦截的函数的函数签名完全相同;1)所有的参数必须相同、2)返回值必须相同、3)调用约定也必须相同 4. 现在,当线程调用被拦截函数(hook function)的时候,跳转指令实际上会跳转到我们的替代函数。这时,我们就可以执行自己想要执行的任何代码 5. 为了撤销对函数的拦截,我们必须把(第二步)保存下来的自己放回被拦截函数起始的几个字节中 6. 我们调用被拦截函数(现在已经不再对它进行拦截了),让该函数执行它的正常处理
需要注意的是,这种方法存在一些严重不足
1. 它对CPU有依赖性;x86、x64、IA-64以及其他CPU的JUMP指令各不相同,为了让这种方法能够工作,我们必须手工编写机器指令 2. 这种方法在抢占式、多线程环境下无法工作。一个线程覆盖另一个函数起始位置的代码是需要时间的,在这个过程中,另一个线程可能试图调用同一个函数,其结果可能是灾难性的
0x2: 通过修改模块的导入段来拦截API
我们知道,一个模块的导入段包含一组DLL,为了让模块能够运行,这些DLL是必须的。此外,导入段还包含一个符号表,其中列出了该模块从各DLL中导入的符号。当该模块调用另一个导入函数的时候,线程实际上会先从模块的导入表中得到相应的导入函数的地址,然后再跳转到那个地址
因此,为了拦截一个特定的函数,我们需要修改它在模块的导入段中的地址(定向针对某进程Hook)
需要注意的是,通过修改模块的导入段只能影响该模块本身(常常是该主进程)的调用行为,而不影响其他进程,同时,如果该模块地址空间中的DLL也不受影响,因为这些DLL有它们自己的导入段,它们并没有被修改。如果想要捕获所有模块对执行函数的所有调用,必须对载入到地址空间中的每个模块都进行导入段修改
1. ReplaceIATEntryInAllMods中遍历模块的框架
void CAPIHOOK::ReplaceIATEntryInAllMods(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, BOOL bExcludeAPIHookMod) { //取得当前模块句柄 HMODULE hModThis = NULL; if (bExcludeAPIHookMod) { MEMORY_BASIC_INFORMATION mbi; if (0 != ::VirtualQuery(ReplaceIATEntryInAllMods, &mbi, sizeof(MEMORY_BASIC_INFORMATION))) //ReplaceIATEntryInAllMods必须为类的static函数 { hModThis = (HMODULE)mbi.AllocationBase; } } //取得本进程的模块列表 HANDLE hModuleSnap = INVALID_HANDLE_VALUE; MODULEENTRY32 me32; hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); if (INVALID_HANDLE_VALUE == hModuleSnap) { return; } me32.dwSize = sizeof( MODULEENTRY32 ); if( !Module32First( hModuleSnap, &me32 ) ) { return; } do { //对每一个模块 if (me32.hModule != hModThis) { ReplaceIATEntryInOneMod(pszExportMod, pfnCurrent, pfnNewFunc, me32.hModule); } } while( Module32Next( hModuleSnap, &me32 ) ); ::CloseHandle(hModuleSnap); //配对写 }
2. 遍历链表摘除自己(恢复被Hook导入函数)的框架
CAPIHOOK::~CAPIHOOK(void) { //取消对函数的HOOK ReplaceIATEntryInAllMods(m_pszModName, m_pfnHook, m_pfnOrig, TRUE); //把自己从链表中删除 CAPIHOOK* p = sm_pHeader; if (p == this) { sm_pHeader = this->m_pNext; } else { while(p != NULL) { if (p->m_pNext == this) { p->m_pNext = this->m_pNext; break; } p = p->m_pNext; } } }
3. ReplaceIATEntryInOneMod
使用IAT Hook劫持技术,需要额外处理几个特殊的情况
1. 如果一个线程在我们调用了ReplaceIATEntryInAllMods之后调用LoadLibrary来动态载入一个新的DLL,这种情况下,新载入的DLL并没有被IAT替换。因此我们需要拦截LoadLibraryA、LoadLibraryW、LoadLibraryExA、LoadLibraryExW函数,这样我们就能够捕获这些调用,并为新载入的模块调用ReplaceIATEntryInAllMod。之所以要用All,是因为新载入的DLL可能有静态依赖其他DLL,这些静态依赖的DLL不会触发我们的LoadLibrary..系列函数 2. 假如目标模块是哟个GetProcAddress动态调用函数,程序流也不会到IAT这里,因此我们需要对GetProcAddress进行单独的Hook处理
.cpp
#include "APIHOOK.h" #include <Tlhelp32.h> CAPIHOOK *CAPIHOOK::sm_pHeader = NULL; CAPIHOOK CAPIHOOK::sm_LoadLibraryA("kernel32.dll", "LoadLibraryA", (PROC)CAPIHOOK::LoadLibraryA, TRUE); CAPIHOOK CAPIHOOK::sm_LoadLibraryW("kernel32.dll", "LoadLibraryW", (PROC)CAPIHOOK::LoadLibraryW, TRUE); CAPIHOOK CAPIHOOK::sm_LoadLibraryExA("kernel32.dll", "LoadLibraryExA", (PROC)CAPIHOOK::LoadLibraryExA, TRUE); CAPIHOOK CAPIHOOK::sm_LoadLibraryExW("kernel32.dll", "LoadLibraryExW", (PROC)CAPIHOOK::LoadLibraryExW, TRUE); CAPIHOOK CAPIHOOK::sm_GetProcAddress("kernel32.dll", "GetProcAddress", (PROC)CAPIHOOK::GetProcess, TRUE); CAPIHOOK::CAPIHOOK(LPTSTR lpszModName, LPSTR pszFuncName, PROC pfnHook, BOOL bExcludeAPIHookMod) { //初始化变量 m_pszModName = lpszModName; m_pszFuncName = pszFuncName; m_pfnOrig = ::GetProcAddress(::GetModuleHandleA(lpszModName), pszFuncName); m_pfnHook = pfnHook; //将此对象加入链表中 m_pNext = sm_pHeader; sm_pHeader = this; //在当前已加载的模块中HOOK这个函数 ReplaceIATEntryInAllMods(lpszModName, m_pfnOrig, m_pfnHook, bExcludeAPIHookMod); } CAPIHOOK::~CAPIHOOK(void) { //取消对函数的HOOK ReplaceIATEntryInAllMods(m_pszModName, m_pfnHook, m_pfnOrig, TRUE); //把自己从链表中删除 CAPIHOOK* p = sm_pHeader; if (p == this) { sm_pHeader = this->m_pNext; } else { while(p != NULL) { if (p->m_pNext == this) { p->m_pNext = this->m_pNext; break; } p = p->m_pNext; } } } //防止程序运行期间动态加载模块 void CAPIHOOK::HookNewlyLoadedModule(HMODULE hModule, DWORD dwFlags) { if (hModule!=NULL && (dwFlags&LOAD_LIBRARY_AS_DATAFILE)==0) { CAPIHOOK* p = sm_pHeader; //循环遍历链表,对每个CAPIHOOK进入HOOK if (p != NULL) { ReplaceIATEntryInOneMod(p->m_pszModName, p->m_pfnOrig, p->m_pfnHook, hModule); p = p->m_pNext; } } } //防止程序运行期间动态调用API函数 FARPROC WINAPI CAPIHOOK::GetProcess(HMODULE hModule, PCSTR pszProcName) { //得到函数的真实地址 FARPROC pfn = ::GetProcAddress(hModule, pszProcName); //遍历列表 看是不是要HOOK的函数 CAPIHOOK* p = sm_pHeader; while(p != NULL) { if (p->m_pfnOrig == pfn) //是要HOOK的函数 { pfn = p->m_pfnHook; //HOOK掉 break; } p = p->m_pNext; } return pfn; } void CAPIHOOK::ReplaceIATEntryInOneMod(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, HMODULE hModCaller) { IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModCaller; IMAGE_OPTIONAL_HEADER* pOpNtHeader = (IMAGE_OPTIONAL_HEADER*)((BYTE*)hModCaller + pDosHeader->e_lfanew + 24); //这里加24 IMAGE_IMPORT_DESCRIPTOR* pImportDesc = (IMAGE_IMPORT_DESCRIPTOR*)((BYTE*)hModCaller + pOpNtHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); BOOL bFindDll = FALSE; while (pImportDesc->FirstThunk) { char* pszDllName = (char*)((BYTE*)hModCaller + pImportDesc->Name); if (stricmp(pszDllName, pszExportMod) == 0)//如果找到pszExportMod模块,相当于hook messageboxa时的“user32.dll” { bFindDll = TRUE; break; } pImportDesc++; } if (bFindDll) { DWORD n = 0; //一个IMAGE_THUNK_DATA就是一个导入函数 IMAGE_THUNK_DATA* pThunk = (IMAGE_THUNK_DATA*)((BYTE*)hModCaller + pImportDesc->OriginalFirstThunk); while (pThunk->u1.Function) { //取得函数名称 char* pszFuncName = (char*)((BYTE*)hModCaller+pThunk->u1.AddressOfData+2); //函数名前面有两个.. //printf("function name:%-25s, ", pszFuncName); //取得函数地址 PDWORD lpAddr = (DWORD*)((BYTE*)hModCaller + pImportDesc->FirstThunk) + n; //从第一个函数的地址,以后每次+4字节 //printf("addrss:%X\n", lpAddr); //在这里是比较的函数地址 if (*lpAddr == (DWORD)pfnCurrent) //找到iat中的函数地址 { DWORD* lpNewProc = (DWORD*)pfnNewFunc; MEMORY_BASIC_INFORMATION mbi; DWORD dwOldProtect; //修改内存页的保护属性 ::VirtualQuery(lpAddr, &mbi, sizeof(MEMORY_BASIC_INFORMATION)); ::VirtualProtect(lpAddr, sizeof(DWORD), PAGE_READWRITE, &dwOldProtect); ::WriteProcessMemory(GetCurrentProcess(), lpAddr, &lpNewProc, sizeof(DWORD), NULL); ::VirtualProtect(lpAddr, sizeof(DWORD), dwOldProtect, NULL); return; } n++; //每次增加一个DWORD } } } void CAPIHOOK::ReplaceIATEntryInAllMods(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, BOOL bExcludeAPIHookMod) { //取得当前模块句柄 HMODULE hModThis = NULL; if (bExcludeAPIHookMod) { MEMORY_BASIC_INFORMATION mbi; if (0 != ::VirtualQuery(ReplaceIATEntryInAllMods, &mbi, sizeof(MEMORY_BASIC_INFORMATION))) //ReplaceIATEntryInAllMods必须为类的static函数 { hModThis = (HMODULE)mbi.AllocationBase; } } //取得本进程的模块列表 HANDLE hModuleSnap = INVALID_HANDLE_VALUE; MODULEENTRY32 me32; hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); if (INVALID_HANDLE_VALUE == hModuleSnap) { return; } me32.dwSize = sizeof( MODULEENTRY32 ); if( !Module32First( hModuleSnap, &me32 ) ) { return; } do { //对每一个模块 if (me32.hModule != hModThis) { ReplaceIATEntryInOneMod(pszExportMod, pfnCurrent, pfnNewFunc, me32.hModule); } } while( Module32Next( hModuleSnap, &me32 ) ); ::CloseHandle(hModuleSnap); //配对写 } //防止自动加载 HMODULE WINAPI CAPIHOOK::LoadLibraryA(LPCTSTR lpFileName) { HMODULE hModule = LoadLibraryA(lpFileName); HookNewlyLoadedModule(hModule, 0); //这个函数中忆检测hModule 了 return hModule; } HMODULE WINAPI CAPIHOOK::LoadLibraryW(LPCTSTR lpFileName) { HMODULE hModule = LoadLibraryW(lpFileName); HookNewlyLoadedModule(hModule, 0); //这个函数中忆检测hModule 了 return hModule; } HMODULE WINAPI CAPIHOOK::LoadLibraryExA(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags) { HMODULE hModule = LoadLibraryExA(lpFileName, hFile, dwFlags); HookNewlyLoadedModule(hModule, dwFlags); //这个函数中忆检测hModule 了 return hModule; } HMODULE WINAPI CAPIHOOK::LoadLibraryExW(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags) { HMODULE hModule = LoadLibraryExW(lpFileName, hFile, dwFlags); HookNewlyLoadedModule(hModule, dwFlags); //这个函数中忆检测hModule 了 return hModule; }
.h
#pragma once #include <Windows.h> class CAPIHOOK { public: CAPIHOOK(LPTSTR lpszModName, LPSTR pszFuncName, PROC pfnHook, BOOL bExcludeAPIHookMod = TRUE); ~CAPIHOOK(void); private: static void ReplaceIATEntryInOneMod(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, HMODULE hModCaller); static void ReplaceIATEntryInAllMods(LPCTSTR pszExportMod, PROC pfnCurrent, PROC pfnNewFunc, BOOL bExcludeAPIHookMod); //防止程序运行期间动态加载模块, 当一个新DLL被加载时调用 static void HookNewlyLoadedModule(HMODULE hModule, DWORD dwFlags); //跟踪当前进程加载新的DLL static HMODULE WINAPI LoadLibraryA(LPCTSTR lpFileName); static HMODULE WINAPI LoadLibraryW(LPCTSTR lpFileName); static HMODULE WINAPI LoadLibraryExA(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags); static HMODULE WINAPI LoadLibraryExW(LPCTSTR lpFileName, HANDLE hFile, DWORD dwFlags); //防止程序运行期间动态调用API函数 对于请求已HOOK的API函数,返回用户自定义的函数地址 static FARPROC WINAPI GetProcess(HMODULE hModule, PCSTR pszProcName); private: //定义成静态的,会自动调用,从而实现自动HOOK static CAPIHOOK sm_LoadLibraryA; static CAPIHOOK sm_LoadLibraryW; static CAPIHOOK sm_LoadLibraryExA; static CAPIHOOK sm_LoadLibraryExW; static CAPIHOOK sm_GetProcAddress; private: static CAPIHOOK* sm_pHeader; //钩子链表 CAPIHOOK* m_pNext; //要钩子的函数 PROC m_pfnOrig; PROC m_pfnHook; //要钩子的函数所在的dll LPSTR m_pszModName; //要钩子的函数名称 LPSTR m_pszFuncName; };
9. Detours - Inline Hook
Detours是一个在x86平台上截获任意Win32函数调用的工具库。中断代码可以在运行时动态加载。Detours使用一个无条件转移指令来替换目 标函数的最初几条指令,将控制流转移到一个用户提供的截获函数。而目标函数中的一些指令被保存在一个被称为“trampoline” (译注:英文意为蹦 床,杂技)的函数中,在这里我觉得翻译成目标函数的部分克隆/拷贝比较贴切。这些指令包括目标函数中被替换的代码以及一个重新跳转到目标函数的无条件分 支。而截获函数可以替换目标函数,或者通过执行“trampoline”函数的时候将目标函数作为子程序来调用的办法来扩展功能。
Detours是执行时被插入的。内存中的目标函数的代码不是在硬盘上被修改的,因而可以在一个很好的粒度上使得截获二进制函数的执行变得更容易。例如, 一个应用程序执行时加载的DLL中的函数过程可以被插入一段截获代码(detoured),与此同时,这个DLL还可以被其他应用程序按正常情况执行(译 注:也就是按照不被截获的方式执行,因为DLL二进制文件没有被修改,所以发生截获时不会影响其他进程空间加载这个DLL)。不同于DLL的重新链接或者 静态重定向,Detours库中使用的这种中断技术确保不会影响到应用程序中的方法或者系统代码对目标函数的定位。
如果其他人为了调试或者在内部使用其他系统检测手段而试图修改二进制代码,Detours将是一个可以普遍使用的开发包。据我所知,Detours是第一 个可以在任意平台上将未修改的目标代码作为一个可以通过“trampoline”调用的子程序来保留的开发包。而以前的系统在逻辑上预先将截获代码放到目 标代码中,而不是将原始的目标代码做为一个普通的子程序来调用。我们独特的“trampoline”设计对于扩展现有的软件的二进制代码是至关重要的。
出于使用基本的函数截获功能的目的,Detours同样提供了编辑任何DLL导入表的功能,达到向存在的二进制代码中添加任意数据节表的目的,向一个新进 程或者一个已经运行着的进程中注入一个DLL。一旦向一个进程注入了DLL,这个动态库就可以截获任何Win32函数,不论它是在应用程序中或者在系统库中
0x1: 基本原理
1. WIN32进程的内存管理
1. WINDOWS NT实现了虚拟存储器,每一WIN32进程拥有4GB的虚存空间 2. 进程要执行的指令也放在虚存空间中 3. 可以使用QueryProtectEx函数把存放指令的页面的权限更改为可读可写可执行,再改写其内容,从而修改正在运行的程序 4. 可以使用VirtualAllocEx从一个进程为另一正运行的进程分配虚存,再使用 QueryProtectEx函数把页面的权限更改为可读可写可执行,并把要执行的指令以二进制机器码的形式写入,从而为一个正在运行的进程注入任意的代码(此时的代码只是写入了,还未触发执行)
2. 拦截WIN32 API的原理
Detours定义了三个概念
1. Target函数:要拦截的函数,通常为Windows的API 2. Trampoline函数:Target函数的部分复制品。因为Detours将会改写Target函数,所以先把Target函数的前5个字节复制保存好,一方面仍然保存Target函数的过程调用语义,另一方面便于以后的恢复。 3. Detour 函数:用来替代Target函数的函数。 Detours在Target函数的开头加入JMP Address_of_ Detour_ Function指令(共5个字节)把对Target函数 的调用引导到自己的Detour函数, 把Target函数的开头的5个字节加上JMP Address_of_ Target _ Function+ 5共10个字节作为Trampoline函数
Detour函数的调用过程
1. 目标函数: 目标函数的函数体(二进制)至少有5个字节以上。按照微软的说明文档Trampoline函数的函数体是拷贝前5个字节加一个无条件跳转指令的话(如果没 有特殊处理不可分割指令的话),那么前5个字节必须是完整指令,也就是不能第5个字节和第6个字节是一条不可分割的指令,否则会造成Trampoline 函数执行错误,一条完整的指令被硬性分割开来,造成程序崩溃。对于第5字节和第6个字节是不可分割指令需要调整拷贝到杂技函数(Trampoline)的 字节个数,这个值可以查看目标函数的汇编代码得到。此函数是目标函数的修改版本,不能在Detour函数中直接调用,需要通过对Trampoline函数 的调用来达到间接调用 2. Trampoline函数: 此函数默认分配了32个字节,函数的内容就是拷贝的目标函数的前5个字节,加上一个JMP Address_of_ Target _ Function+5指令,共10个字节。 此函数仅供您的Detour函数调用,执行完前5个字节的指令后再绝对跳转到目标函数的第6个字节继续执行原功能函数 3. Detour函数: 此函数是用户需要的截获API的一个模拟版本,调用方式,参数个数必须和目标函数相一致。如目标函数是__stdcall,则Detour函数声明也必须 是__stdcall,参数个数和类型也必须相同,否则会造成程序崩溃。此函数在程序调用目标函数的第一条指令的时候就会被调用(无条件跳转过来的)。 如果在此函数中想继续调用目标函数,必须调用Trampoline函数(Trampoline函数在执行完目标函数的前5个字节的指令后会无条件跳转到目标 函数的5个字节后继续执行),不能再直接调用目标函数,否则将进入无穷递归(目标函数跳转到Detour函数,Detour函数又跳转到目标函数的递归, 因为目标函数在内存中的前5个字节已经被修改成绝对跳转)(无条件跳转)。通过对Trampoline函数的调用后可以获取目标函数的执行结果,此特性对分析目标函数非常有用,而且可以将目标函数的输出结果进行修改后再传回给应用程序 基于Detour封装的Hook框架,省去了我们处理call old function的麻烦
Detour提供了向运行中的应用程序注入Detour函数和在二进制文件基础上注入Detour函数两种方式。我们接下来主要讨论第二种工作方式。通过 Detours提供的开发包可以在二进制EXE文件中添加一个名称为Detour的节表,如下图所示,主要目的是实现PE加载器加载应用程序的时候会自 动加载您编写的Detours DLL,在Detours Dll中的DLLMain中完成对目标函数的Detour(最终目的还是Inline Hook)
Detour提供了向运行中的应用程序注入Detour函数和在二进制文件基础上注入Detour函数两种方式。我们接下来主要讨论第二种工作方式。通过 Detours提供的开发包可以在二进制EXE文件中添加一个名称为Detour的节表,如下图所示,主要目的是实现PE加载器加载应用程序的时候会自 动加载您编写的Detours DLL,在Detours Dll中的DLLMain中完成对目标函数的Detour(最终目的还是Inline Hook)
0x2: Detours提供的截获API的相关接口
Detours的提供的API 接口可以作为一个共享DLL给外部程序调用,也可以作为一个静态Lib链接到您的程序内部。
Trampoline函数可以动态或者静态的创建,如果目标函数本身是一个链接符号,使用静态的trampoline函数将非常简单。如果目标函数不能在链接时可见,那么可以使用动态trampoline函数
Relevant Link:
http://www.cnblogs.com/flying_bat/archive/2008/04/18/1159996.html http://blog.csdn.net/zhoujiaxq/article/details/18656951 https://www.microsoft.com/en-us/research/project/detours/
Copyright (c) 2017 LittleHann All rights reserved