远线程注入是指一个进程在另一个进程中创建线程的技术,是一种病毒木马所青睐的注入技术,同时也是一种很巧妙、很经典的DLL注入技术。
远线程注入主要有以下几个步骤
1.OpenProcess函数
打开现有的本地线程对象
函数声明
HANDLE WINAPI OpenProcess(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwProcessId
);
2.VirtualAllocEx函数
向我们要指定的进程虚拟地址空间中申请内存
LPVOID WINAPI VirtualAllocEx(
_In_ HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_ SIZE_T dwSize,
_In_ DWORD flAllocationType,
_In_ DWORD flProtect
);
3.WriteProcessMemory函数
在指定的进程中将数据写入内存区域。 要写入的整个区域必须可访问或操作失败。
函数声明
BOOL WINAPI WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_ LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_ SIZE_T *lpNumberOfBytesWritten
);
4.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
);
5.实现原理
远线程注入 DLL 中的远线程,是因为使用的关键函数是 CreateRemoteThread,来在其它的进程空间中创建一个线程。那么,它为何能够使其它进程加载一个 DLL,实现 DLL 注入呢?接下来我就为大家一一分析。
首先,我们加载一个 DLL,通常使用 LoadLibrary 函数来实现 DLL 的动态加载。那么,先来看下 LoadLibrary 函数的声明:
HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName
);
从上面的函数声明可以知道,LoadLibrary 函数的参数只有一个,传递的是要加载的 DLL 的路径字符串。
然后,我们再看下创建远线程的函数 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
);
我们可以从声明中知道,CreateRemoteThread 需要传递的是目标进程空间的多线程函数地址,以及多线程的参数。
接下来,我们将上述两者结合,开始大胆设想一下,如果,我们能够获取目标进程的 LoadLibrary 函数的地址,而且还能够获取目标进程空间中某个 DLL 路径的字符串地址,那么,将LoadLibrary 函数的地址作为多线程函数的地址,某个 DLL 路径字符串作为多线程函数的参数,传递给 CreateRemoteThread 函数在目标进程空间创建一个多线程,这样能不能创建成功呢?答案是可以的。这样,就可以在目标进程空间中创建一个多线程,这个多线程就是 LoadLibrary 函数加载 DLL。
那么,这样远线程注入的原理大概就了解了吧。那么要实现远线程注入 DLL,还需要解决以下两个问题:一是目标进程空间的 LoadLibrary 函数地址是多少呢?二是如何向目标进程空间中写入 DLL 路径字符串数据呢?
对于第一个问题,我们知道由于机制随机化ASLR(Address space layout randomization),导致每次开机时加载的系统 DLL 的加载基址都不一样,从而导致了 DLL 的导出函数的地址也都不一样。即使如此,但要注意一个关键的一个知识点就是:
有些系统DLL中的指令是Position dependent的,要求所有进程中必须一致。比如kernel32中的新线程入口,ntdll中的异常处理入口等。其实这个地址只是要求系统启动之后必须固定,如果系统重新启动,其地址可以不同。
Copy-On-Write机制,不改多进程共享,改写内容的页系统会立即给当前进程复制一份,这样这个地址与其它进程中相同地址所映射的物理内存就不同了,怎么改都不会影响其它进程。
也就是说,虽然不同进程,但是其 Kernel32.dll 的加载基址是相同的,也就是说,自己程序空间的 LoadLibrary 函数地址和其它进程空间的 LoadLibrary 函数地址相同。
对于第二个问题,我们可以直接调用 VirtualAllocEx 函数在目标进程空间中申请一块内存,然后再调用 WriteProcessMemory 函数将指定的 DLL 路径写入到目标进程空间中。
这样,我们就可以调用 CreateRemoteThread 函数,实现远线程注入DLL了。
6.核心代码
void ShowError(char *pszText)
{
char szErr[MAX_PATH] = {0};
::wsprintf(szErr, "%s Error[%d]\n", pszText, ::GetLastError());
::MessageBox(NULL, szErr, "ERROR", MB_OK);
}
// 使用 CreateRemoteThread 实现远线程注入
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)
{
ShowError("OpenProcess");
return FALSE;
}
// 在注入进程中申请内存
dwSize = 1 + ::lstrlen(pszDllFileName);
pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (NULL == pDllAddr)
{
ShowError("VirtualAllocEx");
return FALSE;
}
// 向申请的内存中写入数据
if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
{
ShowError("WriteProcessMemory");
return FALSE;
}
// 获取LoadLibraryA函数地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (NULL == pFuncProcAddr)
{
ShowError("GetProcAddress_LoadLibraryA");
return FALSE;
}
// 使用 CreateRemoteThread 创建远线程, 实现 DLL 注入
HANDLE hRemoteThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL);
if (NULL == hRemoteThread)
{
ShowError("CreateRemoteThread");
return FALSE;
}
// 关闭句柄
::CloseHandle(hProcess);
return TRUE;
}
7.注意事项
传统的远线程注入DLL方法并不能突破SESSION0隔离。并且需要将我们的进程提权。否则权限不够不能在其他进程中创建新的线程。