【翻译】Hiding In PlainSight - 使用Proxying DLL规避ETWTI堆栈追踪

admin 2023年1月31日10:42:12评论134 views字数 7273阅读24分14秒阅读模式

本文为翻译文章,需要阅读原文的师傅可以直接点击阅读原文。

什么是堆栈?

在计算机中,描述 "堆栈 "的最简单方法是一个临时的内存空间,局部变量和函数参数以不可执行的权限存储在这里。这个堆栈可以包含关于一个线程和它正在执行的函数的若干信息。每当你的进程执行一个新的线程时,就会创建一个新的堆栈。堆栈从下往上增长,以线性方式工作,这意味着它遵循后进先出的原则。RSP"(x64)或 "ESP"(x86)存储了当前线程的堆栈指针。除非开发者在创建线程的过程中明确改变,否则windows系统中每个线程的默认堆栈大小都是1兆字节。这意味着,如果开发者在编码时没有计算和增加堆栈大小,堆栈可能最终会撞到堆栈边界(另一种说法是堆栈金丝雀)并引发异常。通常情况下,msvcrt.dll中的_chkstk例程的任务是探测堆栈,如果需要更多的堆栈,则引发异常。因此,如果你写了一个位置独立的shellcode,需要一个大的堆栈(因为PIC中的一切都存储在堆栈中),你的shellcode将崩溃,引发一个异常,因为你的PIC不会被链接到msvcrt.dll中的_chkstk例程。当你的线程开始时,你的线程可能包含几个函数的执行和各种不同类型变量的使用。与需要手动分配和释放的堆不同,我们不需要手动计算堆。当编译器(mingw gcc或clang)编译C/C++代码时,它会自动计算所需的堆栈并在代码中添加所需的指令。因此,当你的线程运行时,它将首先从保留的1MB的堆栈中分配出 "x "的大小。以下面这个例子为例。

void samplefunction() {    char test[8192];}


在上述函数中,我们只是创建了一个8192字节的变量,但这不会被存储在PE中,因为它最终不会占用磁盘空间。因此,这样的变量被编译器优化并转换为指令,如。

sub rsp, 0x2000


上面的汇编代码从堆栈中减去了0x2000字节(8192十进制),这将在运行时被函数利用。简而言之,如果你的代码需要清理一些堆栈空间,它将向堆栈添加字节,而如果它需要一些堆栈空间,它将从堆栈中减去。线程中每个函数的堆栈将被转换为一个块,这被称为堆栈框架。堆栈框架提供了一个清晰明了的视图,即哪个函数被最后一次调用,从内存的哪个区域,该框架使用了多少堆栈,框架中存储的变量是什么,以及当前函数需要返回到哪里。每当你的函数调用另一个函数时,你当前函数的地址就会被推到堆栈中,这样,当下一个函数调用'ret'或return时,就会返回到当前函数的地址继续执行。一旦你的当前函数返回到上一个函数,当前函数的堆栈框架就会被破坏,虽然不是完全破坏,它仍然可以被访问,但大部分情况下最终会被下一个被调用的函数覆盖。像我对一个5岁的孩子那样解释,它应该是这样的。

