远程线程注入DLL
远程线程注入
0x00 前言
远程线程注入是一种经典的DLL注入技术。其实就是指一个新进程中另一个进程中创建线程的技术。
0x01 介绍
1.远程线程注入原理
画了一个图大致理解了下远程线程注入dll的原理。
如果是实现注入dll的话,流程大致就是:
通过OpenProcess获取目标进程句柄。
通过VirtualAllocEx在目标进程空间中申请内存,通过WriteProcessMemory放入需要载入的dll的路径。
通过GetModuleHandleA获取诸如kernel32.dll这类系统dll的模块句柄,进而获取LoadLibraryA这类载入动态链接库的函数地址(固定)
通过CreateRemoteThread的参数传入目标进程对象句柄、写入到目标进程空间的dll路径、LoadLibraryA函数地址,实现中目标中创建多线程加载dll。
2.函数介绍
OpenProcess 函数
打开现有的本地进程对象。
函数声明
HANDLE WINAPI OpenProcess( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ DWORD dwProcessId);
参数
- dwDesiredAccess [in]
访问进程对象。此访问权限针对进程的安全描述符进行检查。此参数可以是一个或多个进程访问权限。如果调用该函数的进程启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。- bInheritHandle [in]
若此值为TRUE,则此进程创建的进程将继承该句柄。否则,进程不会继承此句柄。- dwProcessId [in]
要打开的本地进程的标识符。
如果指定的进程是系统进程(0x00000000),则该函数失败,最后一个错误代码为ERROR_INVALID_PARAMETER。如果指定的进程是空闲进程或CSRSS进程之一,则此功能将失败,并且最后一个错误代码为ERROR_ACCESS_DENIED,因为它们的访问限制会阻止用户级代码打开它们。
如果您使用GetCurrentProcessId作为此函数的参数,请考虑使用GetCurrentProcess而不是OpenProcess,以提高性能。返回值
- 如果函数成功,则返回值是指定进程的打开句柄。
- 如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。
VirtualAllocEx 函数
在指定进程的虚拟地址空间内保留,提交或更改内存区域的状态。 该函数初始化其分配给零的内存。
函数声明
LPVOID WINAPI VirtualAllocEx( _In_ HANDLE hProcess, _In_opt_ LPVOID lpAddress, _In_ SIZE_T dwSize, _In_ DWORD flAllocationType, _In_ DWORD flProtect);
参数
- hProcess [in]
过程的句柄。该函数在该进程的虚拟地址空间内分配内存。
句柄必须具有PROCESS_VM_OPERATION权限。有关更多信息,请参阅流程安全和访问权限。- lpAddress [in]
指定要分配的页面的所需起始地址的指针。
如果您正在保留内存,则该函数会将该地址舍入到分配粒度的最接近的倍数。
如果您提交已经保留的内存,该功能会将该地址舍入到最接近的页面边界。要SESSION 0 隔离页面的大小和主机上的分配粒度,请使用GetSystemInfo函数。
如果lpAddress为NULL,则该函数确定在哪里分配该区域。- dwSize [in]
要分配的内存大小,以字节为单位。
如果lpAddress为NULL,则函数将dwSize循环到下一个页面边界。
如果lpAddress不为NULL,则该函数将从lpAddress到lpAddress + dwSize的范围内分配包含一个或多个字节的所有页面。这意味着,例如,跨越页面边界的2字节范围会导致功能分配两个页面。- flAllocationType [in]
内存分配类型。此参数必须包含以下值之一:
VALUE MEANING MEM_COMMIT 为指定的预留内存页分配内存费用(从磁盘上的内存和分页文件的总体大小)。 该函数还保证当调用者稍后初次访问存储器时,内容将为零。 除非/直到虚拟地址被实际访问,实际的物理页面才被分配 MEM_RESERVE 保留进程的虚拟地址空间的范围,而不会在内存或磁盘上的分页文件中分配任何实际物理存储 MEM_RESET 表示由lpAddress和dwSize指定的内存范围内的数据不再受关注。 页面不应从页面文件中读取或写入页面文件。 然而,内存块将在以后再次被使用,所以不应该被分解。 该值不能与任何其他值一起使用 MEM_RESET_UNDO 只能在早期成功应用了MEM_RESET的地址范围上调用MEM_RESET_UNDO。 它指示由lpAddress和dwSize指定的指定内存范围内的数据对呼叫者感兴趣,并尝试反转MEM_RESET的影响。 如果功能成功,则表示指定地址范围内的所有数据都是完整的。 如果功能失败,地址范围中的至少一些数据已被替换为零
- flProtect [in]
要分配的页面区域的内存保护。 如果页面被提交,您可以指定任何一个内存保护常量。
如果lpAddress指定了一个地址,flProtect不能是以下值之一:
PAGE_NOACCESS
PAGE_GUARD
PAGE_NOCACHE
PAGE_WRITECOMBINE返回值
- 如果函数成功,则返回值是分配的页面区域的基址。
- 如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。
WriteProcessMemory 函数
在指定的进程中将数据写入内存区域。 要写入的整个区域必须可访问或操作失败。
函数声明
BOOL WINAPI WriteProcessMemory( _In_ HANDLE hProcess, _In_ LPVOID lpBaseAddress, _In_ LPCVOID lpBuffer, _In_ SIZE_T nSize, _Out_ SIZE_T *lpNumberOfBytesWritten);
参数
- hProcess [in]
要修改的进程内存的句柄。 句柄必须具有PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问进程。- lpBaseAddress [in]
指向写入数据的指定进程中的基地址的指针。 在数据传输发生之前,系统会验证指定大小的基地址和内存中的所有数据是否可以进行写入访问,如果不可访问,则该函数将失败。- lpBuffer [in]
指向缓冲区的指针,其中包含要写入指定进程的地址空间的数据。- nSize [in]
要写入指定进程的字节数。- lpNumberOfBytesWritten [out]
指向变量的指针,该变量接收传输到指定进程的字节数。 此参数是可选的。 如果lpNumberOfBytesWritten为NULL,则忽略该参数。返回值
- 如果函数成功,则返回值不为零。
- 如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。
CreateRemoteThread 函数
创建在另一个进程的虚拟地址空间中运行的线程。
使用CreateRemoteThreadEx函数创建在另一个进程的虚拟地址空间中运行的线程,并可选地指定扩展属性。函数声明
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 [in]
要创建线程的进程的句柄。 句柄必须具有PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE和PROCESS_VM_READ访问权限,如果某些平台上没有这些权限,可能会失败。 有关更多信息,请参阅流程安全和访问权限。- lpThreadAttributes [in]
指向SECURITY_ATTRIBUTES结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。 如果lpThreadAttributes为NULL,则线程将获得默认安全描述符,并且该句柄不能被继承。 线程的默认安全描述符中的访问控制列表(ACL)来自创建者的主令牌。dwStackSize [in]
堆栈的初始大小,以字节为单位。 系统将此值循环到最近的页面。 如果此参数为0(零),则新线程使用可执行文件的默认大小。 有关更多信息,请参阅线程堆栈大小。- lpStartAddress [in]
指向由线程执行的类型为LPTHREAD_START_ROUTINE的应用程序定义函数的指针,并表示远程进程中线程的起始地址。 该功能必须存在于远程进程中。 有关更多信息,请参阅ThreadProc。- lpParameter [in]
指向要传递给线程函数的变量的指针。- dwCreationFlags [in]
控制线程创建的标志。若是 0,则表示线程在创建后立即运行。- lpThreadId [out]
指向接收线程标识符的变量的指针。
如果此参数为NULL,则不返回线程标识符。返回值
- 如果函数成功,则返回值是新线程的句柄。
- 如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。
0x02 编码实现
测试demo,指定进程pid和dll位置。
TestCreatePId.cpp
// TestCreatePId.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <iostream>
void InjectDLL(DWORD PID, char* Path)
{
DWORD dwSize;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, PID);
dwSize = strlen(Path) + 1;
LPVOID lpParamAddress = VirtualAllocEx(hProcess, 0, dwSize, PARITY_SPACE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, lpParamAddress, (PVOID)Path, dwSize, NULL);
HMODULE hModule = GetModuleHandleA("kernel32.dll");
LPTHREAD_START_ROUTINE lpStartAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "LoadLibraryA");
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, lpStartAddress, lpParamAddress, 0, NULL);
WaitForSingleObject(hThread, 1000);
CloseHandle(hThread);
}
int main(int argc,char *argv[])
{
char* a = argv[2];
//std::cout << atoi(argv[1]) << a <<std::endl;
InjectDLL(int(argv[1]),a);
//InjectDLL(atoi(argv[1]), "c:\\test\\TestDll.dll");
return 0;
}
0x03 实测
经测试对应位数的dll可以加载到对应位数的进程中。
不过遇到个问题,这里也记录下,当想在同一进程载入两个dll,如果先载入的dll有dllmain中,里面的函数没有走完逻辑,那么第二个dll会在线程挂起结束之后载入到进程。比如我载入到第一个dll里面DLL_PROCESS_ATTACH有个MessageBox,我没给他结束窗口,第二个dll没有加载到进程中,而结束了MessageBox窗口才注入到了进程。
0x04 总结
部分进程需要以管理员权限才可注入到进程,具体原因是OpenProcess打开高权限进程会因权限不足无法打开,其实就是权限继承到原因,这里如果以powershell默认启动就会获得SE_DEBUG权限,而进程令牌权限提升准备再挑一篇文章单独总结学习。
另一个问题就是,不能成功注入到一些系统服务的进程,因为系统存在 SESSION 0 隔离。如果想向系统服务进程中注入,需要突破SESSION 0 隔离,这个也准备另启一篇文章做记录。