首页
论坛
课程
招聘
[原创]常见的几种DLL注入技术
2021-10-21 21:06 14878

[原创]常见的几种DLL注入技术

2021-10-21 21:06
14878

一.前言

这次实验是在WIN7 X86系统上进程,使用的编译器是VS2017。

所谓的DLL注入,其实就是在其他的进程中把我们编写的DLL加载进去。如下图所示

而加载Dll的API就是LoadLibrary,它的参数是保存要加载的DLL的路径的地址。所以DLL注入的核心就是把要注入的DLL的路径写到目标进程中,然后在目标进程中调用LoadLibrary函数,并且指定参数为保存了DLL路径的地址。

要实现DLL注入,首先就要创建一个用来注入的DLL。在VS2017中要生成一个DLL项目,只需要向下图这样创建一个DLL工程就好

在生成的文件中,有个dllmain.cpp,打开以后内容如下

当DLL的状态发生变化的时候,就会调用DllMain函数。而传递的ul_reason_for_call这个参数代表了4种不同的状态变化的情况,我们就可以根据这四种不同的状态根据需要来写出相应的代码,就会让注入的DLL执行我们需要的功能


ul_reason_for_call的值代表的状态
DLL_PROCESS_ATTACHDll刚刚映射到进程空间中
DLL_THREAD_ATTACH进程中有新线程创建
DLL_THREAD_DETACH进程中有新线程销毁

DLL_PROCESS_DETACH

Dll从进程空间中接触映射


不过在实现DLL注入的时候用的DLL几乎都是在Dll刚刚映射到进程空间的时候就执行相关的代码。比如像下面这样,创建一个新线程来执行代码,这里在桌面打开一个文件来并写入加载这个DLL的进程的完成路径名。由于是独占方式打开,此时如果多个线程同时打开这个文件,CreateFile就会出错,错误码就会是32,根据这个来对线程进行休眠,等其他线程使用完了,再次打开文件进行操作。

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include <Windows.h>
#include <Shlobj.h>
#pragma comment(lib, "shell32.lib")

#define FILE_NAME "result.txt"

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	HANDLE hFile = NULL;
	CHAR szDesktopFile[MAX_PATH] = { 0 };	//保存系统桌面路径
	CHAR szFullFilePath[MAX_PATH] = { 0 };	//保存完成的加载DLL文件的文件路径
	DWORD dwRetLen = 0, dwFileLen = 0;
	BOOL bRet = TRUE;

	//获取桌面路径
	bRet = SHGetSpecialFolderPath(NULL, szDesktopFile, CSIDL_DESKTOP, TRUE);
	if (bRet)
	{
		strcat(szDesktopFile, "\\");
		strcat(szDesktopFile, FILE_NAME);
		while (TRUE)
		{
			hFile = CreateFile( szDesktopFile,
								GENERIC_READ | GENERIC_WRITE,
								0, NULL,
								OPEN_ALWAYS,
								FILE_ATTRIBUTE_NORMAL, NULL);
			if (hFile == INVALID_HANDLE_VALUE)	//打开文件错误
			{
				if (GetLastError() == 32)	//错误码是不是其他进程正在使用这个文件,是的话等待一会在继续打开
				{
					Sleep(200);
					continue;
				}
				else break;
			}
			else
			{
				GetModuleFileName(NULL, szFullFilePath, MAX_PATH);	//获取加载DLL的进程的完整路径
				dwFileLen = strlen(szFullFilePath);
				szFullFilePath[dwFileLen] = '\r';	//由于是在WIN7运行,换行符是\r\n
				szFullFilePath[dwFileLen + 1] = '\n';
				SetFilePointer(hFile, 0, NULL, FILE_END);
				WriteFile(hFile, szFullFilePath, dwFileLen + 2, &dwRetLen, NULL);
				if (hFile) CloseHandle(hFile);
				break;
			}
		}
	}
	
	return 0;
}


BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
		case DLL_PROCESS_ATTACH:
		{
			HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
			if (hThread) CloseHandle(hThread);
			break;
		}
		case DLL_THREAD_ATTACH:
		case DLL_THREAD_DETACH:
		case DLL_PROCESS_DETACH:
			break;
    }
    return TRUE;
}

点击生成解决方案以后就可以在项目目录下找到相应的DLL文件,如下图。这个文件就是用来注入到其他进程的DLL。

二.代码框架

