Detours学习6 - 代码注入
前言
将代码注入到目标进程的方式有多种,然吾不明之所有,对于代码注入的方式前面有篇博文有做一下介绍,详情请见常见的十种代码注入技术。
这里只对Dll注入和Detours
注入方式做一个介绍。
思考:这个需要注入的Dll并不是一定要拦截进程空间的函数的,比如可以是对窗口信息的响应,亦或是对于某些特定会触发的事件的响应;但是大部分情况下为了驱动Dll中编写的逻辑,通常是对一些API函数进行了拦截。它是一种强大的技术,它允许劫持一个函数并将其重定向到一个自定义函数。在将控制权传递回原始API之前,可以在这些函数中执行任何操作。也不是说这么个特性是用来做恶意软件的,取决于开发者;可以做的功能很多,比如对软件功能的扩展与加强,管理类功能,监听类功能等等...
准备工作
再讲代码注入之前,事先得准备一个要注入的Dll,本文准备了两种Dll注入的实现:
Windows API
方式的Dll注入Detours
方式的Dll注入- 一个简单的目标进程
这里为了方便就直接拿文章Dtours学习1中的Dll来做说明,代码如下:
#include "pch.h" #pragma comment(lib, "detours.lib") static VOID(WINAPI* TureSleep)(DWORD dwMilliseconds) = Sleep; VOID WINAPI hkSleep(DWORD dwMilliseconds) { ULONGLONG dwBeg = GetTickCount64(); TureSleep(dwMilliseconds); ULONGLONG dwEnd = GetTickCount64(); TCHAR buffer[512]; _stprintf_s(buffer, sizeof(buffer) / sizeof(TCHAR), _T("Sleeped %llu milli sec"), dwEnd - dwBeg); MessageBox(NULL, buffer, _T(""), MB_OK); } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { if (DetourIsHelperProcess()) { return TRUE; } switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: DetourRestoreAfterWith(); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)TureSleep, hkSleep); DetourTransactionCommit(); break; case DLL_PROCESS_DETACH: DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)TureSleep, hkSleep); DetourTransactionCommit(); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; } return TRUE; }
思考:
hkSleep
中有一个很奇怪的问题,在拦截函数中直接调用了被拦截的TrueSleep
,如果是简单的在拦截函数首部加的JMP
跳转到hkSleep
,那么这样使用必然会出现栈溢出,然后在实际的使用过程中非常正常并没有出现栈溢出,这是为什么呢,原因在于Detours
的拦截实现,通过前面几篇关于Detours
的介绍可以知道Detours
重写了目标函数和一个专门用于跳转的Trampoline
函数,所以在调用TureSleep
之前没有Detach却没有出现栈溢出就不难理解了。可以想象到它这样的处理方式避免了某些多线程并发的情况下,由Hook/UnHook带来的诸多不安全的问题。
这里再贴一下目标进程的代码,非常简单只调用了一下Sleep
函数:
#include <iostream> #include <Windows.h> int main() { std::cout << "Hello World!\n"; while (true) { Sleep(5000); } }
Windows API
方式的注入
主要分为以下几个步骤:
- 查找目标进程,主要通过CreateToolhelp32Snapshot取得目标进程的
pid
然后通过OpenPrcess
来得到目标进程的句柄。 - 在进程虚拟空间通过
VirtualAllocEx
分配内存,再由WriteProcessMemory
在分配的内存中写入Dll的路径,这个路径将作为LoadLibaray
的参数。 - 在目标进程中开启一个线程并调用
LoadLibaray
来加载Dll,到此目标已经达成。
BOOL Inject(TCHAR* exePath, TCHAR* dllPath) { STARTUPINFO sinfo = { 0 }; PROCESS_INFORMATION pinfo = { 0 }; TCHAR dir[_MAX_DIR]{ 0 }; TCHAR diver[_MAX_DRIVE]{ 0 }; TCHAR fname[_MAX_FNAME]{ 0 }; TCHAR ext[_MAX_EXT]{ 0 }; _tsplitpath_s(exePath, diver, _MAX_DRIVE, dir, _MAX_DIR, fname, _MAX_FNAME, ext, _MAX_EXT); TCHAR _dir[MAX_PATH]{ 0 }; _tcscat_s(_dir, diver); _tcscat_s(_dir, dir); TCHAR _fname[MAX_PATH]{ 0 }; _tcscat_s(_fname, fname); _tcscat_s(_fname, ext); if (!CreateProcess(NULL, exePath, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, _dir, &sinfo, &pinfo)) { printf("创建进程失败,错误代码: %u\n", GetLastError()); return 0; } void* location = VirtualAllocEx(pinfo.hProcess, NULL, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (location == NULL) { printf("申请内存失败,错误代码:%u\n", GetLastError()); return 0; } // nSize: The number of bytes to be written to the specified process. if (!WriteProcessMemory(pinfo.hProcess, location, dllPath, (_tcslen(dllPath) + 1) * sizeof(TCHAR), NULL)) { printf("写入内存失败,错误代码:%u\n", GetLastError()); return 0; } HANDLE hThread = CreateRemoteThread(pinfo.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, location, 0, NULL); if (hThread == NULL) { printf("加载Dll失败,错误代码:%u\n", GetLastError()); return NULL; } WaitForSingleObject(hThread, INFINITE); VirtualFreeEx(pinfo.hProcess, location, 0, MEM_RELEASE); DWORD module; // module = STILL_ACTIVE 表示线程正在运行 // 若线程己经结束, 则module中存储Dll的HMODULE GetExitCodeThread(hThread, &module); if (module == NULL) { printf("加载的Dll未执行或加载Dll失败\n"); TerminateProcess(pinfo.hProcess, 0); return FALSE; } // 恢复 ResumeThread(pinfo.hThread); CloseHandle(hThread); return TRUE; }
这个方法的使用是只需要传入目标应用程序路径和需要注入到目标进程的Dll路径来完成注入,这个在某些情况下是能够提供一些方便,把启动目标进程和再去注入Dll两步简化成了一步操作,但是在某些情况下过于死板,比如目标进程只能通过一个Launcher
来启动的时候就不好使了,这个情况是存在的,Launcher
去调用Updater
进程,Updater
完成后再启动目标进程。那么还是再把Inject
函数拆分一下,考虑两种情况:
- 判断给出的路径,如果给出的路径只有文件名和扩展名,那么就以
OpenProcess
的方式去取得目标进程的句柄 - 如果给出的路径是完整的,那么先判断目标进程是否已经存在,如果存在则以
OpenProcess
的方式去取得目标进程的句柄,否则使用CreateProcess
扩展后的代码如下:
DWORD GetProcessId(TCHAR* exeName) { PROCESSENTRY32 procEntry; procEntry.dwSize = sizeof(procEntry); HANDLE hTool32 = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); DWORD ret = 0; do { if (_tcsicmp(exeName, procEntry.szExeFile) == 0) { ret = procEntry.th32ProcessID; break; } } while (Process32Next(hTool32, &procEntry)); CloseHandle(hTool32); return ret; } BOOL UseCreate(TCHAR* exePath, TCHAR* exeDir, TCHAR* dllPath) { STARTUPINFO sinfo = { 0 }; PROCESS_INFORMATION pinfo = { 0 }; if (!CreateProcess(NULL, exePath, NULL, NULL, TRUE, CREATE_SUSPENDED, NULL, exeDir, &sinfo, &pinfo)) { printf("创建进程失败,错误代码: %u\n", GetLastError()); return FALSE; } void* location = VirtualAllocEx(pinfo.hProcess, NULL, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (location == NULL) { printf("申请内存失败,错误代码:%u\n", GetLastError()); return FALSE; } // nSize: The number of bytes to be written to the specified process. if (!WriteProcessMemory(pinfo.hProcess, location, dllPath, (_tcslen(dllPath) + 1) * sizeof(TCHAR), NULL)) { printf("写入内存失败,错误代码:%u\n", GetLastError()); return FALSE; } HANDLE hThread = CreateRemoteThread(pinfo.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, location, 0, NULL); if (hThread == NULL) { printf("加载Dll失败,错误代码:%u\n", GetLastError()); return FALSE; } WaitForSingleObject(hThread, INFINITE); VirtualFreeEx(pinfo.hProcess, location, 0, MEM_RELEASE); DWORD module; // module = STILL_ACTIVE 表示线程正在运行 // 若线程己经结束, 则module中存储Dll的HMODULE GetExitCodeThread(hThread, &module); if (module == NULL) { printf("加载的Dll未执行或加载Dll失败\n"); TerminateProcess(pinfo.hProcess, 0); return FALSE; } // 恢复 ResumeThread(pinfo.hThread); CloseHandle(hThread); return TRUE; } BOOL UseOpen(DWORD pid, TCHAR* dllPath) { HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (hProcess == NULL) { printf("打开进程失败,错误代码: %u\n", GetLastError()); return FALSE; } void* location = VirtualAllocEx(hProcess, NULL, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (location == NULL) { printf("申请内存失败,错误代码:%u\n", GetLastError()); return 0; } // nSize: The number of bytes to be written to the specified process. if (!WriteProcessMemory(hProcess, location, dllPath, (_tcslen(dllPath) + 1) * sizeof(TCHAR), NULL)) { printf("写入内存失败,错误代码:%u\n", GetLastError()); return 0; } HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, location, 0, NULL); if (hThread == NULL) { printf("加载Dll失败,错误代码:%u\n", GetLastError()); return NULL; } WaitForSingleObject(hThread, INFINITE); VirtualFreeEx(hProcess, location, 0, MEM_RELEASE); DWORD module; // module = STILL_ACTIVE 表示线程正在运行 // 若线程己经结束, 则module中存储Dll的HMODULE GetExitCodeThread(hThread, &module); if (module == NULL) { printf("加载的Dll未执行或加载Dll失败\n"); return FALSE; } CloseHandle(hThread); CloseHandle(hProcess); return TRUE; } BOOL Inject(TCHAR* exePath, TCHAR* dllPath) { TCHAR dir[_MAX_DIR]{ 0 }; TCHAR drive[_MAX_DRIVE]{ 0 }; TCHAR fname[_MAX_FNAME]{ 0 }; TCHAR ext[_MAX_EXT]{ 0 }; _tsplitpath_s(exePath, drive, _MAX_DRIVE, dir, _MAX_DIR, fname, _MAX_FNAME, ext, _MAX_EXT); TCHAR _dir[MAX_PATH]{ 0 }; TCHAR _proc[MAX_PATH]{ 0 }; // 组合目录名 _tcscat_s(_dir, drive); _tcscat_s(_dir, dir); // 组合进程名 _tcscat_s(_proc, fname); _tcscat_s(_proc, ext); // 给出的进程路径只有文件名和扩展名 if (drive == NULL && dir == NULL) { DWORD pid = GetProcessId(_proc); if (pid == NULL) { printf("获取进程ID失败\n"); return FALSE; } return UseOpen(pid, dllPath); } // 相对路径:drive为空dir不为空 // 绝对路径:drive不为空dir不为空(drive不为空的情况下dir绝对不为空) else { DWORD pid = GetProcessId(_proc); // 目标进程已经打开 if (pid != NULL) { return UseOpen(pid, dllPath); } else { return UseCreate(exePath, _dir, dllPath); } } }
入口函数就是做一下命令行参数的传递和文件是否存在的判断,代码如下:
#include <stdio.h> #include <stdlib.h> #include <Windows.h> #include <tchar.h> #include <TlHelp32.h> #ifndef _UNICODE #include <io.h> #endif // !_UNICODE int main(int argc, char* argv[]) { if (argc != 3) { printf("用法:\nSimpleInjector exe dll\n"); return 0; } BOOL NoError = TRUE; TCHAR argv1[MAX_PATH]{ 0 }; TCHAR argv2[MAX_PATH]{ 0 }; #ifdef _UNICODE size_t len = strlen(argv[1]); MultiByteToWideChar(CP_ACP, 0, argv[1], (int)len, argv1, (int)len); len = strlen(argv[2]); MultiByteToWideChar(CP_ACP, 0, argv[2], (int)len, argv2, (int)len); // 判断Dll文件是否存在 if (_taccess(argv2, 0) == -1) { printf("Dll文件不存在\n"); NoError = FALSE; } if (NoError) Inject(argv1, argv2); #else // 判断Dll文件是否存在 if (_taccess(argv[2], 0) == -1) { printf("Dll文件不存在\n"); NoError = FALSE; } if (NoError) Inject(argv[1], argv[2]); #endif // _UNICODE printf("Press Escape To Exit...\n"); while (true) { if (GetAsyncKeyState(VK_ESCAPE)) { break; } } }
思考:还有可扩展的地方,有注入应该也要有卸载Dll的地方,那么还可以添加一个
Eject
的函数来卸载Dll,主要是通过FreeLibaray
来卸载,它需要一个HMODULE
的参数,也就是Dll在目标进程中的模块句柄,这个句柄可以通过CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid)
去获取,然后通过CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, (LPVOID)module, 0, NULL)
去完成卸载。下面简单的贴一下
GetModuleHandle
的代码:
HMODULE GetModuleHandle(DWORD pid, TCHAR* name) { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid); MODULEENTRY32 modEntery; modEntery.dwSize = sizeof(modEntery); HMODULE ret = NULL; do { if (_tcsicmp(moduleEntery.szModule, moduleName) == 0) { handle = moduleEntery.hModule; break; } } while (Module32Next(snapshot, &modEntery)); CloseHandle(snapshot); return ret; }
Windows API
方式注入的验证
SimpleInjector hksleepdemo.exe e:\hksleep32.dll
注意:这里的SimpleInjector是32位的,hkSleepDemo.exe也是32位的,e:\hkSleep32.dll也是32位的,这种限制在Detours
可以被打破!
Detours
方式的注入
Detours
提供了函数DetourCreateProcessWithDll
,该函数等效于CreateProcess
函数带上CREATE_SUSPENDED
标志的调用,需要留意的地方就是被注入的Dll必须要在导出序号1上面导出一个DetourFinishHelperProcess
的函数,关于如何导出请参数前面的文章Detours学习4,文章末尾有导出的说明。
下面就使用Detours
实现一个与上面相似的功能,main
函数与上面基本是一致的:
#include <stdio.h> #include <Windows.h> #include <tchar.h> #include <detours/detours.h> #include <io.h> BOOL Inject(TCHAR* exePath, char* dllPath) { TCHAR dir[_MAX_DIR]{ 0 }; TCHAR drive[_MAX_DRIVE]{ 0 }; TCHAR fname[_MAX_FNAME]{ 0 }; TCHAR ext[_MAX_EXT]{ 0 }; _tsplitpath_s(exePath, drive, _MAX_DRIVE, dir, _MAX_DIR, fname, _MAX_FNAME, ext, _MAX_EXT); TCHAR _dir[MAX_PATH]{ 0 }; TCHAR _proc[MAX_PATH]{ 0 }; // 组合目录名 _tcscat_s(_dir, drive); _tcscat_s(_dir, dir); // 组合进程名 _tcscat_s(_proc, fname); _tcscat_s(_proc, ext); STARTUPINFO sinfo{ 0 }; PROCESS_INFORMATION pinfo{ 0 }; sinfo.cb = sizeof(sinfo); return DetourCreateProcessWithDllEx(NULL, exePath, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, _dir, &sinfo, &pinfo, dllPath, (PDETOUR_CREATE_PROCESS_ROUTINEW)CreateProcess); }
Detours
方式的验证
在执行过程中发现有一个明显不同的地方,这种方式来注入父进程与目标进程合并成了一个窗口!
思考:有时候可能需要拦截的函数并不是标准的API函数或已知的导出函数,这时可以通过函数的硬缎地址进行拦截。需要知道目标函数的地址与参数,如下有一个代码示例:
#include <windows.h> #include <detours\detours.h> typedef void (WINAPI *pFunc)(DWORD); void WINAPI MyFunc(DWORD); pFunc FuncToDetour = (pFunc)(0x0100347C); //Set it at address to detour in the process INT APIENTRY DllMain(HMODULE hDLL, DWORD Reason, LPVOID Reserved) { switch(Reason) { case DLL_PROCESS_ATTACH: { DisableThreadLibraryCalls(hDLL); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)FuncToDetour, MyFunc); DetourTransactionCommit(); } break; case DLL_PROCESS_DETACH: DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)FuncToDetour, MyFunc); DetourTransactionCommit(); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; } return TRUE; } void WINAPI MyFunc(DWORD someParameter) { //Some magic can happen here }
关于32位与64位智能注入的问题
当父进程是32位目标进程是64位或者父进程是64位目标进程是32位的情况下,我使用DetourCreateProcessWithDllEx
没有测试成功,是因为没有明白文档中说的意思由DetourCreateWithDllEx
的lpDllPathName
参数的说明:
Pathname of the DLL to be insert into the new process. To support both 32- bit and 64-bit applications, The DLL name should end with "32" if the DLL contains 32-bit code and should end with "64" if the DLL contains 64-bit code. If the target process differs in size from the parent process, Detours will automatically replace "32" with "64" or "64" with "32" in the path name.
是我对这个意思产生的误解,上面参数的说明主要就是说了一点:在父进程与目标进程不同的情况下,这个要注入的Dll是32或64它不重要,DetourCreateWithDllEx
会根据目标进程的位自动判断注入哪一个,有一点需要注意的是在父进程与目标进程位数相同的情况下,它将不做自动的判断了,这时指定注入的Dll应该要正确,否则将出现错误。
文件的目录结构如下:
Root Direction ├─hkSleep32.dll ├─hkSleep64.dll ├─hkSleepDemo32.exe └─hkSleepDemo64.exe
对于刚刚编写的DetourInjector
启动参数几种情况说明:
DetourInjector64.exe e:\hksleepdemo32.exe e:\hksleep32.dll
或DetourInjector64.exe e:\hksleepdemo32.exe e:\hksleep64.dll
这种是正常的,它会根据目标进程hkSleepDemo32.exe
是32位的注入32位的hkSleep32.dll
到目标进程DetourInjector32.exe e:\hksleepdemo64.exe e:\hksleep32.dll
或DetourInjector32.exe e:\hksleepdemo64.exe e:\hksleep64.dll
这种是正常的,它会根据目标进程hkSleepDemo64.exe
是64位的注入64位的hkSleep64.dll
到目标进程- 在父进程与目标进程位数相同的情况下,如
DetourInjector32.exe e:\hksleepdemo32.exe e:\hksleep32.dll
或DetourInjector64.exe e:\hksleepdemo64.exe e:\hksleep64.dll
则必须指定相应正确位数的Dll到目标进程中,这个时候Detours
不会自动判断,不匹配将会产生异常。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