上期说了4种常用注入中的2种,今天说下后两种,即“Session 0隔离的远线程注入”和“APC注入”(文章最后提供代码下载地址)。
一、Session 0 隔离的远线程注入
1、什么是 Session 0 隔离?
在Windows XP、Windows Server 2003 或早期Windows 系统时代,当第一个用户登录系统后服务和应用程序是在同一个Session 中运行的。这就是Session 0 如下图所示:
但是这种运行方式提高了系统安全风险,因为服务是通过提升了用户权限运行的,而应用程序往往是那些不具备管理员身份的普通用户运行的,其中的危险显而易见。
从Vista 开始Session 0 中只包含系统服务,其他应用程序则通过分离的Session 运行,将服务与应用程序隔离提高系统的安全性。如下图所示:
我们来看看实际的程序所属的Session:
这是services.exe,明显属于Session 0;
这是360se.exe,明显属于Session 1;
看到两者的区别了吧,一是系统服务,一是应用程序。
2、ZwCreateThreadEx函数
由于Session 0隔离机制,导致远程线程注入系统服务进程失效,最终发现用ZwCreateThreadEx函数进行远程线程注入可以突破Session 0 隔离,成功注入。
ZwCreateThreadEx函数是比CreateRemoteThread函数更为底层的函数,将Dll成功注入到Session 0隔离的系统服务进程中,由于ZwCreateThreadEx函数没有在ntdll.dll中声明,所以需要使用GetProcAddress从ntdll.dll中获取该函数的导出地址。
64 位下,ZwCreateThreadEx函数声明为:
DWORD WINAPI 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);
32 位下,ZwCreateThreadEx 函数声明为:
DWORD WINAPI 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);
要使系统服务进程远线程注入成功,需要将函数的第7个参数CreateSuspended的值设置为0,这样线程创建完成后就会恢复运行,成功注入。
3、代码学习
// 使用 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;
// 打开注入进程,获取进程句柄(略)
// 在注入进程中申请内存(略)
// 向申请的内存中写入数据(略)
// 加载 ntdll.dll
HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll");
// 获取LoadLibraryA函数地址
pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
// 获取ZwCreateThread函数地址
typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
// 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入
dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
// 关闭句柄(略)
return TRUE;
}
4、测试注入Svchost.exe中:
它是属于Session 0;
找到svchost.exe的PID=10028,写入程序中,Vs2019编译成功 ZwCreateThreadEx_Test.exe 和 TestDll.dll;
管理员权限运行ZwCreateThreadEx_Test.exe,
在PID=10028的svchost.exe中注入了TestDll.dll文件;说明成功注入Session 0。
二、APC注入
1、什么是APC?
APC:Asynchronous Procedure Call,异步过程调用,指函数在特定线程中被异步执行,是一种并发机制,常用于异步IO或定时器。
APC队列:每个线程都有一个APC队列,在线程处于可警醒状态时,线程会执行APC队列中APC函数。
2、APC 在什么时候调用?
1)线程已经创建,系统在调用线程函数时会检查APC队列,如果不为空,则调用APC队列中的APC函数,直到队列为空后,才开始调用线程函数。
2)线程通过WaitForSingleObjectEx等函数进入可警醒状态时,会先检查APC队列。如果不为空,则调用APC队列中的 APC函数。
直到队列为空后,才开始等待要等待的对象,此时如果要等待的对象没有进入激发态且没有超时,WaitForSingleObjectEx 不会返回。
3)线程通过WaitForSingleObjectEx等函数进入可警醒状态时,APC队列为空,且要等待的对象没有进入激发态,也没有超时,则线程进入睡眠等待状态。
此时往该APC队列添加APC函数后,该线程会被唤醒执行完所有APC队列中的函数,然后不去看要等待的对象是否进入激发态,立即从WaitForSingleObjectEx中返回,返回值是 WAIT_IO_COMPLETION。
往线程APC队列添加APC,系统会产生一个软中断。在线程下一次被调度的时候就会执行APC函数,APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的 APC被称为用户模式APC。
3、代码
// 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);
// 根据PID获取所有的相应线程ID
bRet = GetAllThreadIdByProcessId(dwProcessId, &pThreadId, &dwThreadIdLength);
// 打开注入进程
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
// 在注入进程空间申请内存
pBaseAddress = ::VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 向申请的空间中写入DLL路径数据
::WriteProcessMemory(hProcess, pBaseAddress, pszDllName, dwDllPathLen, &dwRet);
// 获取 LoadLibrary 地址
pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
// 遍历线程, 插入APC
for (i = 0; i < dwThreadIdLength; 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);
// 释放内存(略)
return bRet;
}
这里用到一个非常重要的函数:QueueUserAPC,把一个APC对象加入到指定线程的APC队列中。
QueueUserAPC:
第1个参数:指向应用程序提供的APC函数的指针;
第2个参数:插入APC的线程句柄;
第3个参数:传递给执行函数的参数。
与远线程注入类似,如果QueueUserAPC函数的第一个参数,即函数地址设置的是LoadLibraryA函数地址,第三个参数,即传递参数设置的是DLL的路径。那么,当执行APC的时候,便会调用LoadLibraryA函数加载指定路径的DLL,完成DLL注入操作。
一个进程中,包含有多个线程,为了确保插入的APC能够被执行,所以,向目标进程的所有线程都插入相同的APC,实现加载DLL的操作。这样,只要进程中任意线程被唤醒,开始执行APC的时候,便会执行插入的APC,实现DLL注入。
那么,实现APC注入的具体流程如下所示。
首先,通过OpenProcess函数打开目标进程,获取目标进程的句柄。
然后,通过调用WIN32 API函数CreateToolhelp32Snapshot、Thread32First以及Thread32Next遍历线程快照,获取目标进程的所有线程ID。
接着,调用VirtualAllocEx函数在目标进程中申请一块内存,并通过WriteProcessMemory函数向内存中写入注入的DLL路径。
最后,遍历上述获取的线程ID,并调用OpenThread函数以THREAD_ALL_ACCESS访问权限打开线程,获取线程句柄。并调用QueueUserAPC函数向线程插入APC 函数,设置APC函数的地址为LoadLibraryA函数的地址,并设置APC函数参数为上述DLL路径地址。
经过上述操作,便可完成APC注入操作。只要目标进程中任意线程被唤醒,便会执行APC,完成注入DLL操作。
4、测试
在Win10 x64上,管理员权限运行APC_Test.exe,成功向explorer.exe进行了APC注入,注入完成后TestDll.dll进行了弹窗显示。
APC注入原理是利用当线程被唤醒时APC中的注册函数会执行的机制,并以此去执行Dll加载代码,进而完成Dll注入。为了增加APC执行的可能性,应向目标进程中所有的线程都插入APC。如果出现向指定进程的所有线程插入APC导致进程崩溃的问题,则可以采取倒序遍历线程ID的方式进行倒序插入来解决程序崩溃的问题。
至此,4种Dll常见的注入编程技术讲解完毕,希望结合代码来看文章。
本期代码地址:
链接:https://pan.baidu.com/s/1-ubqlbo1eh3u78u0RwqyHA
提取码:oev1