由于要编写的代码中,只有注入功能不同,但是其他的辅助功能。比如,提权,获取进程PID等等是一样的,为了避免重复就先在这给出代码的框架。后面的不同注入技术只需根据需要加进去就好。注意,如果想要提权成功,需要用管理员权限运行代码

#include <cstdio>
#include <Windows.h>
#include <TlHelp32.h>

#define PROCESS_NAME "taskmgr.exe"	//要注入的进程名,这个是任务管理器的进程名
#define DLL_NAME "InjectDll.dll"	//要注入的DLL的名称

BOOL InjectDll(DWORD dwPid, CHAR szDllName[]);	//注入DLL
DWORD GetPID(PCHAR pProName); //根据进程名获取PID
VOID ShowError(PCHAR msg);	//打印错误信息
BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName);	//提升进程权限

int main()
{
	CHAR szDllPath[MAX_PATH] = { 0 };	//保存要注入的DLL的路径
	DWORD dwPID = 0;					//保存要注入的进程的PID

	// 提升当前进程令牌权限
	if (!EnbalePrivileges(GetCurrentProcess(), SE_DEBUG_NAME))
	{
		printf("权限提升失败\n");
	}

	dwPID = GetPID(PROCESS_NAME);
	if (dwPID == 0)
	{
		printf("没有找到要注入的进程\n");
		goto exit;
	}
	
	GetCurrentDirectory(MAX_PATH, szDllPath);	//获取程序的目录
	strcat(szDllPath, "\\");
	strcat(szDllPath, DLL_NAME);				//与DLL名字拼接得到DLL的完整路径
	printf("要注入的进程名:%s PID:%d\n", PROCESS_NAME, dwPID);
	printf("要注入的DLL的完整路径%s\n", szDllPath);

	if (InjectDll(dwPID, szDllPath))
	{
		printf("Dll注入成功\n");
	}
exit:
	system("pause");

	return 0;
}

BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
	BOOL bRet = TRUE;

	return bRet;
}

DWORD GetPID(PCHAR pProName)
{
	PROCESSENTRY32 pe32 = { 0 };
	HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	BOOL bRet = FALSE;
	DWORD dwPID = 0;

	if (hSnap == INVALID_HANDLE_VALUE)
	{
		printf("CreateToolhelp32Snapshot process %d\n", GetLastError());
		goto exit;
	}

	pe32.dwSize = sizeof(pe32);
	bRet = Process32First(hSnap, &pe32);
	while (bRet)
	{
		if (lstrcmp(pe32.szExeFile, pProName) == 0)
		{
			dwPID = pe32.th32ProcessID;
			break;
		}
		bRet = Process32Next(hSnap, &pe32);
	}

	CloseHandle(hSnap);
exit:
	return dwPID;
}

VOID ShowError(PCHAR msg)
{
	printf("%s Error %d\n", msg, GetLastError());
}

BOOL EnbalePrivileges(HANDLE hProcess, char *pszPrivilegesName)
{
	HANDLE hToken = NULL;
	LUID luidValue = { 0 };
	TOKEN_PRIVILEGES tokenPrivileges = { 0 };
	BOOL bRet = FALSE;
	DWORD dwRet = 0;


	// 打开进程令牌并获取具有 TOKEN_ADJUST_PRIVILEGES 权限的进程令牌句柄
	if (!OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES, &hToken))
	{
		ShowError("OpenProcessToken");
		goto exit;
	}

	// 获取本地系统的 pszPrivilegesName 特权的LUID值
	if (!LookupPrivilegeValue(NULL, pszPrivilegesName, &luidValue))
	{
		ShowError("LookupPrivilegeValue");
		goto exit;
	}

	// 设置提升权限信息
	tokenPrivileges.PrivilegeCount = 1;
	tokenPrivileges.Privileges[0].Luid = luidValue;
	tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	// 提升进程令牌访问权限
	if (!AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, 0, NULL, NULL))
	{
		ShowError("AdjustTokenPrivileges");
		goto exit;
	}
	else
	{
		// 根据错误码判断是否特权都设置成功
		dwRet = ::GetLastError();
		if (ERROR_SUCCESS == dwRet)
		{
			bRet = TRUE;
			goto exit;
		}
		else if (ERROR_NOT_ALL_ASSIGNED == dwRet)
		{
			ShowError("ERROR_NOT_ALL_ASSIGNED");
			goto exit;
		}
	}
exit:
	return bRet;
}

