首页
社区
课程
招聘
[翻译]漩涡#6:在红队和蓝队中使用AMSI和ETW
2023-8-28 19:57 7752

[翻译]漩涡#6:在红队和蓝队中使用AMSI和ETW

2023-8-28 19:57
7752

原文标题:Maelstrom #6: Working with AMSI and ETW for Red and Blue
原文地址:
https://pre.empt.blog/2023/maelstrom-6-working-with-amsi-and-etw-for-red-and-blue
由于作者博客迁移的原因,原文中的图片已经失效,我在archive中找到了别人保存下来的网页:https://web.archive.org/web/20221126185300/https://pre.empt.dev/posts/maelstrom-etw-amsi/
本文由本人用chatgbt翻译而成,Maelstrom是一个概念验证的命令与控制框架,具有Python后端和C++植入程序。

目录

介绍

上周,我们探讨了事件检测与响应(EDR)程序可以使用的三种机制,用于引起怀疑并阻止C2植入物的运行。然而,Windows本身也有一些机制可以阻止C2植入物的完全功能,这对防御者来说是一个强大的优势,对C2操作者来说是一个值得克服的障碍。
正如我们上周提到的,开发一个功能完善的植入物可能出奇地容易,有时会感到奇怪的是,哪些行为和操作可以毫无问题地执行,哪些会引起不必要的关注。这是因为并非所有的行为都一定是恶意的,防御性机制阻止计算机的使用并不利于生产效率。
随着时间的推移,微软增强了其内置的保护机制,并向第三方应用开放了这些机制。EDR解决方案越来越多地包括这些机制和其遥测数据,这意味着当代的C2植入物必须要么规避,要么抵消这些保护机制。
作为两个部分中的第二部分,本集将讨论当代EDR使用的两个关键的Windows保护机制:ETW和AMSI。

目标

本文将涵盖以下内容:

  • 回顾事件跟踪(Event Tracing) for Windows
    • 信息收集的位置
    • 如何操纵事件
    • 如何规避ETW TI
  • 回顾反恶意软件扫描接口(Anti-Malware Scan Interface)
    • 检测的表现形式
    • AMSI的历史绕过方式
    • AMSI可能持续被绕过的方式

在这篇关于终端保护的第二篇博客中,我们将探讨现代EDR可以保护免受恶意植入物的五种最显著方式,并探索这些保护机制可能被绕过的方法。我们将从一个带有条件的概念验证植入物转变为一个可以作为我们的C2的一部分并执行恶意流量而不被检测的植入物。
重申我们在每篇博客中都提到的同样警告,这些文章中的代码仅仅是为了说明。在一个植入物中,可能有成千上万种潜在的检测和失误,这些都可能引起EDR对植入物的兴趣,如果我们发布一个没有任何缺陷的植入物给全世界,那将是不负责任的行为。此外,还有代码混乱的问题。

重要概念

事件跟踪(Event Tracing) for Windows

在我们深入探讨事件跟踪(Event Tracing) for Windows (ETW)如何利用其威胁情报功能之前,我们首先需要了解ETW本身——它是什么,它是如何工作的,以及它的用途是什么。
ETW最早于Windows 2000引入,最初旨在提供详细的用户和内核日志记录,可以动态启用或禁用,而无需重新启动目标进程。最初和现在主要用于应用程序调试和优化。缓冲区和消息队列的早期使用,类似于较新的Web技术(如Apache Kafka),旨在限制跟踪(日志记录)会话对系统的影响,在尝试调试进程本身的系统影响时非常有帮助。
自Windows 2000以来,ETW的核心结构几乎没有改变,尽管发送和接收日志的过程已经多次进行了改进,以使第三方程序更容易与ETW集成。
微软的文档将ETW架构描述为以下三个不同的组件:

  • 控制器(Controllers):启动和停止事件跟踪会话并启用提供者。
  • 提供者(Providers):提供事件。
  • 消费者(Consumers):消费事件。

