Loading... # Windows核心编程-学习笔记12 ## DLL注入 在Windows中,每个进程都有自己私有的地址空间。用指针来引用内存的时候,指针的值表示的是进程自己地址空间的一个内存地址。进程不能创建一个指针来引用属于其他进程的内存。 但有些情况需要应用程序跨越进程边界来访问另一个进程的地址空间: * 想要从另一个进程创建的窗口派生出子类窗口 * 需要辅助调试 * 想要为另一个进程安装挂钩 ### 注册表注入DLL Windows注册表中默认提供了AppInit_DLLs与LoadAppInit_DLLs两个注册表项:将要注入的DLL路径字符串写入AppInit_DLLs(空格或逗号分隔文件名),然后LoadAppInit_DLLs值设为1 这个方法缺点是自定义的DLL会映射到所有使用了User32.dll的进程中,但CUI程序不会使用;同时映射到的进程很多,可能会导致bug ### Windows挂钩注入DLL 一个例子,进程A为了查看系统中各窗口处理了哪些消息,安装了WH_GETMESSAGE挂钩,通过调用SetWindowsHookEx安装 ~~~c++ HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstDll, 0); ~~~ * WH_GETMESSAGE是安装挂钩的类型 * GetMsgProc是DLL里的一个函数的地址,在窗口即将处理一条消息时,系统调用这个函数 * hInstDll标识了一个含有GetMsgProc函数的DLL * 最后一个0表示要为哪个线程安装挂钩。一个线程可能调用SetWindowsHookEx并传入系统中另一个线程的线程标识符。通过为这个参数传递0,告诉系统为所有系统中的GUI线程安装挂钩 接着来看会发生什么: 1. 进程B中的一个线程准备向一个窗口发送一条消息 2. 系统检查该线程是否已安装了WH_GETMESSAGE挂钩 3. 系统检查GetMsgProc所在的DLL是否已被映射到进程B的地址空间 4. 如果DLL尚未映射,系统会强制将该DLL映射到进程B的地址空间,并将进程B中该DLL的锁计数器递增 5. 由于DLL的hInstDll是在进程B中映射的,因此系统会对它进行检查,看它与该DLL在进程A中的位置是否相同。如果hInstDll相同,那么两个进程的地址空间中,GetMsgProc函数位于相同的位置,系统可以直接在进程A的地址空间中调用GetMsgProc。如果hInstDll不同,那么系统必须确定GetMsgProc函数在进程B中地址空间中的虚拟内存地址。地址公式为`GetMsgProc B= hInstDll B + (GetMsgProc A - hInstDll A)` 6. 系统在进程B中递增该DLL的锁计数器 7. 系统在进程B的地址空间中调用GetMsgProc函数 8. GetMsgProc返回时,系统递减该DLL在进程B中的锁计数器 注意当系统把挂钩过滤函数所在DLL注入或映射到地址空间中时,会映射整个DLL,而不仅仅是挂钩过滤函数 相比注册表注入DLL方法,挂钩允许我们在不需要该DLL时可以从进程的地址空间中撤销对它的映射,只需调用UnHookWindowsHookEx ~~~c++ BOOL UnhookWindowsHookEx(HHOOK hHook); ~~~ 当一个线程调用UnhookWindowsHookEx时,系统会遍历自己内部的一个已经注入过该DLL的进程列表,并将该DLL锁计数器递减。当锁计数器减到0时,系统会自动从进程的地址空间中撤销对该DLL的映射 ### 远程线程注入DLL Windows提供了一些函数让一个进程对另一个进程进行操作。从根本上说,**DLL注入技术要求目标进程中的一个线程调用LoadLibrary来加载目标DLL**。由于不能轻易控制其他进程中的线程,所以可以在目标进程中创建一个新的线程,由我们自己控制: ~~~c++ HANDLE CreateRemoteThread(HANDLE hProcess, PSECURITY_ATTRIBUTES psa, DWORD dwStackSize, PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD fdwCreate, PDWORD pdwThreadId); ~~~ 除了hProcess,其他参数和CreateThread完全相同,hProcess用来表示新创建的线程归哪个进程所有。参数pfnStartAddr是线程函数的内存地址。 对于LoadLibrary来说,实际上是:LoadLibraryA和LoadLibraryW,唯一区别在传给函数的参数类型,前者ANSI后者UNICODE。 那么使用下面一行代码实现远程线程注入DLL: ~~~c++ HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, LoadLibraryW, L"C:\\MyLib.dll", 0, NULL); ~~~ 这样新线程在远程进程中创建时,会立即调用LoadLibraryW并传入DLL路径名。 上面方法还有**两个问题**: 1. CreateRemoteThread直接引用LoadLibraryW,该引用会被解析为我们模块的导入段中的LoadLibraryW转换函数的地址,传到远程线程很可能就是访问违例。因此需要先调用GetProcAddress来获得LoadLibraryW的确切位置。但Kernel32.dll都被映射到进程地址空间中的同一内存地址,所以可以修改代码如下 ~~~c++ PTHREAD_START_ROUTINE pfnThreadRtn = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW"); HANDLE hThread = CreateRemoteThread(hProcessRemote, NULL, 0, pfnThreadRtn, L"C:\\MyLib.dll", 0, NULL); ~~~ 2. DLL路径字符串问题:字符串"C:\\MyLib.dll"是在调用进程的地址空间中,远程访问这个内存地址是没法找到的,此时远程就会崩溃终止。为解决这个问题,需要使用VirtualAllocEx函数在远程进程的地址空间中分配一个内存块来存储DLL路径字符串(VirtualFreeEx用来释放内存)。此外还需要方法将字符串从进程的地址空间复制到远程进程的地址空间中。Windows提供了ReadProcessMemory和WriteProcessMemory来进行读写: ~~~c++ BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID pvAddressRemote, PVOID pvBufferLocal, SIZE_T dwSize, SIZE_T* pdwNumBytesRead); BOOL WriteProcessMemory(HANDLE hProcess, LPCVOID pvAddressRemote, PVOID pvBufferLocal, SIZE_T dwSize, SIZE_T* pdwNumBytesWritten); ~~~ * hProcess:远程进程 * pvAddressRemote:远程进程中的地址 * pvBufferLocal:本地进程中的内存地址 * dwSize:要传输的字节数 * pdwNumBytesRead和pdwNumBytesWritten:实际传输的字节数,函数返回时可查看 完整代码如下 ~~~c++ #include <windows.h> #include <tchar.h> BOOL InjectDll(HANDLE hProcessRemote, const wchar_t* dllPath) { SIZE_T bytesWritten = 0; SIZE_T dllPathBytes = (wcslen(dllPath) + 1) * sizeof(wchar_t); // 1) 获取 LoadLibraryW 在本进程中的真实地址 HMODULE hKernel32 = GetModuleHandleW(L"Kernel32.dll"); if (!hKernel32) return FALSE; LPTHREAD_START_ROUTINE pfnLoadLibraryW = (LPTHREAD_START_ROUTINE)GetProcAddress(hKernel32, "LoadLibraryW"); if (!pfnLoadLibraryW) return FALSE; // 2) 在远程进程中申请内存保存 DLL 路径 LPVOID remoteBuf = VirtualAllocEx( hProcessRemote, NULL, dllPathBytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!remoteBuf) return FALSE; // 3) 把 DLL 路径写入远程进程 if (!WriteProcessMemory(hProcessRemote, remoteBuf, dllPath, dllPathBytes, &bytesWritten) || bytesWritten != dllPathBytes) { VirtualFreeEx(hProcessRemote, remoteBuf, 0, MEM_RELEASE); return FALSE; } // 4) 创建远程线程调用 LoadLibraryW(remoteBuf) HANDLE hThread = CreateRemoteThread( hProcessRemote, NULL, 0, pfnLoadLibraryW, remoteBuf, 0, NULL); if (!hThread) { VirtualFreeEx(hProcessRemote, remoteBuf, 0, MEM_RELEASE); return FALSE; } // 可选:等待线程结束 WaitForSingleObject(hThread, INFINITE); // 清理 CloseHandle(hThread); VirtualFreeEx(hProcessRemote, remoteBuf, 0, MEM_RELEASE); return TRUE; } ~~~ ### 其他方法注入DLL * 木马DLL:直接替换,但不自动适用版本变化,应尽量避免使用 * 将DLL作为调试器注入:麻烦 * 使用CreateProcess来注入代码:如果要注入代码的进程是由我们进程生成的,父进程可以挂起子进程,通过子进程的主线程的句柄,对线程执行的代码进行修改。可以设置线程的指令指针,让它执行内存映射文件中的代码: 1. 让进程生成一个被挂起的子进程 2. 从exe模块的文件头中取得主线程的起始内存地址 3. 将位于该内存地址处的机器指令保存起来 4. 强制将一些手动编写的shellcode写入内存地址。里面应调用LoadLibrary来加载一个DLL 5. 让子进程的主线程恢复运行,从而执行shellcode 6. 将保存起来的原始指令恢复到起始地址处 7. 让进程从起始地址继续执行,好像什么都没发生过一样 ## API拦截 ### 覆盖代码来拦截API 1. 在内存中定位要拦截的函数,得到它的内存地址 2. 将这个哈数起始的几个字节保存到我们自己的内存中 3. 用CPU的一条JUMP指令来覆盖这个函数的起始几个字节,这条jump指令用来跳转到替代函数的内存地址。当然,替代函数的签名必须与要拦截的函数的签名完全一致:**所有参数**必须相同,**返回值**必须相同,**调用约定**必须相同 撤销只需将保存的字节放回去 这个方法存在严重不足 1. CPU依赖性,指令不同 2. 在抢占式、多线程环境下根本不能工作:一个线程覆盖另一个函数起始位置的代码需要时间,这时候另一个线程可能会试图调用同一个函数,所以必须确保任一时刻只能有一个函数试图调用这个函数 ### 修改模块的导入段来拦截API 这个方法可以很好的解决上一种方法的两个问题。 首先需要理解**模块的导入段**,它包含了一组DLL以及一个符号表,其中列举出了该模块从各DLL中导入的符号。当该模块调用一个导入函数的时候,线程实际会先从模块的导入表中得到相应的导入函数底子,然后再跳转 所以要拦截函数很简单,修改这个函数在模块导入段中的地址即可。完全不存在对CPU的依赖,且没有修改函数代码,不必担心线程同步问题。 最后修改:2026 年 01 月 14 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