三.远程线程注入

这种注入方式可以说是最常用的注入方式了,它的核心就是调用Windows提供的CreateRemoteThread函数。该函数可以在其他的进程空间中创建一个新的线程进行执行,该函数在文档中的定义如下

HANDLE WINAPI CreateRemoteThread(
  __in   HANDLE hProcess,
  __in   LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in   SIZE_T dwStackSize,
  __in   LPTHREAD_START_ROUTINE lpStartAddress,
  __in   LPVOID lpParameter,
  __in   DWORD dwCreationFlags,
  __out  LPDWORD lpThreadId);
参数说明
hProcess要创建线程的进程句柄
lpThreadAttributes新线程的安全描述符
dwStackSize堆栈起始大小,为0表示默认大小
lpStartAddress表示要运行线程的起始地址
lpParameter保存要传递给线程参数的地址
dwCreationFlags控制线程创建的标志,为0表示创建后立即执行
lpThreadId指向接收线程标识符变量的指针。为NULL表示不返回线程标识符


其中的关键三个参数分别是

  1. hProcess用来指定在哪个进程中创建新线程

  2. lpStartAddress用来指定将进程中的哪个地址开始作为新线程运行的起始地址

  3. lpParameter保存的也是一个地址,这个地址中保存的就是新线程要用到的参数

那也就是说只要我们指定了一个地址给lpStartAddress,那么我们就可以在其他进程中创建一个线程来执行程序。而再看加载DLL的LoadLibrary函数在文档中的定义如下

HMODULE WINAPI LoadLibrary(__in  LPCTSTR lpFileName);

可以看到,这个函数同样也只需要一个参数,这个参数是一个地址,而这个地址中保存的是我们要加载的DLL的名称的字符串。

根据这些,不难想到,只要我们可以获取新进程中的LoadLibrary函数的地址以及包含有要加载的DLL的字符串的地址就可以通过CreateRemoteThread函数来成功开起一个线程执行LoadLibrary函数来加载我们的DLL。

那么现在的问题就是如何获得LoadLibrary函数的地址以及保存有要加载的DLL路径的字符串的地址。

对于LoadLibrary函数,由于它是在常用的系统DLL,也就是KERNEL32.dll中,所以这个DLL是可以按照它的ImageBase成功装载到每个进程的空间中。这样的话Kernel32.dll在每个进程中的起始地址是一样的,那么LoadLibrary函数的地址也就会一样。那么我们就可以在本进程中查找LoadLibrary函数的地址,并且完全可以相信,在要注入DLL的进程中LoadLibrary的地址也是这个。

至于DLL名称的字符串,我们可以通过在进程中申请一块可以将DLL完整路径写入的内存,并在这个内存中将DLL的完整路径写入,将写入到注入进程DLL完整路径的内存地址作为参数就可以实现进程的注入。

具体代码如下

BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
	BOOL bRet = TRUE;
	HANDLE hProcess = NULL, hRemoteThread = NULL;
	HMODULE hKernel32 = NULL;
	DWORD dwSize = 0;
	LPVOID pDllPathAddr = NULL;
	PVOID pLoadLibraryAddr = NULL;

	// 打开注入进程,获取进程句柄
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
	if (NULL == hProcess)
	{
		ShowError("OpenProcess");
		bRet = FALSE;
		goto exit;
	}

	// 在注入进程中申请可以容纳DLL完成路径名的内存空间
	dwSize = 1 + strlen(szDllName);
	pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
	if (!pDllPathAddr)
	{
		ShowError("VirtualAllocEx");
		bRet = FALSE;
		goto exit;
	}

	// 把DLL完整路径名写入进程中
	if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL))
	{
		ShowError("WriteProcessMemory");
		bRet = FALSE;
		goto exit;
	}

	
	hKernel32 = LoadLibrary("kernel32.dll");
	if (hKernel32 == NULL)
	{
		ShowError("LoadLibrary");
		bRet = FALSE;
		goto exit;
	}

	// 获取LoadLibraryA函数地址
	pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA");
	if (pLoadLibraryAddr == NULL)
	{
		ShowError("GetProcAddress ");
		bRet = FALSE;
		goto exit;
	}

	//创建远程线程进行DLL注入
	hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, 
					  (LPTHREAD_START_ROUTINE)pLoadLibraryAddr,
					   pDllPathAddr, 0, NULL);
	if (hRemoteThread == NULL)
	{
		ShowError("CreateRemoteThread");
		bRet = FALSE;
		goto exit;
	}
	
