Windows DLL 注入技术
本文主要介绍四种常见的 Windows DLL 注入技术。
分别为全局钩子、远线程钩子、突破 SESSION 0 隔离的远线程注入和 APC 注入。
全局钩子注入
Windows 中大部分应用是基于 Windows 的消息机制,Windows提供截获这些消息的钩子函数。
根据钩子作用的不同范围,钩子可以被分为全局和局部钩子。局部钩子是针对某个线程的,全局钩子是只要有使用消息机制的应用。接下来我们主要来看下利用SetWindowsHookEx实现全局钩子。
SetWindowsHookEx函数介绍
HHOOK SetWindowsHookEx(int idHook, // 要安装的钩子程序的类型
HOOKPROC lpfn, // 指向钩子函数的指针
HINSTANCE hmod, // 指向钩子过程的DLL的句柄
DWORD dwThreadId // 与钩子进程关联的线程标识符
);
// 具体详细介绍可以查阅msdn文档,里面有更为详细的介绍
实现过程
首先我们需要创建一个windowsHookDll DLL,这个DLL在安装全局钩子后只要系统中的其他进程接收到可以发出钩子的消息,这个DLL就会被加载到此进程的地址空间中。这样便实现了Dll注入。
具体实现代码如下:
#include "Hook.h"
#include
extern HMODULE g_hDllModule;
// 共享内存
#pragma data_seg("mydata")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")
// 钩子回调函数
LRESULT GetMsgProc(int code,
WPARAM wParam,
LPARAM lParam){
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}
// 注册钩子
BOOL SetHook(){
g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0); //使用WH_GETMESSAGE 确保DLL能够注入所有的进程
if (g_hHook)
{
return TRUE;
}
return FALSE;
}
// 卸载钩子
BOOL UnSetHook(){
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook);
}
return TRUE;
}
试验结果
新建TestHook.exe程序,首先加载windowsHookDll.dll导出函数SetHook并调用。在注册成功后用ProcessExplorer.exe察看explorer.exe中的加载情况。
如果想要卸载可调用函数UnSetHook,执行完成后再观察explorer.exe是否加载了windowsHookDll.dll。
远线程注入
远线程注入是指一个进程在另一个进程中创建线程的技术。主要是利用LoadLibrary在所有进程空间中的地址是一样,进程在另一个进程中创建线程时传入LoadLibrary的地址和我们要注入的DLL的路径,这样在另一个进程中就能通过LoadLibray加载DLL到进程空间中。
说起来简单,但是实现起来却有几大难点需要注意的。
- 如何将DLL路径传给另一个进程,因为进程之间是相互隔离的,所以我们要在我们要注入的进程中申请内存。
- 需要对打开的进程进行提权,这就要求我们的注入程序要有管理员以上的权限。
函数介绍
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 进程访问权限
BOOL bInheritHandle, // 子进程是否继承此句柄
DWORD dwProcessId // 要打开的进程id
);
// 分配指定进程的内存
LPVOID VirtualAllocEx(
HANDLE hProcess, // 进程句柄
LPVOID lpAddress, // 要分配内存起始地址,为NULL则自动分配
SIZE_T dwSize, // 要分配的内存大小
DWORD flAllocationType,// 内存分配类型
DWORD flProtect // 内存保护(读写)
);
// 写入内存数据
BOOL WriteProcessMemory(
HANDLE hProcess, // 进程句柄
LPVOID lpBaseAddress, // 目标进程缓冲区地址
LPCVOID lpBuffer, // 要写入数据的地址
SIZE_T nSize, // 数据大小
SIZE_T *lpNumberOfBytesWritten // 写入数据的返回大小, 为NULL则忽略此参数
);
// 在另一个进程中创建线程
HANDLE CreateRemoteThread(
HANDLE hProcess, // 进程句柄
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程安全描述符
SIZE_T dwStackSize, // 堆栈的初始大小
LPTHREAD_START_ROUTINE lpStartAddress, // 函数地址(存在于另一个进程中)
LPVOID lpParameter, // 函数的参数
DWORD dwCreationFlags, // 线程创建标志
LPDWORD lpThreadId // 线程标识符
);
// 详细信息可参阅msdn
代码实现
/*dwProcessId: 目标进程的pid
**pszDllFileName: 要注入DLL的路径
*/
BOOL CreateRemoteThreadInjectDll(DWORD dwProcessId, char *pszDllFileName){
HANDLE hProcess = NULL;
SIZE_T dwSize = 0;
LPVOID pDllAddr = NULL;
FARPROC pFuncProcAddr = NULL;
// 打开注入进程,获取进程句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (NULL == hProcess)
{
EP_ShowError("OpenProcess");
return FALSE;
}
// 在注入进程中申请内存
dwSize = 1 + ::lstrlenA(pszDllFileName);
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (NULL == pDllAddr)
{
EP_ShowError("VirtualAllocEx");
return FALSE;
}
// 向申请的内存中写入数据
if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
{
EP_ShowError("WriteProcessMemory");
return FALSE;
}
// 获取LoadLibraryA函数地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
if (NULL == pFuncProcAddr)
{
EP_ShowError("GetProcAddress_LoadLibraryA");
return FALSE;
}
// 使用 CreateRemoteThread 创建远线程, 实现 DLL 注入
HANDLE hRemoteThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL);
if (NULL == hRemoteThread)
{
EP_ShowError("CreateRemoteThread");
return FALSE;
}
// 关闭句柄
::CloseHandle(hProcess);
return TRUE;
}
试验结果
远线程注册进阶 突破SESSION 0隔离
在系统普通进程中可以使用远线程注入DLL,但如果要注入系统服务则不行,因为有SESSION 0隔离。想要突破隔离需要使用ZwCreateThreadEx函数。这也是和远线程注入的区别。
ZwCreateThreadEx 是未公开的函数,在ntdll.dll中,它的函数声明如下:
#ifdef _WIN64
typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
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);
#else
typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
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);
#endif
具体实现代码:
// 使用 ZwCreateThreadEx 实现远线程注入
BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, char *pszDllFileName){
HANDLE hProcess = NULL;
SIZE_T dwSize = 0;
LPVOID pDllAddr = NULL;
FARPROC pFuncProcAddr = NULL;
HANDLE hRemoteThread = NULL;
DWORD dwStatus = 0;
// 打开注入进程,获取进程句柄
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (NULL == hProcess)
{
EP_ShowError("OpenProcess");
return FALSE;
}
// 在注入进程中申请内存
dwSize = 1 + ::lstrlenA(pszDllFileName);
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (NULL == pDllAddr)
{
EP_ShowError("VirtualAllocEx");
return FALSE;
}
// 向申请的内存中写入数据
if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
{
EP_ShowError("WriteProcessMemory");
return FALSE;
}
// 加载 ntdll.dll
HMODULE hNtdllDll = ::LoadLibraryA("ntdll.dll");
if (NULL == hNtdllDll)
{
EP_ShowError("LoadLirbary");
return FALSE;
}
// 获取LoadLibraryA函数地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
if (NULL == pFuncProcAddr)
{
EP_ShowError("GetProcAddress_LoadLibraryA");
return FALSE;
}
// 获取ZwCreateThread函数地址
#ifdef _WIN64
typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
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);
#else
typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
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);
#endif
typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
if (NULL == ZwCreateThreadEx)
{
EP_ShowError("GetProcAddress_ZwCreateThread");
return FALSE;
}
// 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
if (NULL == hRemoteThread)
{
EP_ShowError("ZwCreateThreadEx");
return FALSE;
}
// 关闭句柄
::CloseHandle(hProcess);
::FreeLibrary(hNtdllDll);
return TRUE;
}
试验结果
同远线程注入一致,只是在服务程序中不能弹窗。
APC注入
APC(Asynchronous Procedure Call)为异步过程调用,APC注入是指利用线程本身的APC队列进行DLL注入。
函数介绍
// 将异步调用函数添加到指定线程的APC队列中
DWORD QueueUserAPC(
PAPCFUNC pfnAPC, // 函数指针
HANDLE hThread, // 线程句柄
ULONG_PTR dwData // 函数参数
);
// 详细信息可参阅msdn
实现原理
APC队列中的函数需要等待线程挂起时才会被执行,所以要保证我们注入的程序能被执行需要将我们的函数插入到进程的所有线程中。具体代码实现如下:
// apc.h
#include
#include
// 根据进程名称获取PID
DWORD GetProcessIdByProcessName(char *pszProcessName);
// 根据PID获取所有的相应线程ID
BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, DWORD **ppThreadId, DWORD *dwThreadIdLength);
// APC注入
BOOL ApcInjectDll(char *pszProcessName, char *pszDllName);
//apc.cpp
#include "APC.h"
void ShowError(char *pszText){
char szErr[MAX_PATH] = { 0 };
::wsprintf(szErr, "%s Error[%d]\n", pszText);
::MessageBox(NULL, szErr, "ERROR", MB_OK | MB_ICONERROR);
}
// 根据进程名称获取PID
DWORD GetProcessIdByProcessName(char *pszProcessName){
DWORD dwProcessId = 0;
PROCESSENTRY32 pe32 = { 0 };
HANDLE hSnapshot = NULL;
BOOL bRet = FALSE;
::RtlZeroMemory(&pe32, sizeof(pe32));
pe32.dwSize = sizeof(pe32);
// 获取进程快照
hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (NULL == hSnapshot)
{
ShowError("CreateToolhelp32Snapshot");
return dwProcessId;
}
// 获取第一条进程快照信息
bRet = ::Process32First(hSnapshot, &pe32);
while (bRet)
{
// 获取快照信息
if (0 == ::lstrcmpi(pe32.szExeFile, pszProcessName))
{
dwProcessId = pe32.th32ProcessID;
break;
}
// 遍历下一个进程快照信息
bRet = ::Process32Next(hSnapshot, &pe32);
}
return dwProcessId;
}
// 根据PID获取所有的相应线程ID
BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, DWORD **ppThreadId, DWORD *pdwThreadIdLength){
DWORD *pThreadId = NULL;
DWORD dwThreadIdLength = 0;
DWORD dwBufferLength = 1000;
THREADENTRY32 te32 = { 0 };
HANDLE hSnapshot = NULL;
BOOL bRet = TRUE;
do
{
// 申请内存
pThreadId = new DWORD[dwBufferLength];
if (NULL == pThreadId)
{
ShowError("new");
bRet = FALSE;
break;
}
::RtlZeroMemory(pThreadId, (dwBufferLength * sizeof(DWORD)));
// 获取线程快照
::RtlZeroMemory(&te32, sizeof(te32));
te32.dwSize = sizeof(te32);
hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (NULL == hSnapshot)
{
ShowError("CreateToolhelp32Snapshot");
bRet = FALSE;
break;
}
// 获取第一条线程快照信息
bRet = ::Thread32First(hSnapshot, &te32);
while (bRet)
{
// 获取进程对应的线程ID
if (te32.th32OwnerProcessID == dwProcessId)
{
pThreadId[dwThreadIdLength] = te32.th32ThreadID;
dwThreadIdLength++;
}
// 遍历下一个线程快照信息
bRet = ::Thread32Next(hSnapshot, &te32);
}
// 返回
*ppThreadId = pThreadId;
*pdwThreadIdLength = dwThreadIdLength;
bRet = TRUE;
} while (FALSE);
if (FALSE == bRet)
{
if (pThreadId)
{
delete[]pThreadId;
pThreadId = NULL;
}
}
return bRet;
}
// APC注入
BOOL ApcInjectDll(char *pszProcessName, char *pszDllName){
BOOL bRet = FALSE;
DWORD dwProcessId = 0;
DWORD *pThreadId = NULL;
DWORD dwThreadIdLength = 0;
HANDLE hProcess = NULL, hThread = NULL;
PVOID pBaseAddress = NULL;
PVOID pLoadLibraryAFunc = NULL;
SIZE_T dwRet = 0, dwDllPathLen = 1 + ::lstrlen(pszDllName);
DWORD i = 0;
do
{
// 根据进程名称获取PID
dwProcessId = GetProcessIdByProcessName(pszProcessName);
if (0 >= dwProcessId)
{
bRet = FALSE;
break;
}
// 根据PID获取所有的相应线程ID
bRet = GetAllThreadIdByProcessId(dwProcessId, &pThreadId, &dwThreadIdLength);
if (FALSE == bRet)
{
bRet = FALSE;
break;
}
// 打开注入进程
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (NULL == hProcess)
{
ShowError("OpenProcess");
bRet = FALSE;
break;
}
// 在注入进程空间申请内存
pBaseAddress = ::VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == pBaseAddress)
{
ShowError("VirtualAllocEx");
bRet = FALSE;
break;
}
// 向申请的空间中写入DLL路径数据
::WriteProcessMemory(hProcess, pBaseAddress, pszDllName, dwDllPathLen, &dwRet);
if (dwRet != dwDllPathLen)
{
ShowError("WriteProcessMemory");
bRet = FALSE;
break;
}
// 获取 LoadLibrary 地址
pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (NULL == pLoadLibraryAFunc)
{
ShowError("GetProcessAddress");
bRet = FALSE;
break;
}
// 遍历线程, 插入APC
i = dwThreadIdLength;
for (; i > 0; --i)
{
// 打开线程
hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadId[i]);
if (hThread)
{
// 插入APC
::QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pBaseAddress);
// 关闭线程句柄
::CloseHandle(hThread);
hThread = NULL;
}
}
bRet = TRUE;
} while (FALSE);
// 释放内存
if (hProcess)
{
::CloseHandle(hProcess);
hProcess = NULL;
}
if (pThreadId)
{
delete[]pThreadId;
pThreadId = NULL;
}
return bRet;
}
总结
Windows注入技术可以方便我们对目标进程作修改,但是也可能使目标进程崩溃,在使用的时候需要小心谨慎。
- END -