void func3() {    char test[2048];    // do something    return;}
void func2() { char test[4096]; func3();}
void func1() { char test[8192]; func2();}


上述代码被转换为汇编,就像下面这样。

func3:    sub rsp, 0x800    ; do something    add rsp, 0x800    retfunc2:    sub rsp, 0x1000    call func3    add rsp, 0x1000    retfunc1:    sub rsp, 0x2000    call func2    add rsp, 0x2000    ret

好吧,一个5岁的孩子不会理解它,但你什么时候能找到一个5岁的孩子写一个恶意软件呢?XD! 因此,每个堆栈帧将包含为变量分配的字节数、前一个函数推送到堆栈的返回地址以及当前函数的局部变量的信息(简而言之)。

EDR的 "D "在哪里?


这里的检测技术是非常聪明的。有些EDR使用用户区钩子,而有些则使用ETW来捕获堆栈遥测数据。例如,假设你想在没有模块踩踏的情况下执行你的shellcode。所以,你通过VirtualAlloc或相对的NTAPI NtAllocateVirtualMemory分配一些内存,然后复制你的shellcode并执行它。现在你的shellcode可能有自己的依赖性,它可能调用LoadLibraryA或LdrLoadDll来从磁盘加载一个dll到内存。如果你的EDR使用用户区钩子,他们可能已经钩住了LoadLibrary和LdrLoadDll,在这种情况下,他们可以检查由你的RX shellcode区域推到堆栈的返回地址。这是一些EDR所特有的,如Sentinel One,Crowdstrike等,这将立即杀死你的有效载荷。其他EDR如Microsoft Defender ATP(MDATP),Elastic,FortiEDR将使用ETW或内核回调来检查LoadLibrary调用的来源。堆栈跟踪将提供一个完整的返回地址的堆栈框架,以及所有调用LoadLibrary开始的函数。简而言之,如果你执行一个DLL Sideload,执行你调用LoadLibrary的shellcode,它将看起来像这样。

|-----------Top Of The Stack-----------||                                      ||                                      ||--------------------------------------||------Stack Frame of LoadLibrary------||     Return address of RX on disk     ||                                      ||----------Stack Frame of RX-----------|  <- Detection (An unbacked RX region should never call LoadLibraryA)|     Return address of PE on disk     ||                                      ||-----------Stack Frame of PE----------|| Return address of RtlUserThreadStart ||                                      ||---------Bottom Of The Stack----------|


这意味着任何在usermode或通过内核回调/ETW钩住LoadLibrary的EDR,都可以检查最后的返回地址区域或调用来自哪里。在BRc4的v1.1版本中,我开始使用RtlRegisterWait API,它可以请求线程池中的一个工作线程在一个单独的线程中执行LoadLibraryA来加载库。一旦库被加载,我们就可以通过简单地行走PEB(Process Environment Block)来提取其基地址。Nighthawk后来在RtlQueueWorkItem API中采用了这一技术,该API是QueueUserWorkItem背后的主要NTAPI,它也可以将请求排到一个工人线程中,以便用一个干净的堆栈加载一个库。然而这是由Proofpoint去年某个时候在他们的博客中研究的,最近Elastic的Joe Desimone也发布了一条关于RtlRegisterWait API被BRc4使用的推特。这意味着迟早有一天,检测会围绕它进行,而且需要更多这样的API,可以用来进一步规避。因此,我决定花一些时间从ntdll中反转一些未记录的API,并发现了至少27个不同的回调,只要稍加调整和黑客攻击,就可以利用这些回调来加载我们的DLL与一个干净的栈。

Windows的回调。请允许我们介绍一下自己

回调函数是指向一个函数的指针,可以传递给其他函数在其内部执行。微软为软件开发者提供了大量的回调函数,以便通过其他函数执行代码。在这个github资源库中可以找到很多这些函数,自过去两年以来,这些函数已经被广泛利用。然而,所有这些回调有一个主要问题。当你执行一个回调时,你不希望回调与你的调用者线程在同一个线程中。这就意味着,你不希望堆栈跟踪遵循这样的线索。LoadLibrary返回到 -> 回调函数返回到 -> RX区域。为了有一个干净的堆栈,我们需要确保我们的LoadLibrary在一个独立于RX区域的线程中执行,如果我们使用回调,我们需要回调能够传递适当的参数给LoadLibraryA。在Windows中,大多数回调要么没有参数,要么不把参数 "原封不动 "地传给我们的目标函数 "LoadLibrary"。以下面的代码为例。

#include <windows.h>#include <stdio.h>
int main() { CHAR *libName = "wininet.dll";
PTP_WORK WorkReturn = NULL; TpAllocWork(&WorkReturn, LoadLibraryA, libName, NULL); // pass `LoadLibraryA` as a callback to TpAllocWork TpPostWork(WorkReturn); // request Allocated Worker Thread Execution TpReleaseWork(WorkReturn); // worker thread cleanup
WaitForSingleObject((HANDLE)-1, 1000); printf("hWininet: %pn", GetModuleHandleA(libName)); //check if library is loaded
return 0;}

如果你编译并运行上述代码,它将崩溃。原因是TpAllocWork的定义如下:

NTSTATUS NTAPI TpAllocWork(    PTP_WORK* ptpWrk,    PTP_WORK_CALLBACK pfnwkCallback,    PVOID OptionalArg,    PTP_CALLBACK_ENVIRON CallbackEnvironment);

这意味着我们的回调函数LoadLibraryA应该是PTP_WORK_CALLBACK的类型。这个类型扩展为。

VOID CALLBACK WorkCallback(    PTP_CALLBACK_INSTANCE Instance,    PVOID Context,    PTP_WORK Work);

从上图中可以看出,我们来自TpAllocWork API的PVOID OptionalArg被转发给我们的回调(PVOID Context)作为第二参数。因此,如果我们的假设是正确的,我们传递给TpAllocWork的参数libName(wininet.dll)将最终成为LoadLibraryA的第二个参数。但是LoadLibraryA并没有第二个参数。在调试器中检查这个问题,可以看到下面的图片。

【翻译】Hiding In PlainSight - 使用Proxying DLL规避ETWTI堆栈追踪

所以这确实创造了一个干净的堆栈,比如。LoadLibraryA返回到 -> TpPostWork返回到 -> RtlUserThreadStart,但是我们对LoadLibrary的参数被作为第二个参数发送,而第一个参数是一个指向TP_CALLBACK_INSTANCE结构的指针,由TpPostWork API发送。经过一番反思,我发现这个结构是由TppWorkPost(不是TpPostWork)动态生成的,正如预期的那样,它是ntdll.dll的一个内部函数,如果没有这个API的调试符号,就没有什么可以做。

【翻译】Hiding In PlainSight - 使用Proxying DLL规避ETWTI堆栈追踪

然而,所有的希望还没有消失。我们可以尝试的一个肮脏的技巧是将LoadLibrary的一个回调函数替换成TpAllocWork中的一个自定义函数,然后通过我们的回调调用LoadLibraryA。就像这样。

#include <windows.h>#include <stdio.h>
VOID CALLBACK WorkCallback( _Inout_ PTP_CALLBACK_INSTANCE Instance, _Inout_opt_ PVOID Context, _Inout_ PTP_WORK Work) { LoadLibraryA(Context);}
int main() { CHAR *libName = "wininet.dll";
PTP_WORK WorkReturn = NULL; TpAllocWork(&WorkReturn, WorkerCallback, libName, NULL); // pass `LoadLibraryA` as a callback to TpAllocWork TpPostWork(WorkReturn); // request Allocated Worker Thread Execution TpReleaseWork(WorkReturn); // worker thread cleanup
WaitForSingleObject((HANDLE)-1, 1000); printf("hWininet: %pn", GetModuleHandleA(libName)); //check if library is loaded
return 0;}

然而这意味着,回调将在我们的RX区域内,堆栈将变成。LoadLibraryA返回到 -> RX区域的回调返回到 -> RtlUserThreadStart -> TpPostWork,这不是很好,因为我们最终做了我们想避免的同样的事情。其原因是堆栈框架。因为当我们从RX区域的回调调用LoadLibraryA时,我们最终将RX区域的回调的返回地址推到了堆栈中,这最终成为了堆栈帧的一部分。然而,如果我们操作堆栈,不推送返回地址呢?当然,我们将不得不在汇编中写几行,但这应该可以完全解决我们的问题,我们可以直接从TpPostWork调用LoadLibrary,而不需要中间的错综复杂的问题。

#include <windows.h>#include <stdio.h>
typedef NTSTATUS (NTAPI* TPALLOCWORK)(PTP_WORK* ptpWrk, PTP_WORK_CALLBACK pfnwkCallback, PVOID OptionalArg, PTP_CALLBACK_ENVIRON CallbackEnvironment);typedef VOID (NTAPI* TPPOSTWORK)(PTP_WORK);typedef VOID (NTAPI* TPRELEASEWORK)(PTP_WORK);
FARPROC pLoadLibraryA;
UINT_PTR getLoadLibraryA() { return (UINT_PTR)pLoadLibraryA;}
extern VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work);
int main() { pLoadLibraryA = GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryA"); FARPROC pTpAllocWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpAllocWork"); FARPROC pTpPostWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpPostWork"); FARPROC pTpReleaseWork = GetProcAddress(GetModuleHandleA("ntdll"), "TpReleaseWork");
CHAR *libName = "wininet.dll"; PTP_WORK WorkReturn = NULL; ((TPALLOCWORK)pTpAllocWork)(&WorkReturn, (PTP_WORK_CALLBACK)WorkCallback, libName, NULL); ((TPPOSTWORK)pTpPostWork)(WorkReturn); ((TPRELEASEWORK)pTpReleaseWork)(WorkReturn);
WaitForSingleObject((HANDLE)-1, 0x1000); printf("hWininet: %pn", GetModuleHandleA(libName));
return 0;}