exit:
	if (hKernel32) FreeLibrary(hKernel32);
	if (hProcess) CloseHandle(hProcess);
	if (hRemoteThread) CloseHandle(hRemoteThread);
	
	return bRet;
}

四.加强版远程线程注入

上面的方法虽然可以方便的注入DLL。但是在WIN7,WIN10系统上,会由于SESSION 0隔离机制从而导致只能成功注入普通的用户进程,如果注入系统进程就会导致失败。而经过逆向分析发现,使用Kernel32.dll中的CreateRemoteThread进行注入的时候,程序会走到ntdll.dll中的ZwCreateThreadEx函数进行执行。这是一个未导出的函数,所以需要手动获取函数地址来进行调用,相比于CreateRemoteThread更加底层。这个函数在64位和32位系统中的函数声明也不相同,在32位中的声明如下

typedef DWORD(WINAPI *pFnZwCreateThreadEx)(
		PHANDLE ThreadHandle,
		ACCESS_MASK DesiredAccess,
		LPVOID ObjectAttributes,
		HANDLE ProcessHandle,
		LPTHREAD_START_ROUTINE lpStartAddress,
		LPVOID lpParameter,
		BOOL CreateSuspended,
		DWORD dwStackSize,
		DWORD dw1,
		DWORD dw2,
		LPVOID pUnkown);

而在64位中的声明如下

typedef DWORD(WINAPI *pFnZwCreateThreadEx)(
		PHANDLE ThreadHandle,
		ACCESS_MASK DesiredAccess,
		LPVOID ObjectAttributes,
		HANDLE ProcessHandle,
		LPTHREAD_START_ROUTINE lpStartAddress,
		LPVOID lpParameter,
		ULONG CreateThreadFlags,
		SIZE_T ZeroBits,
		SIZE_T StackSize,
		SIZE_T MaximumStackSize,
		LPVOID pUnkown);

根据逆向分析的结果,在内核6.0(WIN7, WIN10)等系统上调用CreateRemoteThread的时候,当程序走到ZwCreateThreaEx的时候它第7个参数,也就是CreateThreadFlags会被设置为1,如下图

它会导致线程创建的时候就被挂起,随后查看要运行的进程所在的会话层之后再决定是否要恢复线程的运行。所以要破解这种情况只需要将第7个参数设为0就可以,相应代码如下

typedef DWORD(WINAPI *pFnZwCreateThreadEx)(PHANDLE, ACCESS_MASK, LPVOID, 
					   HANDLE, LPTHREAD_START_ROUTINE,
					   LPVOID, BOOL, DWORD, DWORD, DWORD, LPVOID);
					
BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
	BOOL bRet = TRUE;
	HANDLE hProcess = NULL, hRemoteThread = NULL;
	HMODULE hKernel32 = NULL, hNtDll = NULL;
	DWORD dwSize = 0;
	LPVOID pDllPathAddr = NULL;
	PVOID pLoadLibraryAddr = NULL;
	pFnZwCreateThreadEx ZwCreateThreadEx = NULL;

	// 打开注入进程,获取进程句柄
	hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
	if (NULL == hProcess)
	{
		ShowError("OpenProcess");
		bRet = FALSE;
		goto exit;
	}

	// 在注入进程中申请可以容纳DLL完成路径名的内存空间
	dwSize = 1 + strlen(szDllName);
	pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
	if (!pDllPathAddr)
	{
		ShowError("VirtualAllocEx");
		bRet = FALSE;
		goto exit;
	}

	// 把DLL完成路径名写入进程中
	if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL))
	{
		ShowError("WriteProcessMemory");
		bRet = FALSE;
		goto exit;
	}

	
	hKernel32 = LoadLibrary("kernel32.dll");
	if (hKernel32 == NULL)
	{
		ShowError("LoadLibrary kernel32");
		bRet = FALSE;
		goto exit;
	}

	// 获取LoadLibraryA函数地址
	pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA");
	if (pLoadLibraryAddr == NULL)
	{
		ShowError("GetProcAddress LoadLibraryA");
		bRet = FALSE;
		goto exit;
	}

	hNtDll = LoadLibrary("ntdll.dll");
	if (hNtDll == NULL)
	{
		ShowError("LoadLibrary ntdll");
		bRet = FALSE;
		goto exit;
	}
	
	ZwCreateThreadEx = (pFnZwCreateThreadEx)GetProcAddress(hNtDll, "ZwCreateThreadEx");
	if (!ZwCreateThreadEx)
	{
		ShowError("GetProcAddress ZwCreateThreadEx");
		bRet = FALSE;
		goto exit;
	}

	ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, 
					 hProcess, (LPTHREAD_START_ROUTINE)pLoadLibraryAddr,
					 pDllPathAddr, 0, 0, 0, 0, NULL);
	if (hRemoteThread == NULL)
	{
		ShowError("ZwCreateThreadEx");
		bRet = FALSE;
		goto exit;
	}