图片描述
控制器仅限于具有管理员权限的用户,但有一些例外情况。
除了回调功能外,事件跟踪(Event Tracing) for Windows威胁情报还提供了来自内核的跟踪,并允许以各种方式使用这些跟踪数据。

使用ETW

在Windows中,存在名为logman的二进制文件,可以将其视为控制器,因为它具有相应的功能:

1
2
3
4
5
6
7
8
9
Verbs:
  create                        Create a new data collector.
  query                         Query data collector properties. If no name is given all data collectors are listed.
  start                         Start an existing data collector and set the begin time to manual.
  stop                          Stop an existing data collector and set the end time to manual.
  delete                        Delete an existing data collector.
  update                        Update an existing data collector's properties.
  import                        Import a data collector set from an XML file.
  export                        Export a data collector set to an XML file.

提供者

Windows中有一个巨大的默认提供者列表;虽然有这些提供者的列表可供使用,但我们可以使用logman命令列出系统上的所有提供者:

1
logman query providers

运行此命令将生成一个长长的提供者列表。我们现在将重点关注Microsoft-Windows-DotNETRuntime
再次运行命令:

1
logman query providers Microsoft-Windows-DotNETRuntime

结果为:
图片描述
对于那些更喜欢可视化界面的人,Pavel Yosifovich(zodiacon)开发了一个名为EtwExplorer的工具,用于在图形界面中探索ETW。
下面是与logman查询相同的提供者的示例:
图片描述

示例代码

为了使用ETW进行实验,我们将尝试检测通过一个精简加载器(slim loader)反射加载一个概念验证的被加载程序(loadee executable):

  • 加载器(Loader)代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Reflection;
 
namespace Loader
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Assembly assembly = Assembly.LoadFrom(@"C:\Users\mez0\Desktop\Loader\Example\bin\Debug\Example.exe");
            assembly.EntryPoint.Invoke(null, null);
        }
    }
}

然后我们有被加载程序(loadee)"Example.exe",我们将对其进行编译,并由加载器通过反射访问:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
 
namespace Example
{
    internal class Program
    {
        static void Main()
        {
            Console.WriteLine("--> Hello From Example.exe <--");
        }
    }
}

加载器将调用Assembly.LoadFrom加载被加载程序(loadee),它只是将一条消息打印到屏幕上。当运行时,被加载程序(loadee)将显示出来:
图片描述

配置控制器

为了查看ETW的操作情况,我们需要一个控制器来创建、启动和停止我们的跟踪。为此,我们可以再次使用logman命令。
首先,我们创建跟踪,提供会话的名称("pre.empt.etw"),并使用-ets标志将命令直接发送到事件跟踪,而不进行调度或保存:

1
logman create trace pre.empt.etw -ets

运行此命令应该返回以下结果:

1
The command completed successfully.

创建了我们的跟踪后,我们现在可以查询它以获取其状态和配置,使用以下命令:

1
logman query pre.empt.etw -ets

这应该返回类似于以下的结果:
图片描述
请注意输出位置,它基于我们创建的跟踪的名称。我们将需要这个位置来在事件查看器(Event Viewer)中打开我们的跟踪:

1
C:\Users\mez0\pre.empt.etw.etl

完成后,我们可以将新的提供者添加到控制器:

1
logman update pre.empt.etw -p Microsoft-Windows-DotNETRuntime 0x2038 -ets

其中0x2038是在"Detecting Malicious Use of .NET – Part 2"中显示的事件的位掩码:

1
LoaderKeyword,JitKeyword,NGenKeyword,InteropKeyword

再次查询跟踪的情况:
图片描述
现在设置完成后,再次运行程序,并在事件查看器(Event Viewer)中打开etl文件:
图片描述
事件ID为152时:

1
LoaderModuleLoad(加载模块)

然后是事件ID为145时:MethodJittingStarted_V1(方法即时编译开始)
图片描述
要停止跟踪,只需运行以下命令:

1
logman stop pre.empt.etw -ets

干扰ETW

MDSec的文章《Hiding your .NET ETW》中,xpn提到:

