加密堆分配
为什么要对堆分配进行加密?堆栈是局部作用域的,通常在函数完成时退出作用域。这意味着在函数运行期间设置在堆栈上的项目会在函数返回并完成时脱离堆栈;这显然不适用于你长期保存内存变量的期望。此时,就需要用到堆了。堆更像是一种长期内存存储解决方案。堆上的分配保留在堆上,直到你手动释放它们。如果你不断地将数据分配到堆上而从未释放任何内容,也可能导致内存溢出。也就是说,堆可能包含长期配置信息,例如 Cobalt Strike 的牺牲进程、休眠时间、回调路径等。这意味着如果你的 Cobalt Strike 代理在内存中运行,则任何防御者都可以在进程堆空间中以纯文本形式看到你的配置。作为防御者,我们甚至不需要识别你注入的线程,就可以轻松地 HeapWalk()所有分配并确定像“%windir%”这样简单的内容来尝试识别你的牺牲进程:
如你所见,这是一个非常令人担忧的想法。既然我们已经了解了这个问题,现在就必须去解决它。
我们有几个潜在的解决方案,不过每个方案各有其优缺点。让我们从独立的 EXE情况开始,因为这个要简单得多。该二进制文件是你的 Cobalt Strike 有效载荷。在这种情况下,我们可以很容易地实现我们的目标。使用前面提到的 HeapWalk() 函数,我们就可以迭代堆中的每个分配并对其进行加密!为了防止错误,我们可以在加密堆之前挂起所有线程,然后在加密后恢复所有线程。
即使你认为你的程序是单线程的,Windows 似乎也在后台提供线程,为 RPC 和 WININET 等实用程序执行垃圾收集和其他类型的函数。如果你不挂起这些线程,它们将在尝试引用加密分配时使你的进程崩溃。崩溃示例如下:
Windows 后台线程
wininet.dll 线程崩溃
从理论上讲,这是一个很容易实现的方法!最后一个难题是如何在 Cobalt Strike 休眠时调用所有这些函数,解决方法很简单。
挂钩
如果我们查看 Cobalt Strike 二进制文件的 IAT(导入地址表),我们将看到它利用 Kernel32.dll Sleep 来实现其休眠函数。
Cobalt Strike 进口我们需要做的就是在 kernel32.dll 中挂钩 Sleep,然后将挂钩休眠中的行为修改为以下内容:
基本上,我们挂起所有的线程并运行我们的加密例程,如下所示:
这将创建一个 PROCESS_HEAP_ENTRY 结构,在每次调用时将其清零,然后遍历堆并将数据放入该结构中。然后我们检查当前堆条目的标志并验证它是否已分配,以便我们只对分配进行加密。
然后我们运行原始/旧休眠函数,该函数将作为挂钩函数的一部分创建,然后在恢复线程之前解密。这样我们就可以防止再次引用分配时发生崩溃。总之,这是一个相当简单的进程。目前我们还没有用到的是挂钩功能。
首先,什么是函数挂钩?函数挂钩意味着我们在进程空间内重新路由对函数的调用,例如 Sleep() ,以在内存中运行我们的任意函数。这样,我们局可以改变函数的行为,观察被调用的参数(因为我们的任意函数现在被调用,可以打印传递给它的参数),甚至完全阻止函数工作。在许多情况下,这就是 EDR 监控可疑行为并发出警报的方式。他们挂钩自认为有趣的函数,例如 CreateRemoteThread,并记录所有的参数,以便以后在可疑调用时发出警报。
那如何挂钩该函数?有很多方法可以实现这一点,但我只会提到两种并深入研究一种。我将提到的两种技术是 IAT挂钩和 Trampoline Patching
IAT挂钩
IAT 挂钩背后的想法很简单,每个进程空间都有所谓的导入地址表。此表包含已由二进制文件导入以供使用的 DLL 和相关函数指针的列表。推荐的和最稳定的挂钩方法是遍历导入地址表,识别你尝试挂钩的 DLL,识别你想要挂钩的函数并覆盖其指向任意挂钩函数的函数指针。每当进程调用该函数时,它都会定位指针并调用你的函数。如果你想调用旧函数作为挂钩函数的一部分,你可以存储旧指针,ired.team 上已经存在一个示例。
这种方法有优点也有缺点,优点是实现起来非常简单,而且非常稳定。你只是改变了调用的函数,你并没有改变任何内容,但却有很大的崩溃风险。
如果使用 GetProcAddress() 来解析函数,它不会在 IAT 中。这是一个非常有针对性的挂钩方法,可以带来好处,但如果你想监视更广泛的调用,例如能够挂钩 NtCreateThreadEx 与仅使用 CreateRemoteThread,理论上也更容易检测。
Trampoline Patching
现在让我们谈谈 Trampoline Patching。 Trampoline Patching 更难实现,很不稳定,并且由于必须解决许多相对寻址问题,因此可能需要很长时间才能对 x64 进行普遍处理。值得庆幸的是,已经有人花时间制作了一个开源库,以非常稳定的方式执行所有这些操作。
接下来让我们继续看看挂钩是如何工作的,这样我们就可以根据需要重新实现我们自己的挂钩。首先使用 GetProcAddress 和 LoadLibrary 解析函数。然后,解析有效程序集的前X个指令数,加起来最少为五个字节。更具体地说,我们将使用一种非常常见的技术,该技术使用五字节相对跳转操作码 (E9) 从函数库跳转到 +- 2GB 的位置,然后跳转到我们的任意函数。显然,要使其工作,我们需要覆盖函数的前五个字节。如果我们这样做,就需要再次调用它这样会破坏原始函数。为了确保我们可以在需要时解决旧函数,就必须保存第一条指令,稍后将写入代码cave作为trampoline的一部分,该trampoline将为我们运行它,然后跳回下一条指令的函数。但如果第一条指令只有四个字节,写入五个字节就会破坏第二条指令的第一个操作码。然后我们需要将前两条指令存储在我们的trampoline中,现在trampoline将运行前两条指令并跳回到第三条指令继续执行。这个trampoline所在的地方将是被挂钩的原始函数的新指针。所以,原来的函数指针现在是这样运行。
这个代码cave也会跳转到任意函数的某个位置,写在原始函数基础的相对五字节跳转跳转到这个位置,然后像这样跳转到任意函数。
这样,我们现在就有了一种方法,可以在调用旧函数时运行任意函数,并根据需要调用原始函数。
现在让我们开始调试,看看 MessageBoxA 是干净的还是经过挂钩的。
首先,我们挂钩 MessageBoxA。代码看起来像这样:
MessageBoxA位于user32.dll中,找到基地址后,Patching所有内容,向cave添加一些代码,解析相对跳转并将trampoline存储在 OldMessageBoxA() 中。
任意/挂钩 MessageBoxA 函数将如下所示:
我们需要匹配返回类型和参数,此时,我们将运行原始 MessageBoxA,无论如何我们将修改文本,始终显示“HOOKED”。
现在看一下前后对比情况:
Patching未进行挂钩的消息框之前
Patching进行挂钩的消息框之后
如你所见,在 BEFORE 屏幕截图中,第一条指令只有四个字节。这意味着我们需要存储前两条指令;然后我们的相对跳转继续覆盖前五个。我们不需要修改剩余的字节,因为我们将让trampoline执行我们存储的前两个字节,然后跳回位置 0x00007FF8EF70AC27。让我们继续在调试器中查看新的挂钩函数是什么样的。我们将在运行 JMP 后立即开始:
跳转到挂钩函数
首先看到两个 00。这样做是为了确保如果我们将多个trampoline写入cave,我们不会覆盖函数指针中 00 00 的末尾。接下来我们看到FF 25 00 00 00 00,也就是JMP QWORD PTR指令。紧接着,你将看到八个字节,它们是挂钩函数的指针!如果我们执行这条指令,就会看到:
挂钩函数中的第一条指令
最后:
内部挂钩函数
我们可以看到我们在我们的挂钩函数中,挂钩函数只运行并返回旧函数,所以让我们继续执行旧函数:
调用旧函数
让我们看看结果如何:
trampoline
如果你看这张图,可以看到我们正在执行我们覆盖的前两条指令。在复制的字节之后,我们对OriginalFunction+7的位置执行第二个JMP QWORD PTR(因为在这个示例中trampoline的大小是7个字节)。这将使我们处于第三条指令的开头。让我们来看看:
继续执行
可以看到我们现在处于 CMP 指令处,从我们停止的地方继续执行。
通过这个进程,你可以看到像 minhook 这样的实用程序是如何工作的。现在,你可以像我一样自己实现它,也可以使用像 minhook 这样稳定的内容。:
将 EXE 放在一起
把所有内容放在一起看看它是什么样子的:
Hook Sleep();
在挂钩函数中,挂起所有线程;
使用 HeapWalk() 加密所有分配;
通过trampoline函数运行原始的Sleep();
使用 HeapWalk() 解密所有分配;
恢复所有线程;
我将假设你拥有自己的加密、挂钩和完整的线程挂起函数。代码如下所示:
非常简单,这段代码显然不包括你的植入程序。你可以通过以某种方式执行 shell 代码在同一进程空间中运行植入程序,也可以将其转换为 DLL 并将其注入执行后的信标中。由于它使用了 HeapWalk(),所以它可以加密过去、现在和将来的分配,只需要挂钩 Sleep() 来开始调用。
在这个演示中,对于任何sleep值为1或更少的内容,我们都不加密。
EXE HeapWalk() 加密器演示
如你所见,首先我们进行 1 次休眠,BeaconEye 会捕获我们的配置。将 sleep值 修改为 5,然后开始加密,成功关闭 BeaconEye。
注意,由于这会加密所有堆分配,因此它不会作为注入线程工作,因为当 Cobalt Strike 处于休眠状态时,它注入的进程将无法运行。想象一下注入 explorer.exe,每次信标休眠时,所有的 Explorer 都会冻结。当需要作为线程注入时,这个解决方案显然不是最佳的。如果我们想要一些可以作为线程工作的内容,将需要做更多的工作。
可以在此处找到演示过程。
线程目标堆加密
我们的新设计必须使用单独的线程,因为无法挂起额外的线程也无法锁定堆,主进程将需要继续运行。这意味着当我们注入一个信标线程时,必须确保所有加密的分配都只来自该线程。如果我们正确定位线程,则可以成功地避免该问题。
现在在我们的 dropper 中有挂钩函数。为了操作堆,需要在 Windows 中调用了一个函数子集:
HeapCreate()
HeapAllocate()
HeapReAllocate()
HeapFree()
HeapDestroy()
Windows中的Malloc和free,位于msvcrt.dll中,实际上是HeapAllocate()和HeapFree()的高级包装器,它们是RtlAllocateHeap()和RtlFreeHeap()的高级包装器,它们是Windows中最低级别的函数,最终直接管理堆。
来自Ghidra的图
这意味着如果我们挂钩 RtlAllocateHeap()、RtlReAllocateHeap() 和 RtlFreeHeap(),则可以在 Cobalt Strike 中跟踪堆空间内分配和释放的所有内容。通过挂钩这三个函数,我们可以在映射中插入分配和重新分配,并在调用 free 时将它们从映射中删除,但这仍然不能解决我们的线程目标问题。
事实证明,如果你从一个钩子函数调用GetCurrentThreadId(),你实际上可以获取调用线程的线程 id,使用它,你可以注入你的信标,获取其线程 id 并执行类似于以下的操作:
这样做是为了重新分配,也可以免费进行删除,现在你的目标是一个线程!到目前为止很容易。但是还记得之前的那个问题,我们不得不挂起其他线程的原因吗?在我们及时解密之前,WININET 和 RPC 调用仍会尝试访问加密的内存。这里有几个选项,但我使用了我认为有趣的选项。由于加载的 shell 代码既不是有效的 EXE 也不是 DLL,因此我能够从任何发起调用的对象中进行分配,这些调用源自没有名称的模块。
为了让这个机制起作用,我们需要解析进行函数调用的模块。这可以通过以下代码实现:
这将获得_ReturnAddress内在函数,并将其与GetModuleHandleEx和标志
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 一起使用,以识别哪个模块正在进行此调用。然后我们可以将其转换为小写字符串,如果字符串不包含 DLL 或 EXE,我们继续插入它。这样,你就有了一个稳定的分配列表,可以在休眠时加密。你将需要重复这个进程为你的挂钩重新分配。
要运行加密,你需要迭代列表并加密这些分配,而不是执行 HeapWalk()。这将取决于你是否决定使用映射、矢量、链表或其他内容。你希望将真正的HeapAlloc或ReAlloc返回的指针存储到您的数组中,迭代数组并按大小对数据进行加密。上面示例中的Arg3是size 。
现在我们挂钩了四个不同的函数,将基于线程 id 的分配插入向量中,迭代向量并在休眠时加密每个地址。如果成功,我们应该可以再次绕过 BeaconEye。
本文翻译自:https://www.cyberark.com/resources/threat-research-blog/hook-heaps-and-live-free如若转载,请注明原文地址