exit:
	if (hKernel32) FreeLibrary(hKernel32);
	if (hNtDll) FreeLibrary(hNtDll);
	if (hProcess) CloseHandle(hProcess);
	if (hRemoteThread) CloseHandle(hRemoteThread);
	return bRet;
}

五.APC注入

在Windows系统中,每个线程都会维护一个自己的APC队列,这个APC队列中保存了要求线程执行的一些APC函数。对于用户模式的APC队列,当线程处在可警告状态时,就会执行这些APC函数。而要往APC队列中增加APC函数,需要通过QueueUserAPC函数来实现,这个函数在文档中的定义如下

DWORD WINAPI QueueUserAPC(
  __in  PAPCFUNC pfnAPC,
  __in  HANDLE hThread,
  __in  ULONG_PTR dwData);
参数说明
pfnAPC当满足条件时,要执行的APC函数的地址
hThread指定增加APC函数的线程句柄
dwData要执行的APC函数参数地址


可以看到pfnAPC和dwData这两个参数和CreateRemoteThread中的lpStartAddress和lpParameter的作用是一样的。不过这里是对线程进行操作,一个进程有多个线程。所以为了确保程序正确运行,所以需要遍历所有线程,查看是否是要注入的进程的线程,依次获得句柄插入APC函数。具体代码如下

BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
	BOOL bRet = TRUE;
	HANDLE hProcess = NULL, hThread = NULL, hSnap = NULL;
	HMODULE hKernel32 = NULL;
	DWORD dwSize = 0;
	PVOID pDllPathAddr = NULL;
	PVOID pLoadLibraryAddr = NULL;
	THREADENTRY32 te32 = { 0 };

	// 打开注入进程,获取进程句柄
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
	if (NULL == hProcess)
	{
		ShowError("OpenProcess");
		bRet = FALSE;
		goto exit;
	}

	// 在注入进程中申请可以容纳DLL完成路径名的内存空间
	dwSize = 1 + strlen(szDllName);
	pDllPathAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
	if (!pDllPathAddr)
	{
		ShowError("VirtualAllocEx");
		bRet = FALSE;
		goto exit;
	}

	// 把DLL完成路径名写入进程中
	if (!WriteProcessMemory(hProcess, pDllPathAddr, szDllName, dwSize, NULL))
	{
		ShowError("WriteProcessMemory");
		bRet = FALSE;
		goto exit;
	}


	hKernel32 = LoadLibrary("kernel32.dll");
	if (hKernel32 == NULL)
	{
		ShowError("LoadLibrary");
		bRet = FALSE;
		goto exit;
	}

	// 获取LoadLibraryA函数地址
	pLoadLibraryAddr = GetProcAddress(hKernel32, "LoadLibraryA");
	if (pLoadLibraryAddr == NULL)
	{
		ShowError("GetProcAddress");
		bRet = FALSE;
		goto exit;
	}

	//获得线程快照
	hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
	if (!hSnap)
	{
		ShowError("CreateToolhelp32Snapshot");
		bRet = FALSE;
		goto exit;
	}

	//遍历线程
	te32.dwSize = sizeof(te32);
	if (Thread32First(hSnap, &te32))
	{
		do
		{
			//这个线程的进程ID是不是要注入的进程的PID
			if (te32.th32OwnerProcessID == dwPid)
			{
				hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
				if (hThread)
				{
					QueueUserAPC((PAPCFUNC)pLoadLibraryAddr, hThread, (ULONG_PTR)pDllPathAddr);
					CloseHandle(hThread);
					hThread = NULL;
				}
				else
				{
					ShowError("OpenThread");
					bRet = FALSE;
					goto exit;
				}
			}
		} while (Thread32Next(hSnap, &te32));
	}