为了禁用这个函数,我们将使用相同的ret 14h操作码字节c21400,并将其应用到函数的开头。

然后提供了示例代码:

1
2
3
4
5
6
7
8
// 获取EventWrite函数
void *eventWrite = GetProcAddress(LoadLibraryA("ntdll"), "EtwEventWrite");
// 允许对页面进行写入
VirtualProtect(eventWrite, 4, PAGE_EXECUTE_READWRITE, &oldProt);
// 使用x86上的"ret 14"进行修补
memcpy(eventWrite, "\xc2\x14\x00\x00", 4);
// 将内存恢复为原始保护状态
VirtualProtect(eventWrite, 4, oldProt, &oldOldProt);

让我们更新Loader代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;
using System.Reflection;
using System.Runtime.InteropServices;
 
namespace Loader
{
    internal class Program
    {
        [DllImport("kernel32")]
        private static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
 
        [DllImport("kernel32")]
        private static extern IntPtr LoadLibrary(string name);
 
        [DllImport("kernel32")]
        private static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
 
        public static void PatchEtw()
        {
            IntPtr hNtdll = LoadLibrary("ntdll.dll");
            IntPtr pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");
 
            byte[] patch = { 0xc3 };
 
            _ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, 0x40, out uint oldProtect);
 
            Marshal.Copy(patch, 0, pEtwEventWrite, patch.Length);
 
            _ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, oldProtect, out uint _);
        }
 
        private static void Main(string[] args)
        {
            Console.WriteLine("Inspect the AppDomains, then press any key...");
            Console.ReadLine();
 
            PatchEtw();
 
            Console.WriteLine("ETW is patched! Recheck then press any key...");
            Console.ReadLine();
 
            Assembly assembly = Assembly.LoadFrom(@"C:\Users\mez0\Desktop\Loader\Example\bin\Debug\Example.exe");
            assembly.EntryPoint.Invoke(null, null);
        }
    }
}

在这种情况下,根据xpn的逻辑,已将项目转换为x64,并添加了以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void PatchEtw()
{
    IntPtr hNtdll = LoadLibrary("ntdll.dll");
    IntPtr pEtwEventWrite = GetProcAddress(hNtdll, "EtwEventWrite");
 
    byte[] patch = { 0xc3 };
 
    _ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, 0x40, out uint oldProtect);
 
    Marshal.Copy(patch, 0, pEtwEventWrite, patch.Length);
 
    _ = VirtualProtect(pEtwEventWrite, (UIntPtr)patch.Length, oldProtect, out uint _);
}

这个函数在NTDLL的EtwEventWrite指令上设置了0xc3ret)指令。
在应用修补程序之前:
图片描述
然后在应用修补程序之后:
图片描述
那么,关键问题是,对于实际的ETW(事件跟踪)会话,这是否重要?
重新设置:
图片描述
答案是:有点重要。
图片描述
目前没有像以前那样提到Example.exe。然而,由于Loader首先运行,然后修补了ETW,显然在修补之前仍然存在与之相关的事件:
图片描述
当与其他启发式方法结合使用时,这仍然足够作为威胁指示的依据。例如,启用了ETW的EDR如果检测到某个进程的事件突然停止,而该进程仍然可见,那么如果该进程仍在运行,EDR仍然可能将其标记为可疑行为。

修复ETW

如果内存被修补,最好在完成后撤销修补。在这种情况下,0xc3变为0x4c

1
2
byte[] breakEtw = { 0xc3 };
byte[] repairEtw = { 0x4c };

这很容易实现,只需调用相同的函数,使用不同的字节值即可。接下来,在.NET中的情况是卸载程序集。这有点复杂,但是可以实现。我们可以通过以下方法解决:

首先,创建一个AppDomain

1
AppDomain appDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString());

现在,考虑一下Assembly.Load和FileNotFoundException

AppDomain.Load返回一个Assembly,这就是问题出在哪里。两个AppDomains不能直接相互传递内容。AppDomains的存在完全是为了能够在一个应用程序中“隔离”某些功能。AppDomains之间的通信(几乎)对用户(程序员)来说是透明的,使用通道和代理进行,但并非完全如此。您需要知道可以通过值传递对象(它们需要实现ISerialize或声明为Serializable)到另一个AppDomain,或者通过引用传递,这种情况下类需要继承MarshalByRefObj

