0x01.前言
提到Dll的注入,立马能够想到的方法就有很多,比如利用远程线程、Apc等等,这里我对Ring3层的Dll注入学习做一个总结吧。
我把注入的方法分成六类,分别是:1.创建新线程、2.设置线程上下背景文,修改寄存器、3.插入Apc队列、4.修改注册表、5.挂钩窗口消息、6.远程手动实现LoadLibrary。
那么下面就开始学习之旅吧!
0x02.预备工作
在涉及到注入的程序中,提升程序的权限自然是必不可少的,这里我提供了两个封装的函数,都可以用于提权。第一个是通过权限令牌来调整权限;第二个是通过ntdll.dll的导出的未文档化函数RtlAdjustPrivilege来调整权限。
// 传入参数 SE_DEBUG_NAME,提升到调试权限
BOOL GrantPriviledge(WCHAR* PriviledgeName)
{
TOKEN_PRIVILEGES TokenPrivileges, OldPrivileges;
DWORD dwReturnLength = sizeof(OldPrivileges);
HANDLE TokenHandle = NULL;
LUID uID;
// 打开权限令牌
if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &TokenHandle))
{
if (GetLastError() != ERROR_NO_TOKEN)
{
return FALSE;
}
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &TokenHandle))
{
return FALSE;
}
}
if (!LookupPrivilegeValue(NULL, PriviledgeName, &uID)) // 通过权限名称查找uID
{
CloseHandle(TokenHandle);
return FALSE;
}
TokenPrivileges.PrivilegeCount = 1; // 要提升的权限个数
TokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 动态数组,数组大小根据Count的数目
TokenPrivileges.Privileges[0].Luid = uID;
// 在这里我们进行调整权限
if (!AdjustTokenPrivileges(TokenHandle, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), &OldPrivileges, &dwReturnLength))
{
CloseHandle(TokenHandle);
return FALSE;
}
// 成功了
CloseHandle(TokenHandle);
return TRUE;
}
紧接着,既然我们要对目标进程注入Dll,那么获得目标进程的Id是不可或缺的吧,因为OpenProcess是肯定会使用的,这里我也提供了两种通过目标进程映像名称获得进程Id的方法。第一种是最常见的使用TlHelp创建系统的进程快照;第二种是借助Psapi枚举系列函数,不过这个方法我实现的有缺憾,32位下不能得到64位进程的Id。
// 使用ToolHelp系列函数
#include
BOOL GetProcessIdByProcessImageName(IN PWCHAR wzProcessImageName, OUT PUINT32 ProcessId)
{
HANDLE ProcessSnapshotHandle = INVALID_HANDLE_VALUE;
PROCESSENTRY32 ProcessEntry32 = { 0 };
ProcessEntry32.dwSize = sizeof(PROCESSENTRY32); // 初始化PROCESSENTRY32结构
ProcessSnapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); // 给系统所有的进程快照
if (ProcessSnapshotHandle == INVALID_HANDLE_VALUE)
{
return FALSE;
}
if (Process32First(ProcessSnapshotHandle, &ProcessEntry32)) // 找到第一个
{
do
{
if (lstrcmpi(ProcessEntry32.szExeFile, wzProcessImageName) == 0) // 不区分大小写
{
*ProcessId = ProcessEntry32.th32ProcessID;
break;
}
} while (Process32Next(ProcessSnapshotHandle, &ProcessEntry32));
}
CloseHandle(ProcessSnapshotHandle);
ProcessSnapshotHandle = INVALID_HANDLE_VALUE;
if (*ProcessId == 0)
{
return FALSE;
}
return TRUE;
}
// 使用Psapi系列枚举函数
#include
BOOL GetProcessIdByProcessImageName(IN PWCHAR wzProcessImageName, OUT PUINT32 ProcessId)
{
DWORD dwProcessesId[1024] = { 0 };
DWORD BytesReturned = 0;
UINT32 ProcessCount = 0;
// 获得当前操作系统中的所有进程Id,保存在dwProcessesId数组里
if (!EnumProcesses(dwProcessesId, sizeof(dwProcessesId), &BytesReturned))
{
return FALSE;
}
ProcessCount = BytesReturned / sizeof(DWORD);
// 遍历
for (INT i = 0; i < ProcessCount; i++)
{
HMODULE ModuleBase = NULL;
WCHAR wzModuleBaseName[MAX_PATH] = { 0 };
HANDLE ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessesId[i]);
if (ProcessHandle == NULL)
{
continue;
}
if (EnumProcessModulesEx(ProcessHandle, &ModuleBase, sizeof(HMODULE), &BytesReturned, LIST_MODULES_ALL))
{
// 获得进程第一模块名称
GetModuleBaseName(ProcessHandle, ModuleBase, wzModuleBaseName, MAX_PATH * sizeof(WCHAR));
}
CloseHandle(ProcessHandle);
ProcessHandle = NULL;
if (lstrcmpi(wzModuleBaseName, wzProcessImageName) == 0) // 不区分大小写
{
*ProcessId = dwProcessesId[i];
break;
}
}
if (*ProcessId == 0)
{
return FALSE;
}
return TRUE;
}
然后在比如插入Apc队列、挂起线程等等操作中,需要对目标进程的线程操作,所以获得线程Id也有必要,同样的我也提供了两种通过进程Id获得线程Id的方法。第一个仍然是使用TlHelp创建系统的线程快照,把所有的线程存入vector模板里(供Apc注入使用);第二个是利用ZwQuerySystemInformation大法,枚举系统进程信息,这个方法我只返回了一个线程Id,已经够用了。
// 枚举指定进程Id的所有线程,压入模板中
#include
#include
using namespace std;
BOOL GetThreadIdByProcessId(IN UINT32 ProcessId, OUT vector& ThreadIdVector)
{
HANDLE ThreadSnapshotHandle = NULL;
THREADENTRY32 ThreadEntry32 = { 0 };
ThreadEntry32.dwSize = sizeof(THREADENTRY32);
ThreadSnapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); // 给系统所有的线程快照
if (ThreadSnapshotHandle == INVALID_HANDLE_VALUE)
{
return FALSE;
}
if (Thread32First(ThreadSnapshotHandle, &ThreadEntry32))
{
do
{
if (ThreadEntry32.th32OwnerProcessID == ProcessId)
{
ThreadIdVector.emplace_back(ThreadEntry32.th32ThreadID); // 把该进程的所有线程id压入模板
}
} while (Thread32Next(ThreadSnapshotHandle, &ThreadEntry32));
}
CloseHandle(ThreadSnapshotHandle);
ThreadSnapshotHandle = NULL;
return TRUE;
}
// ZwQuerySystemInformation+SystemProcessInformation
typedef
NTSTATUS(NTAPI * pfnZwQuerySystemInformation)(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN UINT32 SystemInformationLength,
OUT PUINT32 ReturnLength OPTIONAL);
BOOL GetThreadIdByProcessId(IN UINT32 ProcessId, OUT PUINT32 ThreadId)
{
BOOL bOk = FALSE;
NTSTATUS Status = 0;
PVOID BufferData = NULL;
PSYSTEM_PROCESS_INFO spi = NULL;
pfnZwQuerySystemInformation ZwQuerySystemInformation = NULL;
ZwQuerySystemInformation = (pfnZwQuerySystemInformation)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwQuerySystemInformation");
if (ZwQuerySystemInformation == NULL)
{
return FALSE;
}
BufferData = malloc(1024 * 1024);
if (!BufferData)
{
return FALSE;
}
// 在QuerySystemInformation系列函数中,查询SystemProcessInformation时,必须提前申请好内存,不能先查询得到长度再重新调用
Status = ZwQuerySystemInformation(SystemProcessInformation, BufferData, 1024 * 1024, NULL);
if (!NT_SUCCESS(Status))
{
free(BufferData);
return FALSE;
}
spi = (PSYSTEM_PROCESS_INFO)BufferData;
// 遍历进程,找到我们的目标进程
while (TRUE)
{
bOk = FALSE;
if (spi->UniqueProcessId == (HANDLE)ProcessId)
{
bOk = TRUE;
break;
}
else if (spi->NextEntryOffset)
{
spi = (PSYSTEM_PROCESS_INFO)((PUINT8)spi + spi->NextEntryOffset);
}
else
{
break;
}
}
if (bOk)
{
for (INT i = 0; i < spi->NumberOfThreads; i++)
{
// 返出找到的线程Id
*ThreadId = (UINT32)spi->Threads[i].ClientId.UniqueThread;
break;
}
}
if (BufferData != NULL)
{
free(BufferData);
}
return bOk;
}
嗯,目前为止,预备工作差不多完工,那我们就开始正题吧!
0x03.注入方法一 -- 创建新线程
创建新线程,也就是在目标进程里,创建一个线程为我们服务,而创建线程的方法我找到的有三种:1.CreateRemoteThread;2.NtCreateThreadEx;3.RtlCreateUserThread。
基本思路是:1.在目标进程内存空间申请内存;2.在刚申请的内存中写入Dll完整路径;3.创建新线程,去执行LoadLibrary,从而完成注入Dll。
ps:这里直接使用从自己加载的kernel32模块导出表中获得LoadLibrary地址,是因为一般情况下,所有进程加载这类系统库在内存中的地址相同!
因为只是创线程所使用的函数不一样,所以下面的代码随便放开一个创线程的步骤,屏蔽其他两个,都是可以成功的,这里我放开的是NtCreateThreadEx。
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer);
#define NT_SUCCESS(x) ((x) >= 0)
typedef struct _CLIENT_ID {
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
typedef NTSTATUS(NTAPI * pfnRtlCreateUserThread)(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits OPTIONAL,
IN SIZE_T StackReserve OPTIONAL,
IN SIZE_T StackCommit OPTIONAL,
IN PTHREAD_START_ROUTINE StartAddress,
IN PVOID Parameter OPTIONAL,
OUT PHANDLE ThreadHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL);
BOOL InjectDll(UINT32 ProcessId)
{
HANDLE ProcessHandle = NULL;
ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
// 在对方进程空间申请内存,存储Dll完整路径
UINT32 DllFullPathLength = (strlen(DllFullPath) + 1);
PVOID DllFullPathBufferData = VirtualAllocEx(ProcessHandle, NULL, DllFullPathLength, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (DllFullPathBufferData == NULL)
{
CloseHandle(ProcessHandle);
return FALSE;
}
// 将DllFullPath写进刚刚申请的内存中
SIZE_T ReturnLength;
BOOL bOk = WriteProcessMemory(ProcessHandle, DllFullPathBufferData, DllFullPath, strlen(DllFullPath) + 1, &ReturnLength);
LPTHREAD_START_ROUTINE LoadLibraryAddress = NULL;
HMODULE Kernel32Module = GetModuleHandle(L"Kernel32");
LoadLibraryAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(Kernel32Module, "LoadLibraryA");
pfnNtCreateThreadEx NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtCreateThreadEx");
if (NtCreateThreadEx == NULL)
{
CloseHandle(ProcessHandle);
return FALSE;
}
HANDLE ThreadHandle = NULL;
// 0x1FFFFF #define THREAD_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFFF)
NtCreateThreadEx(&ThreadHandle, 0x1FFFFF, NULL, ProcessHandle, (LPTHREAD_START_ROUTINE)LoadLibraryAddress, DllFullPathBufferData, FALSE, NULL, NULL, NULL, NULL);
/*
pfnRtlCreateUserThread RtlCreateUserThread = (pfnRtlCreateUserThread)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "RtlCreateUserThread");
HANDLE ThreadHandle = NULL;
NTSTATUS Status = RtlCreateUserThread(ProcessHandle, NULL, FALSE, 0, 0, 0, LoadLibraryAddress, DllFullPathBufferData, &ThreadHandle, NULL);
*/
/*
HANDLE ThreadHandle = CreateRemoteThread(ProcessHandle, NULL, 0, LoadLibraryAddress, DllFullPathBufferData, 0, NULL); // CreateRemoteThread 函数
*/
if (ThreadHandle == NULL)
{
CloseHandle(ProcessHandle);
return FALSE;
}
if (WaitForSingleObject(ThreadHandle, INFINITE) == WAIT_FAILED)
{
return FALSE;
}
CloseHandle(ProcessHandle);
CloseHandle(ThreadHandle);
return TRUE;
}
0x04.注入方法二 -- 设置线程上下背景文
设置线程上下背景文的主要目的是让目标进程的某一线程转去执行我们的代码,然后再回来做他该做的事,而我们的代码,就是一串由汇编硬编码组成的ShellCode。
这串ShellCode做了三件事:1.传入Dll完整路径参数;2.呼叫LoadLibrary函数地址;3.返回原先的Eip或Rip。
这里我选用的呼叫指令是ff 15 和 ff 25,在32位下为跳转到15(25)指令后面字节码对应地址里面存放的地址,在64位下15(25)指令后面四字节存放的是偏移,该跳转为跳转到换算出来的地址里面存放的地址,这里我把偏移写成0,以便于计算。
#ifdef _WIN64
// 测试 64 位 dll被注,Bug已修复
/*</