exit:
	if (hKernel32) FreeLibrary(hKernel32);
	if (hProcess) CloseHandle(hProcess);
	if (hThread) CloseHandle(hThread);
	return bRet;
}

六.AppInit_DLLs注入

这种注入方式主要是通过修改注册表中HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows中的AppInit_DLLs和LoadAppInit_Dlls,如下图

只要将AppInit_DLLs设置为要注入的DLL的路径并且将LoadAppInit_DLLs的值改成1。那么,当程序重启的时候,所有加载user32.dll的进程都会根据AppInit_Dlls中的DLL路径加载指定的DLL。

所以这种DLL注入的实现代码如下

BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
	BOOL bRet = TRUE;
	HKEY hKey = NULL;
	CHAR szAppKeyName[] = { "AppInit_DLLs" };
	CHAR szLoadAppKeyName[] = { "LoadAppInit_DLLs" };
	DWORD dwLoadAppInit = 1; //设置LoadAppInit_DLLs的值

	//打开相应注册表键
	if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows",
		0, KEY_ALL_ACCESS, &hKey) != ERROR_SUCCESS)
	{
		ShowError("RegOpenKeyEx");
		bRet = FALSE;
		goto exit;
	}

	//设置AppInit_DLLs为相应的DLL路径
	if (RegSetValueEx(hKey, szAppKeyName, 0, REG_SZ, (PBYTE)szDllName, strlen(szDllName) + 1) != ERROR_SUCCESS)
	{
		ShowError("RegSetValueEx");
		bRet = FALSE;
		goto exit;
	}

	//将LoadAppInit_DLLs的值设为1
	if (RegSetValueEx(hKey, szLoadAppKeyName, 0, REG_DWORD, (PBYTE)&dwLoadAppInit, sizeof(dwLoadAppInit)) != ERROR_SUCCESS)
	{
		ShowError("RegSetValueEx");
		bRet = FALSE;
		goto exit;
	}
exit:
	return bRet;
}

运行程序以后,会发现相应的键值已经被设置

七.全局钩子注入

Windows系统中的大多数应用都是基于消息机制的,也就是说它们都有一个消息过程函数,可以根据收到的不同消息来执行不同的代码。基于这种消息机制,Windows维护了一个OS message queue以及为每个程序维护着一个application message queue。当发生各种事件的时候,比如敲击键盘,点击鼠标等等,操作系统会从OS message queue将消息取出给到相应的程序的application message queue。

而OS message queue和application message queue的中间有一个称为钩链的结果如下

在这个钩链中保存的就是设置的各种钩子函数,而这些钩子函数会比应用程序还早接收到消息并对消息进行处理。所以程序员可以通过在钩子中设置钩子函数,而要设置钩子函数就需要使用SetWindowHookEx来将钩子函数安装到钩链中,函数在文档中的定义如下

 HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hMod, DWORD dwThreadId);
参数含义
idHook要安装的钩子类型,为了挂全局钩子,这里选择WH_GETMESSAGE。表示的是安装一个挂钩过程,它监视发送到消息队列的消息
lpfn表示的是钩子的回调函数。如果dwThreadId为0,则lpfn指向的钩子过程必须指向DLL中的钩子过程
hMod包含由lpfn参数执行的钩子过程的DLL句柄
dwThreadId与钩子过程关联的线程标识符,如果为0则表示与所有线程相关联。


如果函数成功,则返回钩子过程的句柄,否则为NULL。

根据上面的介绍可以得知,想要创建一个全局钩子,就必须在DLL文件中创建。这是因为进程的地址空间是独立的,发生对应事件的进程不能调用其他进程地址空间的钩子函数。如果钩子函数的实现代码在DLL中,则在对应事件发生时,系统会把这个DLL加载到发生事件的进程地址空间中,使它可以调用钩子函数进行处理。

所以只要在系统中安装了全局钩子,那么只要进程接收到可以发出钩子的消息,全局钩子的DLL就会被系统自动或者强行加载到进程空间中,这就可以实现DLL注入。

而这里之所以设置为WH_GETMESSAGE,是因为这种类型的钩子会监视消息队列,又因为Windows系统是基于消息驱动的,所以所有的进程都会有自己的一个消息队列,都会加载WH_GETMESSAGE类型的全局钩子。

当idHook设置为WH_GETMESSAGE的时候,回调函数lpfn的定义如下

LRESULT CALLBACK GetMsgProc(int code,
                            WPARAM wParam,
                            LPARAM lParam);