提出的解决方案是使用继承自MarshalByRefObject的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Proxy : MarshalByRefObject
{
    public Boolean InvokeAssembly(byte[] bytes)
    {
        try
        {
            Assembly assembly = Assembly.Load(bytes);
            assembly.EntryPoint.Invoke(null, null);
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }
}

然后可以使用它来从字节数组调用程序集:

1
2
Proxy proxy = (Proxy)appDomain.CreateInstanceAndUnwrap(typeof(Proxy).Assembly.FullName, typeof(Proxy).FullName);
proxy.InvokeAssembly(File.ReadAllBytes(@"C:\Users\mez0\Desktop\Loader\Example\bin\x64\Debug\Example.exe"));

然后卸载AppDomain:

1
AppDomain.Unload(appDomain);

在离开这个主题之前,值得一提的是:Assembly.Lie - 使用Transactional NTFS和API Hooking欺骗CLR从磁盘加载代码。这里不会讨论这个话题,.NET C2操作,这是值得考虑的。

ETW:威胁情报

ETW提供了大量的跟踪功能。然而,ETW的一个子部分被终端保护供应商广泛用于获取信息,其中包括像Microsoft Defender for Identity这样的解决方案,它们大量使用ETW进行威胁情报。这个子部分被称为"Event Tracing for Windows Threat Intelligence"(ETW TI)。
以下截图展示了ETW TI的功能:
图片描述
内存/进程/线程操作、驱动程序事件等等,各种技术都是ETW提供的威胁情报和终端可见性的重要组成部分。随着越来越多的供应商开始实施这些技术,对终端的可见性变得更加清晰。
这是一个庞大的主题,在这里我们无法详细讨论,所以我为你提供了一些很好的参考资料:

ETWTi识别进程注入

作为一个例子,下面的截图显示了PreEmpt检测到Maelstrom以反射方式加载DLL的情况:
图片描述
在左侧,maelstrom被执行。然后,在右侧,PreEmpt接收到一个包含受影响内存区域所有信息的事件。下面是完整的JSON内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "data": {
    "allocation": "0x3000",
    "protectType": "0x1d0000",
    "protection": "0x40",
    "regionsize": "73728",
    "source_name": "C:\\Users\\admin\\Desktop\\maelstrom.unsafe.x64.exe",
    "source_pid": "9708"
  },
  "id": "cd27e5a5-df06-4859-96f0-d0b207d21ebf",
  "reason": "Malicious Activity Detected",
  "task": "EtwTi Process Injection",
  "time": "Tue May  3 19:34:33 2022"
}

需要考虑的事项

在使用利用ETWTi的EDR时,请记住内存修改、进程/线程创建等操作都会被记录下来。然而,并非所有事件都会引发预防措施或行动,但相关信息将被记录。这就是为什么我们避免使用以下类似的论调:
正如在《Bypassing EDR real-time injection detection logic》中所示,如果检测逻辑不够强大,可以绕过该逻辑。在DripLoader的情况下,它通过逐渐向内存区域添加越来越多的数据来绕过检测。正如博客中所描述的,DripLoader通过以下方式避免了ETWTi内存分配警报:

  • 使用风险最高的API,如NtAllocateVirtualMemory和NtCreateThreadEx。
  • 混合调用参数以创建强制供应商放弃或记录并忽略的事件,原因是事件数量太多。
  • 通过引入延迟来避免多事件相关性。

最后,对于防御者来说,ETWTi是一个非常有价值的接口,EDR系统正越来越努力将其纳入其中。虽然目前并非所有代理程序都收集ETW TI,但Mandiant的SilkETW是将ETW整合到ELK SOC中的一种快速方式。

反恶意软件扫描接口(AMSI)

来自 Antimalware Scan Interface (AMSI)