通过操纵堆栈框架将WorkCallback改道至LoadLibrary的ASM代码如下

section .text
extern getLoadLibraryA
global WorkCallback
WorkCallback: mov rcx, rdx xor rdx, rdx call getLoadLibraryA jmp rax

现在如果你把它们两个编译在一起,我们的TpPostWork会调用WorkCallback,但是WorkCallback并没有调用LoadLibraryA,而是跳转到它的指针上。WorkCallback只是将RDX寄存器中的库名移到RCX,擦除RDX,从一个特设函数中得到LoadLibraryA的地址,然后跳转到LoadLibraryA,最后重新排列整个堆栈框架,而没有加入我们的返回地址。这最终使堆栈框架看起来像这样。

【翻译】Hiding In PlainSight - 使用Proxying DLL规避ETWTI堆栈追踪

堆栈清晰如水晶,没有任何恶意的迹象。在发现这个技术之后,我开始寻找类似的其他可以操作的API,并发现只需要一点点类似的调整,你就可以实现代理DLL的加载,并使用驻扎在kernel32、kernelbase和ntdll中的其他27个Callbacks。我将把它作为一个练习留给本博客的读者去弄清楚。对于Brute Ratel的用户来说,你会在下一个版本v1.5中找到这些更新。本博客就写到这里,完整的代码可以在我的github仓库中找到。https://github.com/paranoidninja/Proxy-DLL-Loads






     ▼
更多精彩推荐,请关注我们


请严格遵守网络安全法相关条例!此分享主要用于学习,切勿走上违法犯罪的不归路,一切后果自付!


【翻译】Hiding In PlainSight - 使用Proxying DLL规避ETWTI堆栈追踪



原文始发于微信公众号(鸿鹄实验室):【翻译】Hiding In PlainSight - 使用Proxying DLL规避ETWTI堆栈追踪

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年1月31日10:42:12
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【翻译】Hiding In PlainSight - 使用Proxying DLL规避ETWTI堆栈追踪http://cn-sec.com/archives/1530361.html

发表评论

匿名网友 填写信息