参数含义
code指定钩子过程是否必须处理该消息。如果代码是HC_ACTION,则钩子过程必须处理该消息。如果代码小于零,则钩子过程必须将消息传递给CallNextHookEx函数而无需进一步处理,并且应该返回CallNextHookEx返回的值
wParam

指定消息是否已从队列中删除。此参数可以是以下值之一。

PM_NOREMOVE:指定消息尚未从队列中删除

PM_REMOVE:指定消息已从队列中删除

lParam指向包含消息详细信息的MSG结构体的指针


如果要卸载钩子,则需要使用UnhookWindowsHookEx,该函数定义如下

BOOL UnhookWindowsHookEx(HHOOK hhk);
参数含义
hhk
需要卸载的钩子句柄。此参数是通过上一次调用SetWindowsHookEx获得的钩子句柄


由于设置全局钩子的代码需要在DLL文件中完成,所以首先需要新建一个InjectDll.cpp。

随后在文件中写入如下设置全局钩子的函数

extern HMODULE g_hDllModule;
// 设置全局钩子
BOOL SetGlobalHook()
{
	g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
	if (NULL == g_hHook)
	{
		return FALSE;
	}
	return TRUE;
}

其中的回调函数的实现如下

// 钩子回调函数
LRESULT GetMsgProc(
	int code,
	WPARAM wParam,
	LPARAM lParam)
{
	return CallNextHookEx(g_hHook, code, wParam, lParam);
}

这里只是简单的调用CallNextHookEx函数表示将当前钩子传递给钩链中的下一个钩子,第一个参数要指定当前钩子的句柄。如果直接返回0,则表示中断钩子传递,这就实现了对钩子进行拦截。

而g_hDllModule则是在DLL加载的时候被赋值的

当钩子不再使用,可以卸载掉全局钩子,这样此时已经包含钩子回调函数的DLL模块的进程就会释放DLL模块。卸载钩子的代码如下

// 卸载钩子
BOOL UnSetGlobalHook()
{
	if (g_hHook)
	{
		UnhookWindowsHookEx(g_hHook);
	}
	return TRUE;
}

上面的全局钩子的设置,钩子回调函数的实现以及全局钩子的卸载都需要使用到全局钩子的句柄。为了让任意一个独立的进程中对句柄的修改都可以影响到其他进程,就需要在DLL中使用共享内存的,来保证将DLL中加载到多个进程以后,一个进程对它的修改可以影响到其他进程。设置共享内存的方式如下

// 共享内存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")

而为了调用设置钩子和卸载钩子的函数,就需要创建一个.def文件来将两个函数导出

此时使用PEID查看InjectDll.dll可以看到导出表有如下的导出函数

接下来只要在代码中将DLL引入并或者对应的函数对它们进行调用就好

BOOL InjectDll(DWORD dwPid, CHAR szDllName[])
{
	BOOL bRet = TRUE;
	HMODULE hDll = NULL;
	pFnSetGlobalHook SetGlobalHook = NULL;
	pFnUnSetGlobalHook UnSetGlobalHook = NULL;

	hDll = LoadLibrary(szDllName);
	if (hDll == NULL)
	{
		ShowError("LoadLibrary");
		bRet = FALSE;
		goto exit;
	}

	SetGlobalHook = (pFnSetGlobalHook)GetProcAddress(hDll, "SetGlobalHook");
	if (SetGlobalHook == NULL)
	{
		ShowError("GetProcAddress SetGlobalHook");
		bRet = FALSE;
		goto exit;
	}

	if (!SetGlobalHook())
	{
		printf("钩子安装失败\n");
		bRet = FALSE;
		goto exit;
	}

	printf("钩子安装成功,按回车卸载钩子\n");
	system("pause");

	UnSetGlobalHook = (pFnUnSetGlobalHook)GetProcAddress(hDll, "UnSetGlobalHook");
	if (UnSetGlobalHook == NULL)
	{
		ShowError("GetProcAddress UnSetGlobalHook");
		bRet = FALSE;
		goto exit;
	}
	if (UnSetGlobalHook())
	{
		printf("已将全局钩子卸载\n");
	}
exit:
	return bRet;
}

八.实验结果

将编译好的exe文件和dll文件放到同一路径中,运行exe以后会在桌面生成一个result.txt文件。打开文件以后会看到里面的内容是被注入的进程的完整的路径名


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-11-19 16:44 被1900编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回