Antimalware Scan Interface (AMSI)是一个通用的接口标准,允许应用程序和服务与计算机上存在的任何杀毒软件产品进行集成。AMSI为最终用户及其数据、应用程序和工作负载提供增强的恶意软件保护。
AMSI与杀毒软件供应商无关;它旨在允许应用程序集成当今杀毒软件产品提供的最常见的恶意软件扫描和保护技术。它支持调用结构,允许对文件、内存或流进行扫描、内容源URL/IP声誉检查和其他技术。

为了提供背景信息,这里是AMSI架构的示意图:

图片描述
对于脚本恶意软件,它经常会进行易于混淆的处理。然而,AMSI允许开发人员扫描最终缓冲区,因为最终代码必须进行解混淆。Antimalware Scan Interface (AMSI)如何帮助你防御恶意软件详细说明了这个过程,并提供了多个示例。
基本上,AMSI是由Microsoft提供的一种接口,允许开发人员注册提供程序并使用提供的功能。传统上,可以像开发人员目标受众和示例代码中所示,注册一个DLL。至于所暴露的功能:

Function Description
AmsiCloseSession Close a session that was opened by AmsiOpenSession.
AmsiInitialize Initialize the AMSI API.
AmsiNotifyOperation Sends to the antimalware provider a notification of an arbitrary operation.
AmsiOpenSession Opens a session within which multiple scan requests can be correlated.
AmsiResultIsMalware Determines if the result of a scan indicates that the content should be blocked.
AmsiScanBuffer Scans a buffer-full of content for malware.
AmsiScanString Scans a string for malware.
AmsiUninitialize Remove the instance of the AMSI API that was originally opened by AmsiInitialize.

这样做的好处在于检测逻辑来自于 Microsoft。这意味着不需要维护自己的恶意软件数据库,提供程序可以直接连接到 Microsoft 的信息上。

AMSI检测示例

我们的示例工具Hunter已经更新,支持AMSI,并将在本博客系列结束时发布。

AMSI支持位于以下命名空间中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#ifndef AMSISCANNER_H
#define AMSISCANNER_H
 
#include "pch.h"
 
namespace AmsiManager
{
    class Amsi
    {
    public:
        Amsi()
        {
            HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
            if (hr != S_OK) {
                throw std::runtime_error("COM library failed to initialize");
            }
        }
        ~Amsi()
        {
            CoUninitialize();
        }
 
        void ScanMemory(std::vector<MEMORY_BASIC_INFORMATION> regions, HANDLE hProcess)
        {
            HRESULT hr = S_OK;
            AMSI_RESULT res = AMSI_RESULT_CLEAN;
            HAMSISESSION hSession = nullptr;
            HAMSICONTEXT hAmsi = NULL;
 
            ZeroMemory(&hAmsi, sizeof(hAmsi));
            hr = AmsiInitialize(L"Hunter", &hAmsi);
            if (hr != S_OK) {
                Errors::Show().print_hresult("AmsiInitialize", hr);
                return;
            }
 
            hr = AmsiOpenSession(hAmsi, &hSession);
            if (hr != S_OK) {
                Errors::Show().print_hresult("AmsiOpenSession", hr);
                return;
            }
 
            for (MEMORY_BASIC_INFORMATION& mbi : regions)
            {
                if (mbi.BaseAddress == nullptr)
                {
                    continue;
                }
                if (mbi.Protect == PAGE_EXECUTE_READWRITE || mbi.Protect == PAGE_EXECUTE || mbi.Protect == PAGE_READWRITE)
                {
                    std::vector<unsigned char> buffer = ReadMemoryRegion(mbi, hProcess);
                    if (buffer.empty())
                    {
                        continue;
                    }
                    hr = AmsiScanBuffer(hAmsi, buffer.data(), buffer.size(), NULL, hSession, &res);
                    if (hr != S_OK) {
                        Errors::Show().print_hresult("AmsiScanBuffer", hr);
                        return;
                    }
                    if (res != AMSI_RESULT_CLEAN && res != AMSI_RESULT_NOT_DETECTED)
                    {
                        printf("  | AMSI Detection @ 0x%p: %s\n", mbi.BaseAddress, GetResultDescription(res));
                    }
                }
            }
        }
 
