Detours学习6 - 代码注入

前言

将代码注入到目标进程的方式有多种,然吾不明之所有,对于代码注入的方式前面有篇博文有做一下介绍,详情请见常见的十种代码注入技术

这里只对Dll注入和Detours注入方式做一个介绍。

思考:这个需要注入的Dll并不是一定要拦截进程空间的函数的,比如可以是对窗口信息的响应,亦或是对于某些特定会触发的事件的响应;但是大部分情况下为了驱动Dll中编写的逻辑,通常是对一些API函数进行了拦截。它是一种强大的技术,它允许劫持一个函数并将其重定向到一个自定义函数。在将控制权传递回原始API之前,可以在这些函数中执行任何操作。也不是说这么个特性是用来做恶意软件的,取决于开发者;可以做的功能很多,比如对软件功能的扩展与加强,管理类功能,监听类功能等等...


准备工作

再讲代码注入之前,事先得准备一个要注入的Dll,本文准备了两种Dll注入的实现:

  1. Windows API方式的Dll注入
  2. Detours方式的Dll注入
  3. 一个简单的目标进程

这里为了方便就直接拿文章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方式的注入

主要分为以下几个步骤:

  1. 查找目标进程,主要通过CreateToolhelp32Snapshot取得目标进程的pid然后通过OpenPrcess来得到目标进程的句柄。
  2. 在进程虚拟空间通过VirtualAllocEx分配内存,再由WriteProcessMemory在分配的内存中写入Dll的路径,这个路径将作为LoadLibaray的参数。
  3. 在目标进程中开启一个线程并调用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函数拆分一下,考虑两种情况:

  1. 判断给出的路径,如果给出的路径只有文件名和扩展名,那么就以OpenProcess的方式去取得目标进程的句柄
  2. 如果给出的路径是完整的,那么先判断目标进程是否已经存在,如果存在则以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没有测试成功,是因为没有明白文档中说的意思由DetourCreateWithDllExlpDllPathName参数的说明:

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启动参数几种情况说明:

  1. DetourInjector64.exe e:\hksleepdemo32.exe e:\hksleep32.dllDetourInjector64.exe e:\hksleepdemo32.exe e:\hksleep64.dll这种是正常的,它会根据目标进程hkSleepDemo32.exe是32位的注入32位的hkSleep32.dll到目标进程
  2. DetourInjector32.exe e:\hksleepdemo64.exe e:\hksleep32.dllDetourInjector32.exe e:\hksleepdemo64.exe e:\hksleep64.dll这种是正常的,它会根据目标进程hkSleepDemo64.exe是64位的注入64位的hkSleep64.dll到目标进程
  3. 在父进程与目标进程位数相同的情况下,如DetourInjector32.exe e:\hksleepdemo32.exe e:\hksleep32.dllDetourInjector64.exe e:\hksleepdemo64.exe e:\hksleep64.dll则必须指定相应正确位数的Dll到目标进程中,这个时候Detours不会自动判断,不匹配将会产生异常
posted @ 2020-12-23 23:56  非法关键字  阅读(880)  评论(0编辑  收藏  举报