    private:
 
        const char* GetResultDescription(HRESULT hRes) {
            const char* description;
            switch (hRes)
            {
            case AMSI_RESULT_CLEAN:
                description = "AMSI_RESULT_CLEAN";
                break;
            case AMSI_RESULT_NOT_DETECTED:
                description = "AMSI_RESULT_NOT_DETECTED";
                break;
            case AMSI_RESULT_BLOCKED_BY_ADMIN_START:
                description = "AMSI_RESULT_BLOCKED_BY_ADMIN_START";
                break;
            case AMSI_RESULT_BLOCKED_BY_ADMIN_END:
                description = "AMSI_RESULT_BLOCKED_BY_ADMIN_END";
                break;
            case AMSI_RESULT_DETECTED:
                description = "AMSI_RESULT_DETECTED";
                break;
            default:
                description = "";
                break;
            }
            return description;
        }
 
        std::vector<unsigned char> ReadMemoryRegion(MEMORY_BASIC_INFORMATION region, HANDLE hProcess)
        {
            std::vector<unsigned char> vectorBuffer(region.RegionSize);
            BOOL bRead = ReadProcessMemory(hProcess, (LPVOID)region.BaseAddress, vectorBuffer.data(), region.RegionSize, NULL);
 
            if (bRead == FALSE)
            {
                Errors::Show().print_win32error("ReadProcessMemory");
                return std::vector<unsigned char>();
            }
            return vectorBuffer;
        }
    };
}
 
#endif

然后,通过以下代码触发扫描:

1
2
3
4
5
6
void ScanWithAmsi()
{
    AmsiManager::Amsi amsi = AmsiManager::Amsi();
 
    amsi.ScanMemory(_regions, _hProcess);
}

根据文档,在初始化AMSI并创建会话之后:

1
2
3
4
5
6
7
8
9
10
11
12
ZeroMemory(&hAmsi, sizeof(hAmsi));
hr = AmsiInitialize(L"Hunter", &hAmsi);
if (hr != S_OK) {
    Errors::Show().print_hresult("AmsiInitialize", hr);
    return;
}
 
hr = AmsiOpenSession(hAmsi, &hSession);
if (hr != S_OK) {
    Errors::Show().print_hresult("AmsiOpenSession", hr);
    return;
}

一旦设置完成,就会循环遍历内存区域(_regions),然后传递给AmsiScanBuffer函数进行扫描。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for (MEMORY_BASIC_INFORMATION& mbi : regions)
{
    if (mbi.BaseAddress == nullptr)
    {
        continue;
    }
    if (mbi.Protect == PAGE_EXECUTE_READWRITE || mbi.Protect == PAGE_EXECUTE || mbi.Protect == PAGE_READWRITE)
    {
        std::vector<unsigned char> buffer = ReadMemoryRegion(mbi, hProcess);
        if (buffer.empty())
        {
            continue;
        }
        hr = AmsiScanBuffer(hAmsi, buffer.data(), buffer.size(), NULL, hSession, &res);
        if (hr != S_OK) {
            Errors::Show().print_hresult("AmsiScanBuffer", hr);
            return;
        }
        if (res != AMSI_RESULT_CLEAN && res != AMSI_RESULT_NOT_DETECTED)
        {
            printf("  | AMSI Detection @ 0x%p: %s\n", mbi.BaseAddress, GetResultDescription(res));
        }
    }
}

很抱歉您遇到了AMSIs触发不稳定的问题。让我们继续讨论AMSIs的典型使用方式。

AMSI自动加载

NET Framework的新功能中提到:

对所有程序集进行防恶意软件扫描。.NET Framework版本中,运行时会使用Windows Defender或第三方防恶意软件扫描从磁盘加载的所有程序集。然而,从其他来源加载的程序集(例如通过Assembly.Load()方法加载的程序集)不会进行扫描,可能包含未被检测到的恶意软件。.NET Framework 4.8开始,在Windows 10上运行时会触发实现了Antimalware Scan Interface (AMSI)的防恶意软件解决方案进行扫描。
.NET Framework 4.8开始,.NET Framework的一部分。因此,当加载程序集时,AMSI.DLL也会被加载,以支持使用AMSI进行恶意软件检测。

如果已安装了4.8版本,则可以检查已加载的模块。以下是针对PowerShell的示例代码:
图片描述
对于.NET程序集也会发生同样的情况。如果所讨论的C2是.NET的,并且依赖于Assembly.Load来执行分阶段或后渗透操作,这将是一个重要的考虑因素。话虽如此,有一些替代的方法可以减少风险,例如禁用特定事件。这里不会涉及这些内容,但可以参考SharpTransactedLoad

历史上的AMSI绕过

多年来,AMSIs存在绕过问题,因此出现了一些应用程序,例如amsi.fail。无论C2是否位于.NET中,或者植入物是否能够托管CLR,都需要处理AMSIs。由于maelstrom既不属于这两个部分,我们可以略过一些内容。
目前最常见的做法是通过覆盖AmsiScanBuffer的内存来修补AMSIs。

1
var patch = new byte[] { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };

这在"Memory Patching AMSI Bypass"中有记录。在这个例子中,返回时更新了HRESULT

1
2
mov eax, 0x80070057
ret

在这个例子中,0x80070057HRESULTE_INVALIDARG。因此,理论上返回值可以是这四种之一:

Error Value Bytecode
E_ACCESSDENIED 0x80070005 "\xB8\x05\x00\x07\x80\xC3"
E_HANDLE 0x80070006 "\xB8\x06\x00\x07\x80\xC3"
E_INVALIDARG 0x80070057 "\xB8\x57\x00\x07\x80\xC3"
E_OUTOFMEMORY 0x8007000E "\xB8\x0E\x00\x07\x80\xC3"

然而,这里存在风险。如果所讨论的EDR正在对内存区域进行完整性检查,那么它将注意到内存已被更改。从代码的角度来看,这是一个简单的计算。
假设修补程序如下:

1
var patch = new byte[] { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };

这个修补程序的长度为6个字节,因此读取前6个字节并存储它们。在某个事件(时间、操作等)时检查这些字节是否匹配。此外,还可以使用内核回调、ETW(Event Tracing for Windows)对AMSI.DLL内存修改进行检测等。因此,对于修改内存的可能性有相当高的检测方式。如果需要修补内存,建议先读取原始字节,应用修补程序,执行一些恶意操作,然后重新应用原始字节以处理完整性检查。

未来的AMSI绕过

我们在使用硬件断点(Hardware Breakpoints)矢量异常处理程序(Vectored Exception Handlers)方面取得了很大的成功。Ethical Chaos在《In-Process Patchless AMSI Bypass》中对这个过程进行了非常好的文档记录。然而,请记住,这种方法也是可以被检测到的。可以在这个gist中看到一个用于扫描设置断点的进程的概念验证(Proof-of-concept)。
在这里,我们不会演示如何使用这种方法,而是将其作为读者的任务。

结论

这篇文章涉及的只是两种本地防护措施,但篇幅相对较长。我们试图对EDR可以使用的机制进行解释,以便不仅可以识别恶意活动,还可以防止它。在这个过程中,我们讨论了常见的陷阱以及一些可以采取的增强措施来防止绕过。
通过这样做,我们试图对“X绕过EDR”的说法进行更多解释,即使植入物可能已经回来,但活动很可能已被记录。就像上周的博客一样,要完全避开防御机制是很困难的,而且更难的是在不记录这些绕过行为的情况下抵消这些保护措施。归根结底,从广义上讲,操作人员所做的一切都可以被记录下来。捍卫者的责任是确保这些事件被捕获并与他们的EDR、SOC和意识相连接。
下周我们将回到我们的植入物,探讨如何改进其静态操作安全性。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2023-8-29 16:46 被Max_hhg编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回