(反)EDR 概要

科技   2025-01-02 14:46   广东  

简介

什么是 EDR

EDR 是“端点检测和响应”的缩写。它是部署在每台机器上的代理,用于观察操作系统生成的事件以识别攻击。如果检测到某些东西,它将生成警报并将其发送到 SIEM 或 SOAR,由人工分析师进行查看。“响应”是指在识别威胁后执行的操作,例如隔离主机,这不是本文的一部分。EPP 是端点保护平台,它将尝试中断攻击,而不仅仅是检测攻击。

MDE (Microsoft Defender for Endpoint) 的 UI:

我们可以看到 EDR 检测到了某些东西,并试图向分析师提供有关该事件的更多信息:涉及的进程、它们的参数和哈希值、子进程等。分析师最终必须决定这是误报还是主动攻击。但一般来说,红队希望避免引起任何警报,并试图保持低调。

EDR 尝试在痛苦金字塔的更高层实施检测,主要是在TTP:工具、技术、程序。

理想化的 EDR

了解和理解哪怕只是一个 EDR 都很困难,而了解和理解所有的 EDR 则是不可能的。这里写的 EDR 是理想 EDR 的抽象版本。与其说是今天正在做的事情,不如说是理论上使用可用的 Windows 传感器/遥测基础设施可以实现什么。最接近的灵感是 Windows Defender for Endpoint (MDE),我用它来进行测试。

我不会教你如何绕过特定的 EDR,而是教你如何从概念上思考攻击面,以实施你自己的技术。EDR 的实际内部工作原理大多未知(Elastic 除外),并且被视为黑盒。虽然我们大多知道 EDR 会收到什么样的信息,但不太清楚这些信息在内部是如何被使用和关联的。

作为一名黑客,我们对系统的输入和输出很感兴趣。本文应该对输入进行概述。

Shellcode 加载器

加载程序将加载 shellcode。shellcode 通常是我们的信标,例如 CobaltStrike、Sliver 或 Metasploit。

加载程序包含加密的 shellcode,将其加载到内存中并执行。

目标是使该过程不会被 EDR 检测到初始访问 (IA)。

Shellcode 加载器示例

在执行shellcode时,通常的步骤如下:

  • 分配具有读写权限的内存区域

  • 将 shellcode复制到该区域(也对其进行解密)

  • 将内存区域的权限更改为读取-执行

  • 执行shellcode

在 C 语言中看起来像这样,但在大多数语言中都类似:

char *shellcode = "\xAA\xBB...";
char *dest = VirtualAlloc(NULL, 0x1234, 0x3000, p_RW);
memcpy(dest, shellcode, 0x1234)
VirtualProtect(dest, 0x1234, p_RX, &result)
(*(void(*)())(dest))(); // jump to dest: execute shellcode

这个简单的配方有很多变体,其中一些专注于远程进程的 shellcode 注入。其工作原理相同,在OpenProcess()目标进程上使用,并将其用作hProcess函数调用(如VirtualAlloc(hProcess, ...)WriteProcessMemory(hProcess, ...))的参数。EDR 会更严格地审查跨进程访问hProcess。

另一个典型的做法是通过创建新线程来调用 shellcode。无论是CreateThread()在您自己的地址空间中,还是CreateRemoteThread()用于进程注入或模块踩踏。

复制本身,这里由用户空间函数执行memcpy(),也可以用或其他函数完成RtlCopyMemory()

EDR 检测

检测加载器主要有三种技术:

  • 文件扫描:文件签名(“yara”)扫描

  • 内存扫描:签名(“yara”)扫描进程内存

  • 遥测/行为:进程执行的操作(主要通过操作系统)

例如,Windows Defender Antivirus 可实现 AV 扫描,而 Windows Defender for Endpoint MDE 是一种 EDR,它严重依赖遥测来执行行为分析。如果它觉得有必要,它也会扫描进程的内存。

我把这称为“Bubbles of Bane”:

C2 框架生成的大多数 .exe 文件植入程序都经过签名,因此没有用处。因此,第一步是混淆代码,这很难。有关示例,请参阅利用 Cobalt Strike 配置文件的强大功能来逃避 EDR 。

或者也可以使用加载器,它将植入物作为有效载荷携带并在执行时加载它。这种技术通常使用 C2 生成的 shellcode(或者,可以使用 C2 生成的 DLL 输出或 EXE。可以将其转换为 Shellcode 或 DLL,例如使用 Donut)。使用加载器的优点是可以加密有效载荷,因此唯一需要从 AV 文件签名扫描中混淆的是实际的加载器本身。

公共加载器通常迟早都会被签名。但它们很容易用 Windows 理解的基本上所有语言编写(C、.net C#、vba、vbs、powershell、jscript……)。简单的自写加载器出奇地有效,正如本文将展示的那样。

除了扫描文件之外,EDR 还可以扫描进程的内存。这样可以击败加载程序,因为有效载荷代码必须在内存中解密才能执行。为了避免在内存中检测到,进程需要在休眠时加密其内存区域。这样,当 EDR 扫描进程时,内存中就不应该有任何可疑内容。内存扫描是一项性能密集型操作,只有当 EDR 认为值得时才会执行。这是基于收集的遥测数据(或定期“按需”执行,例如每天一次)。

典型的内存扫描器是 pe-sieve 和 moneta

大多数检测用例都依赖于遥测:Windows 中的重要函数调用会生成事件,这些事件由 EDR 进行处理、关联和分析。例如更改内存区域的权限、创建进程和线程、复制内存等。

例如,如果我们使用加载程序绕过 AV,并简单地为我们的 shellcode 分配一个内存区域,我们就不会为 EDR 生成太多遥测数据。但有效载荷将被内存扫描仪检测到。如果我们引入内存加密来绕过内存扫描仪,那么我们会生成更多的遥测数据,进而可以用来检测内存加密。

带有艾克记忆加密的Bubbles of Bane

AV 签名扫描

当文件被写入磁盘时,AV 会对其进行扫描。AV 有一个包含已知恶意软件签名的数据库(如 yara 规则)。文件写入事件由操作系统生成,并通过 AMSI 或内核微过滤器传递给 AV。

签名扫描基于文件的静态内容。将解析 PE 标头并扫描 PE 部分的内容。它发生在 EXE 执行之前。一旦检测到,将在执行前删除该文件。

签名看起来类似于 yara 规则:

// https://github.com/Yara-Rules/rules/blob/master/malware/APT_APT17.yar (shortened)
rule APT17_Sample_FXSST_DLL
{
    meta:
        ...
    strings:
        $x1 = "Microsoft? Windows? Operating System" fullword wide
        $x2 = "fxsst.dll" fullword ascii
        $y1 = "DllRegisterServer" fullword ascii
        $y2 = ".cSV" fullword ascii
        $s1 = "VirtualProtect"
        $s2 = "Sleep"
        $s3 = "GetModuleFileName"
   
   condition:
        uint16(0) == 0x5a4d and filesize < 800KB and ( 1 of ($x*) or all of ($y*) ) and all of ($s*)
}

一个通用的解决方案是代码混淆,本文不会介绍它。它通常不能可靠地应用于编译后的代码,但需要纳入编译过程。这意味着每个工具都需要自己实现它。

它将解决我们所有的问题:磁盘或内存中没有签名,不需要加载它,因此没有遥测。

https://retooling.io/blog/an-unexpected-journey-into-microsoft-defenders-signature-world https://avred.r00ted.ch

AV 仿真

AV 组件还将执行目标二进制的模拟。

模拟意味着 AV 将自行读取和解释 .text 部分中的 ASM 指令。它不会在本机执行这些指令,它不是虚拟化执行,也不是 qemu/bochs 完整模拟。它是一种 CPU 模拟,包括常见的 Windows 系统调用和子系统。

伪代码如下:

asm_bytes = [
        0xB8, 0x04, 0x00, 0x00, 0x00, # mov eax, 4
        0xBB, 0x06, 0x00, 0x00, 0x00, # mov ebx, 6
        0x01, 0xD8                      # add eax, ebx
    ]

    asm_instructions = disassembler.disasm(asm_bytes);
    # asm_instructions = [
    # { name = "mov", src = "4", dst="eax" }
    # { name = "mov", src = "6", dst="ebx" }
    # { name = "add", src = "ebx", dst="eax" }
    # ]

    for instruction in asm_instructions:
      if instruction.name == "add":
        register[instruction.dst] += register[instruction.src]
      if instruction.name == "mov":
        ...

AV 仿真为 X86 汇编创建了自己的“解释器”,并重新实现了部分 Windows 操作系统系统调用,以及虚拟文件系统(FileOpen())、虚拟注册表RegOpen()、虚假进程等。该ntdll.dll函数 GetUserNameA()可以实现为始终返回“JohnDoe”。

RedTeamer 的示例经验:

  • 编写加载器

  • 插入 Metasploit shellcode

  • 文件被放到磁盘上时被检测到

然后:

  • 编写第二个加载器

  • 使用强 AES 加密 metasploit shellcode

  • 当放到磁盘上时仍能检测到

AV 模拟器将执行/模拟加载程序。一段时间后,执行停止,并在内存中发现 Metasploit shellcode 未加密。然后 AV 将在内存中检测它的签名。

检测模拟器的可能性是无限的。但一般来说,模拟器不会永远运行,而是受到以下限制:

参考:

  • Windows Offender:对 Windows Defender 的防病毒模拟器进行逆向工程 (视频)https://i.blackhat.com/us-18/Thu-August-9/us-18-Bulazel-Windows-Offender-Reverse-Engineering-Windows-Defenders-Antivirus-Emulator.pdf

接收事件

EDR 通过操作系统接收进程正在执行的事件:

接收数据的渠道主要有两个:

  • 用户模式(挂钩 API)

  • 内核回调(ETW、ETW-TI、内核模式驱动程序)

当添加/删除/更改某些内容时,这些传感器将创建有关系统正在发生的事情的事件,例如: 

  • 文件

  • 注册表项

  • 进程、线程

  • 内存区域

EDR 将包含与恶意行为事件相匹配的规则。规则可以是: 

  • 精确/脆弱:能很好地检测某一特定事物(低假阳性FP),容易绕过

  • 稳健性:更通用的检测,更难绕过,FP 更高,更多异常

请注意,EDR 本身无法看到进程内部的数据修改。换句话说,调用函数RtlCopyMemory()的进程ntdll.dll可能会生成遥测数据,因为ntdll.dll可以将其挂钩。在 for 循环中对字节复制执行相同操作不会产生任何遥测数据。

遥测数据可从挂钩ntdll.dll和内核获取。用户模式挂钩可以轻松删除,但这会生成遥测数据。内核空间事件更可靠,无法删除。

请注意,Windows 的主要执行单元是线程,而不是进程。但为了简单起见,我将主要使用进程。

该图形有点过于简单,可以通过更多传感器进行扩展,这些传感器是 EDR 的输入:

因此,EDR 输入为:

  • 用户模式钩子/AMSI

  • 内核回调

  • ETW

  • ETW-TI

我将对每一个问题分别进行讨论。

用户模式钩子

虽然 Linux 的官方内核接口是系统调用,但对于 Windows 来说,它的是ntdll.dll。这称为本机 API (NtAPI)。ntdll.dll将为我们调用正确的系统调用。Windows 应用程序接口 (WinAPI),其他 DLL 类似kernel32.dll,都在末尾使用或调用 NtAPI ( ntdll.dll)。请注意,系统调用号可能会因 Windows 版本而异,因此对它们进行硬编码并不可靠。

示例 NtAPI 函数ntdll.dll,使用 ASM 指令执行系统调用syscall:

SysNtCreateFile proc
      mov r10, rcx
      mov eax, 55h
      syscall
      ret
  SysNtCreateFile endp

典型的 WinAPI 调用,带有一个hook:

用户空间钩子只是ntdll.dll导出函数中的补丁,它在函数执行之前调用另一个 DLL。Windows 提供了直接钩子函数的功能。

Original Function On-Disk: EDR Hooked Function In-Memory:
 ---------------------- -----------------------

 mov r10, rcx mov r10, rcx
>mov eax, 50h jmp 0x7ffaeadea621
 test byte ptr [0x7FFE0h], 1          test byte ptr [0x7FFE0h], 1
 jne 0x17e76540ea5 jne 0x17e76540ea5
 syscall syscall
 ret                                     ret

常见挂钩ntdll.dll函数的示例:

EDR 接收函数调用名称及其参数作为遥测数据。

这是通过使用内核回调(PsSetCreateProcessNotifyRoutine)在早期阶段每当有新进程创建时得到通知,然后将 DLL 注入到该进程中(如amsi.dll),通过使用异步过程调用(kKAPC 注入)修补原始ntdll.dll函数以绕道而行来实现的amsi.dll

ntdll.dll修补后,每个函数调用都会被拦截amsi.dll

使用 KAPC 进行 EDR 函数挂钩将创建一个执行挂钩的 APC。“Early Bird APC 注入”技术使用相同的 APC 机制,因此可以在执行 KAPC 挂钩之前运行。

可以使用以下方法绕过用户模式钩子:

  • 直接系统调用(避免调用ntdll.dll

  • 间接系统调用(调用ntdll.dll函数,但在钩子之后)

  • 修补/恢复ntdll.dll(彻底移除挂钩)

用户模式钩子很容易被绕过,因为它们完全位于“我们自己的”内存空间中,我们可以自由地对其进行操作。但恢复ntdll.dll自身会产生遥测数据,这就是为什么要使用直接系统调用来实现这一点的原因。

EDR 不应仅依赖用户模式钩子,而应仅将其用于辅助遥测。但它们提供的信息比内核回调更多。内核回调仅“看到” syscall/ntdll.dll 函数,而不是最初启动的原始函数。这很有用,因为它可以生成更通用的检测,而无需挂钩所有奇怪和不寻常的 DLL 函数。但它可能会产生更多的误报,因为仅使用系统调用来识别“非恶意”行为更加困难。

例如,、 和CreateFileA()都会CreateFileW()在 最后调用。OpenFile()CreateFileTransacted()NtCreateFile()

请注意,调用堆栈可以显示链中最初调用了哪个函数。用户模式钩子越来越少使用,而且并非所有 EDR 都使用用户模式钩子( 来源):

内核遥测

Windows 操作系统以通知回调例程的形式提供有关进程的信息。尤其是有关进程、线程和映像创建的信息。它由内核本身生成,无法像使用用户模式挂钩(没有内核权限)那样抑制它们。

这些回调是在相关进程和线程的上下文中启动的。因此,事件包含有关原始进程的信息。

内核模式检测有多种不同的来源:

  • ETW(Windows 事件跟踪基础结构)

  • ETW-TI(线程智能)

  • 内核回调(PsSetCreateProcessNotifyRoutine 等)

  • NDIS / Minifilter 驱动程序(用于文件系统)

内核回调包括:

  • PsSetCreateProcessNotifyRoutine:进程创建、终止

  • PsSetCreateThreadNotifyRoutine:线程创建、删除

  • PsSetLoadImageNotifyRoutine:Windows 图像加载器

  • ObRegisterCallbacks:对象管理器回调,如 NtOpenProcess、NtOpenThread、NtOpenFile 等

参考:

https://blog.whiteflag.io/blog/from-windows-drivers-to-a-almost-complete-working-edr/

一个示例事件是PS_CREATE_NOTIFY回调,它向 EDR 提供不同的信息:

Sysmon 可以从内核捕获该事件,并会产生 以下内容:

Process Create:
RuleName: -
UtcTime: 2024-04-28 22:08:22.025

ProcessGuid: {a23eae89-bd56-5903-0000-0010e9d95e00}
ProcessId: 6228
Image: C:\Windows\System32\wbem\WmiPrvSE.exe
FileVersion: 10.0.22621.1 (WinBuild.160101.0800)
Description: WMI Provider Host
Product: Microsoft® Windows® Operating System
Company: Microsoft Corporation
OriginalFileName: Wmiprvse.exe
CommandLine: C:\Windows\system32\wbem\wmiprvse.exe -secured -Embedding
CurrentDirectory: C:\Windows\system32\

User: NT AUTHORITY\NETWORK SERVICE
LogonGuid: {a23eae89-b357-5903-0000-002005eb0700}
LogonId: 0x7EB05
TerminalSessionId: 1
IntegrityLevel: System
Hashes: SHA1=91180ED89976D16353404AC982A422A707F2AE37,MD5=7528CCABACCD5C1748E63E192097472A,SHA256=196CABED59111B6C4BBF78C84A56846D96CBBC4F06935A4FD4E6432EF0AE4083,IMPHASH=144C0DFA3875D7237B37631C52D608CB

ParentProcessGuid: {a23eae89-bd28-5903-0000-00102f345d00}
ParentProcessId: 580
ParentImage: C:\Windows\System32\svchost.exe
ParentCommandLine: C:\Windows\system32\svchost.exe -k DcomLaunch -p
ParentUser: NT AUTHORITY\SYSTEM

请注意,只有字段ImageFilename、CommandLine、ParentProcessId 直接转换为内核事件的Image、CommandLine、ParentProcessId。但大多数其他信息都是由 Sysmon 额外收集的。这些附加信息是通过查询内核(例如通过GetProcessInformation在 上发出 )收集的ProcessId。或者以其他方式收集,例如解析进程的 PEB。并非所有提供的信息都同样值得信赖。

使用 SilkETW 记录的ETWImageLoad事件Microsoft-Windows-kernel-Process:

{
  ProviderGuid: "22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716",
  ProviderName: "Microsoft-Windows-kernel-Process",
  EventName: "ImageLoad",
  ThreadID: 9584,
  ProcessID: 7536,
  ProcessName: "notepad",

  YaraMatch: [],
  Opcode: 0,
  OpcodeName: "Info",
  TimeStamp: "2024-07-08T19:06:10.8845667+01:00",
  PointerSize: 8,
  EventDataLength: 142,

  XmlEventData: {
    ProviderName: "Microsoft-Windows-kernel-Process",
    FormattedMessage: "Process 7’536 had an image loaded with name \Device\HarddiskVolume2\Windows\System32\notepad.exe. ",
    
    EventName: "ImageLoad"
    ProcessID: "7’536",
    PID: "7536",
    TID: "9584",
    
    PName: "",
    DefaultBase: "0x7ff631650000",
    ImageName: "\Device\HarddiskVolume2\Windows\System32\notepad.exe",
    ImageBase: "0x7ff631650000",
    ImageCheckSum: "265’248",
    ImageSize: "0x38000",

    MSec: "9705.0646",
    TimeDateStamp: "1’643’917’504",
  }
}

内存区域

启动 .exe 时,PE .exe 文件中的各个部分会被完全以块的形式复制到内存中。

.text包含汇编代码,而.data和类似的包含程序的数据。

可以使用或类似方法创建新的内存区域VirtualAlloc()

来自 PE 映像的内存区域称为备份区域。它们是值得信赖的,因为它们是 AV 在磁盘上扫描的 PE 文件的 1:1 副本。内存区域由磁盘上的文件“备份”。它也可以称为 IMAGE 区域。

如果进程通过分配来分配额外的内存,则该内存为“无备份”。也称为用户内存或私有内存。没有文件后端,因此它是“无备份的”。

一般认为,内存区域具有以下属性:

  • USER/PRIVATE/Unbacked:不良、潜在恶意的 shellcode

  • 图像/背景:很好,非常值得信赖

这主要是因为漏洞利用或进程注入的 shellcode 通常位于 PRIVATE 内存中。此外,线程应该从备份区域启动。PRIVATE RWX 内存更加可疑。

这里有一些 IMG 类型(IMAGE,backed)的可信内存区域:

这里有一些类型为 PRV (PRIVATE,无备份) 的不可信内存区域:内存区域损坏

内存页的一个属性是写入时复制 (COW)。内存扫描器能够检查内存页是否被写入,这对于只读 .text 部分和其他部分来说并不常见,因为这些部分应该在进程之间共享。Moneta 通过PSAPI_WORKING_SET_EX_BLOCKfromPSAPI_WORKING_SET_EX_INFORMATION结构使用它。首选仅数据攻击,例如针对 AMSI 补丁或 ETW 补丁。

参考:

  • https://www.trustedsec.com/blog/windows-processes-nefarious-anomalies-and-you-memory-regions

  • https://www.arashparsa.com/bypassing-pesieve-and-moneta-the-easiest-way-i-could-find/

  • https://www.outflank.nl/blog/2023/10/05/solving-the-unhooking-problem/

  • https://www.ired.team/offective-security/code-injection-process-injection/ntcreatesection-+-ntmapviewofsection-code-injection

内存扫描

内存签名扫描将检测内存中的恶意代码,无论是在 .text 还是数据部分(堆栈、堆、.data 等)。

它基本上与 AV 签名扫描相同;针对已知的恶意签名,对内存内容进行 grep 或 yara 检测。

内存扫描对性能要求很高。它不是持续进行的,而是依赖于触发器。

查询进程信息

EDR 在收到事件后,还将尝试丰富它:

  • 进程信息(如可执行文件名称和命令行参数)

  • 内存扫描(可能)

  • 处理图像文件扫描(很少)

EDR 不仅会接收事件,还会主动向操作系统查询更多信息。例如,在接收PS_CREATE_NOTIFY事件时,EDR 将获取有关创建事件的进程的更多信息,例如通过使用GetProcessInformation()或OpenProcess(),访问 PEB、参数或内存区域。或者访问ImageFileName并扫描原始 EXE 映像文件。

请注意,即使经过 SYSTEM 或 PPL,EDR 也是一个正常进程,并且拥有自己的专用内核驱动程序。凭借其 SYSTEM 权限,它可以收集有关几乎所有其他进程的信息。

以下是处理程序函数的一个例子PsSetCreateProcessNotifyRoutine:

void CreateProcessNotifyRoutine(HANDLE ppid, HANDLE pid, BOOLEAN create) {
    if (create) {
        PEPROCESS process = NULL;
        PUNICODE_STRING processName = NULL;

        // Retrieve the process name from the EPROCESS structure
        PsLookupProcessByProcessId(pid, &process);
        SeLocateProcessImageName(process, &processName);

        DbgPrint("MyDumbEDR: %d (%wZ) launched", pid, processName);
    }
}

处理函数仅接收pid进程的。为了显示图像名称,必须调用一些函数来访问 PEB 或 EPROCESS 结构。

数据存放在PEB(Process Environment Block,进程环境块GS:[0x60])中。它处于用户模式,可以自由操作。

  • 图像基地址

  • 已加载的 DLL

  • 工艺参数:

  • 图片名称

  • 参数

  • 环境变量

  • 工作目录

EPROCESS 是一个内核数据结构,不能直接操作(有时是间接操作):

  • 进程创建和退出时间

  • 进程 ID

  • 父进程 ID

  • PEB 地址

  • 图像文件名

  • 类似于PEB中的流程参数图像名称

  • 也可以在 SectionObject 中使用

流程信息数据结构

PEB:

typedef struct _PEB {
  BYTE Reserved1[2];
  BYTE BeingDebugged;
  BYTE Reserved2[1];
  PVOID Reserved3[2];
  PPEB_LDR_DATA Ldr;
  PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
  PVOID Reserved4[3];
  PVOID AtlThunkSListPtr;
  PVOID Reserved5;
  ULONG Reserved6;
  PVOID Reserved7;
  ULONG Reserved8;
  ULONG AtlThunkSListPtr32;
  PVOID Reserved9[45];
  BYTE Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE Reserved11[128];
  PVOID Reserved12[1];
  ULONG SessionId;
} PEB, *PPEB;

而ProcessParameters:

typedef struct _RTL_USER_PROCESS_PARAMETERS {
  BYTE Reserved1[16];
  PVOID Reserved2[10];
  UNICODE_STRING ImagePathName;
  UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

调用堆栈分析

当进程调用某个 Windows 函数时,可以找出导致此次调用的父函数。这称为调用堆栈。

EDR 可以选择检查启动函数或 API 调用的进程,并分析调用堆栈中是否存在可疑情况:

使用该技术可以检测多种攻击和绕过。但它对性能要求较高。

调用堆栈的起源应该来自备用内存中的内存区域,经过支持 DLL(例如user32.dll),然后 ntdll.dll,最后syscall执行实际指令。

Elastic 具有调用堆栈分析规则来识别:

  • 直接系统调用

  • 基于回调的逃避攻击

  • 模块踩踏

  • 从未支持的区域加载库

  • 从不受支持的区域创建的进程

如果调用来自不受支持的区域,则它很可能来自 shellcode。

调用堆栈分析通常不适用于所有 API 函数。Elastic 提到以下内容:

  • VirtualAlloc、VirtualProtect

  • MapViewOfFile、MapViewOfFile2

  • VirtualAllocEx,VirtualProtectEx

  • 队列用户APC

  • 设置线程上下文

  • 写入进程内存、读取进程内存

参考:

  • https://www.elastic.co/security-labs/upping-the-ante-detecting-in-memory-threats-with-kernel-call-stacks

  • https://www.elastic.co/security-labs/doubling-down-etw-callstacks

线程状态分析

线程可能因各种原因而处于休眠状态。通过调查状态以及线程如何由于其调用堆栈而进入休眠状态,我们发现了休眠信标或内存加密的指标。

清理(欺骗)调用堆栈NtDelayExecution()

如果正在使用内存加密,则通常通过调用以下任一方法使线程进入睡眠状态:

  • Kernelbase.dll!SleepEx

  • ntdll.dll!NtDelayExecution

对这些睡眠函数的调用有疑点:

  • 调用堆栈中的虚拟内存调用

  • 来源位于非支持内存区域

参考:

  • https://www.mdsec.co.uk/2022/07/part-1-how-i-met-your-beacon-overview/

性能影响

EDR 的性能至关重要。如果开发人员的机器在安装 10,000 个 NPM 包时速度很慢,人们就会转向 Apple,因为 Apple 的保护措施较少,而 Microsoft 不能允许这种情况发生。这是一个严重的问题,因此 Microsoft 引入了异步Dev Drive扫描。

如果检测可以直接应用于罕见事件(比如打开 lsass.exe 的进程句柄),则性能最不密集的操作。内存扫描可能涉及迭代或 yara 扫描兆字节的 .text 部分,这非常昂贵。扫描文件是最昂贵的,即使使用 SSD 也是如此。

大多数检测都介于两者之间:一个或多个事件包含可疑信息,从而导致更多关联。然后这些事件可能会启动内存扫描。

什么可能触发内存扫描?

VirtualAlloc()并且WriteProcessMemory()通常被称为函数。CreateRemoteThread()不仅不经常被调用,而且它还是潜在恶意行为的更明显指标。

EDR 攻击

EDR 接收来自大量传感器的事件,这些传感器的可信度各不相同。此外,所需的许多信息在事件本身中不可用,而必须在内核(KPROCESS、EPROCESS)或进程内存空间本身(例如包括命令行参数、父进程 ID 的 PEB)中或通过内核访问。

许多攻击都依赖于TOCTOU漏洞的事实:检查时间、使用时间。

命令行欺骗

EDR 可以检查新生成的进程是否存在潜在的恶意命令行参数,例如使用 mimikatz 时:mimikatz.exe "privilege::debug" "lsadump::sam"。即使我们重命名mimikatz.exe,参数privilege::debug也是一个非常明确的指标,误报率很低。

但在 Windows 中,命令行参数是可以伪造的。进程的命令行参数存储在相应进程的 PEB 中。此外,当我们创建新进程时,进程创建函数还将包含初始参数(要启动的 exe 的参数)。

因此我们基本上有两个地方可以放置命令行参数:

  • 在子进程的 PEB 中

  • 在子创建函数中:CreateProcessW(..., "command line args", ...)

在 PEB 中:

typedef struct _PEB {
  ...
    PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
  ...
}

typedef struct _RTL_USER_PROCESS_PARAMETERS {
  ...
  UNICODE_STRING ImagePathName;
  UNICODE_STRING CommandLine;
} *PRTL_USER_PROCESS_PARAMETERS;

由于 PEB 可通过其流程进行修改,因此其中的数据不可信。

EDR 向现有进程查询其命令行,并且通常盲目信任它:

但可以验证一下,当父进程调用CreateProcess()创建子进程时:

EDR 可以比较命令行CreateProcess()和结果子进程的 PEB,如果它们不匹配则发出警报。

拦截函数调用参数实际上CreateProcessW(..., "command line args", ...) 也没有多大帮助,因为我们可以使用虚假参数创建处于暂停状态的进程,然后远程用正确的参数覆盖它们,然后恢复该进程。

  1. 父级:使用虚假参数创建新的暂停进程

  2. EDR:接收带有虚假参数的事件

  3. 父级:使用真实参数覆盖子级的 PEB

  4. 父进程:继续(启动)子进程(使用实参数)

  5. 子进程:再次用假参数覆盖其 PEB

  6. EDR:查询进程获取虚假参数

如果 EDR 将来认为子进程是恶意的,它将向分析师提供信息,包括从 PEB 中获取的进程的命令行参数。因此,子进程需要再次覆盖 PEB,作为“清理”。

因此,进程的命令行参数是相当不可信的。

PPID 欺骗

在 Windows 中,与 Linux 不同,父进程和子进程之间没有依赖关系,因为没有fork()。子进程从父进程获取某些属性,包括父进程的 PID。它也将存储在进程的 EPROCESS 结构中。

CreateProcessW()可以指示该函数在STARTUPINFOEX结构中提供其自身的属性,包括子进程的父进程。因此,在创建时,我们可以为子进程指定错误的父进程 PID。

CreateProcessW()界面:

BOOL CreateProcessW(
  [in, optional] LPCWSTR lpApplicationName,
  [in, out, optional] LPWSTR lpCommandLine,
  [in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in] BOOL                  bInheritHandles,
  [in] DWORD dwCreationFlags,
  [in, optional] LPVOID lpEnvironment,
  [in, optional] LPCWSTR lpCurrentDirectory,
  [in] LPSTARTUPINFOW lpStartupInfo, // PPID spoofing here
  [out] LPPROCESS_INFORMATION lpProcessInformation
);

实际的 PPID 欺骗只是设置属性struct STARTUPINFOEX并将其作为lpStartupInfo参数:

{
  STARTUPINFOEXA si;
  HANDLE fakeParent = OpenProcess(.., <pid of fake parent process>);
  ..
  UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &fakeParent, ..);
  CreateProcessA(NULL, (LPSTR)"notepad", .., EXTENDED_STARTUPINFO_PRESENT, .., &si.StartupInfo, ..);
}

在哪里:

typedef struct _STARTUPINFOEXA {
  STARTUPINFOA StartupInfo;
  LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; // attributes, one is the ppid
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;

它将存储在EPROCESS内核结构中:

typedef struct _EPROCESS
{

    KPROCESS Pcb;
    ...
    HANDLE InheritedFromUniqueProcessId; // PPID
    ...
}

EDR 可以使用以下命令检索NtQueryInformationProcess():

__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in] HANDLE ProcessHandle,
  [in] PROCESSINFOCLASS ProcessInformationClass,
  [out] PVOID ProcessInformation, // PROCESS_BASIC_INFORMATION
  [in] ULONG ProcessInformationLength,
  [out, optional] PULONG ReturnLength
);

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS ExitStatus;
    PPEB PebBaseAddress;
    ULONG_PTR AffinityMask;
    KPRIORITY BasePriority;
    ULONG_PTR UniqueProcessId;
    ULONG_PTR InheritedFromUniqueProcessId; // PID
} PROCESS_BASIC_INFORMATION;

可以检测到 PPID 欺骗,因为在创建进程时,会向 EDR 传递有关新进程的事件。此事件通常位于原始进程的上下文中,或者在其中引用该进程。然后,EDR 可以将结构的内容 STARTUPINFOEX与事件来自的进程进行比较(例如,只需比较两者的 PID)。在这里,EDR 看到CreateProcess()PPID=y 的调用(2),以及发起此调用的进程的有效 PID(1),其 PID=x。

因此,EDR 具有:

  1. 父级:PID

  2. 父进程:PPID 在其发出的CreateProcess()发往子进程的调用中

  3. 子代:其 PPID

并比较它们,尤其是 1) 和 2)。或者后面的 1/2 和 3。对于收到的事件,原始 PID 来自哪里并不总是完全清楚的(例如 ETW)。

请注意,它InheritedFromUniqueProcessId存储在 EPROCESS 中,但仍然不可信任,因为它可以从用户空间进行设置。

ETW 补丁

ETW 补丁将覆盖EtwEventWrite(),ntdll.dll因此该进程将不再自行发出任何 ETW 事件。这主要适用于 Powershell 和 .NET 相关事件。它通常涉及:

  • VirtualProtect .文本:RX -> RW

  • 覆盖内存(用 替换函数体return 0)

  • VirtualProtect .文本:RW -> RX

更改权限ntdll.dll进行修改可能会产生比修补 ETW 避免的更多的遥测数据。其内存权限需要从 RX 更改为 RW,然后再改回 RX。
请注意,这只会影响修补进程生成的事件。ETW 无法全局停用。
ETW 事件主要用于托管进程(DotNet、C#)和 Powershell。ETW 以前被 Sysmon 大量使用,因此 ETW-patch 是针对 Sysmon 的。
参考:
  • https://jsecurity101.medium.com/understanding-etw-patching-9f5af87f9d7b
  • https://jsecurity101.medium.com/refining-detection-new-perspectives-on-etw-patching-telemetry-e6c94e55a9ad
AMSI-AV 修补
AMSI 将扫描在受支持的 Windows 解释器(如 Powershell、MS Office VBA 运行时或 .NET)中执行的脚本。换句话说,应用程序本身要求操作系统通过 AMSI 对其打算执行的某个文件或缓冲区执行 AV 扫描。
要禁用 AMSI 运行时代码扫描,例如修补amsi.dll!AmsiOpenSession以删除遥测。替代方案是AmsiScanString() / AmsiScanBuffer()。
该过程与ETW-patch相同:使代码部分可写,破坏功能,再次恢复原始权限。

禁用 AMSI-AV 功能通常由加载程序在执行签名良好的恶意托管代码或 Powershell 脚本之前完成。加载程序正在接受扫描,但运行时加载的 .NET/Powershell 不会被扫描。

这对于在 powershell 中加载签名的恶意 powershell 脚本非常有用,否则该脚本将被 AMSI 接口扫描。一个著名的生成混淆 AMSI-AV 补丁的网站是https://amsi.fail。

AMSI-hooks 修补

ntll.dllAMSI-hook 修补(或 AMSi 修补)只是删除调用 的EDR补丁amsi.dll。它与 ETW 补丁或 AMSI-AV 补丁基本相同,因为它只是ntdll.dll 再次修改。它可以生成额外的遥测数据,例如从磁盘加载干净版本的 时ntll.dll。

参考:

  • https://github.com/ZeroMemoryEx/Amsi-Killer

  • https://github.com/Vixx/AMSI-BYPASS

AMSI 绕过

AMSI 绕过既可以指绕过上述的 AMSI-AV 接口,也可以指调用 OS 内核函数而不调用ntdll.dll其中的钩子。

这可以通过使用直接系统调用来完成:如果您知道正确的系统调用号,则可以直接调用它,而无需涉及ntdll.dll。

ntdll.dll或者对于间接系统调用:在钩子调用之后重新使用部分函数。

在这两种情况下,AMSI-hook 都会被绕过,并且 EDR 将不会获得任何遥测数据。

如果这是带有 hooked 的正常函数调用图ntdll.dll:

这里与:

  • 直接系统调用:自己执行系统调用(使用正确的系统调用号)

  • 间接系统调用:重用 hooked 的部分ntdll.dll,调用系统调用但不调用钩子

ntdll.dll或者完全用磁盘中未挂钩的版本替换,就像在 RefleXXion 中一样。

参考:

  • https://eversinc33.com/posts/avoiding-direct-syscalls.html

  • https://www.outflank.nl/blog/2019/06/19/red-team-tropics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/

  • https://passthehashbrowns.github.io/hiding-your-syscalls

  • https://github.com/JustasMasiulis/inline_syscall

  • https://github.com/jthuraisamy/SysWhispers2

  • https://github.com/klezVirus/SysWhispers3

  • https://alice.climent-pommeret.red/posts/direct-syscalls-hells-halos-syswhispers2

图像欺骗

与欺骗参数类似,攻击者可能还想“欺骗”exe:启动 EDR 记录的非恶意 exe(如 notepad.exe),然后用恶意 exe(如 mimikatz)替换进程内容。这会试图欺骗 EDR,让其认为已启动非恶意程序。这可以绕过简单的 EDR。

源 .exe 文件被称为进程的映像。

工艺镂空:

还有一些其他的技术:

  • Process Hollowing:使用以下代码覆盖已暂停进程的进程内存:WriteProcessMemory()

  • 进程复制:使用事务性 NTFS (TxF) 覆盖文件,启动进程,然后回滚事务,以恢复原始文件

  • Process Herpaderping:将恶意代码写入 exe,创建进程,在扫描之前快速用非恶意内容替换恶意内容

  • 进程重影:创建空文件,将其半删除,写入恶意数据,然后从中创建进程

内存扫描将使用签名(如 AV)扫描进程的内存。因此,即使注入了真正的进程,仍可以识别出像 CobaltStrike 这样的恶意代码。

或者通过将进程内存内容与 exe 文件内容进行比较。原始 exe 名称存储在 PEB(peb.ProcessParameters.ImagePathName)或内核的 EPROCESS 结构(eprocess.ImageFilename[15],eprocess.SeAuditProcessCreationInfo.ImageFileName)中。将内存内容与文件内容进行比较会耗费大量性能。

或者,EDR 可以收集识别操作的遥测数据。或者使用直接系统调用之类的支持技术,例如使用调用堆栈分析。

挖空参考:

  • https://www.ired.team/offective-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations

  • https://github.com/m0n0ph1/Process-Hollowing

  • https://www.darkrelay.com/post/demystifying-hollow-process-injection

模块踩踏(Module Stomping)

这与图像欺骗类似,但使用 DLL。

模块踩踏将 shellcode 写入远程进程中未使用的 DLL 的 .text 部分,并从那里开始创建新的线程。

与图像欺骗相同,可以通过以下方式检测:

  • 内存签名扫描

  • .text 部分的内存/文件比较

  • 踩踏遥测

  • 识别支持技术,例如使用遥测的直接/间接系统调用

参考:

  • https://www.blackhillsinfosec.com/dll-jmping/

  • https://blog.f-secure.com/hiding-malicious-code-with-module-stomping/

  • https://blog.f-secure.com/hiding-malicious-code-with-module-stomping-part-2/

  • https://trustedsec.com/blog/loading-dlls-reflections

  • https://williamknowles.io/living-dangerously-with-module-stomping-leveraging-code-coverage-analysis-for-injecting-into-legitimately-loaded-dlls/

  • https://notes.dobin.ch/#root/PBXfEsTWGbEg/yFUsQJlBd3r0/iMYKnoX7AZ7w/W5TwpJ5or9DW-dRWk

内存加密

可以在休眠前加密所有可疑区域,并在进程恢复时再次解密。这并非易事,需要非常小心、奇怪的 Windows 功能以及有效负载(例如信标本身)的支持。它可以创建大量遥测数据,但其中大部分都无法被 EDR 很好地捕获。

信标通常Sleep()会持续一段时间。如果它使用内存加密,则在此期间执行的任何扫描都只会看到加密内存。

调用堆栈欺骗

调用堆栈基本上是一个函数调用层次结构:一个函数列表,每个函数都由其前面的函数调用。当进程调用系统调用(或挂钩ntdll.dll函数)时,EDR 可以检索并分析此列表。

当使用直接系统调用、间接系统调用或其他诡计时,调用堆栈默认看起来“错误”,这可以通过 EDR 识别。

调用堆栈欺骗可确保调用堆栈再次看起来真实。它是一种支持技术:例如,可以使用调用堆栈检测 AMSI 绕过,因此我们需要改进 AMSI 绕过,使调用堆栈看起来更自然。

实际的调用堆栈欺骗通常不会生成遥测数据,并且可以相当安全地实现。但通过重新使用现有的调用堆栈欺骗实现,可以通过签名扫描(无论是在磁盘上还是在内存中)来识别它。

可疑的调用堆栈NtDelayExecution():

清理(欺骗)调用堆栈NtDelayExecution():

反检测依赖于伪造调用堆栈、复制干净的调用堆栈或隐藏恶意调用堆栈。存在许多检查调用堆栈完整性的技术,通常是通过与其他信息关联。例如,线程起始地址应该来自合理的位置。

在普通线程中,用户模式起始地址通常是线程堆栈中的第三个函数调用 - 位于 ntdll!RtlUserThreadStart 和 kernel32!BaseThreadInitThunk 之后。因此,当线程被劫持时,调用堆栈中这一点将显而易见。对于“早起的” APC 注入,调用堆栈的基础将是 ntdll!LdrInitializeThunk、ntdll!NtTestAlert、ntdll!KiUserApcDispatcher,然后是注入的代码。

参考:

  • https://sabotagesec.com/gotta-catch-em-all-catching-your-favorite-c2-in-memory-using-stack-thread-telemetry/

  • https://trustedsec.com/blog/windows-processes-nefarious-anomalies-and-you-threads

  • https://www.mdsec.co.uk/2022/07/part-1-how-i-met-your-beacon-overview/

  • https://gist.github.com/jaredcatkinson/23905d34537ce4b5b1818c3e6405c1d2

  • https://whiteknightlabs.com/2024/04/30/sleeping-safely-in-thread-pools/

  • https://oldboy21.github.io/posts/2024/06/sleaping-issues-swappala-and-Reflective-dll-friends-forever/

  • https://oldboy21.github.io/posts/2024/05/swappala-why-change-when-you-can-hide/

  • https://kyleavery.com/posts/avoiding-memory-scanners/

远程进程

攻击者可以选择干扰自己的进程,还是干扰系统中的另一个进程。此处描述的 Windows 函数大多也可用于另一个进程,只需OpenProcess()先使用即可。

这主要用于进程注入。迁移到另一个进程(如teams.exe)非常有用。它的C2可以隐藏在应用程序的正常通信中,它是JavaScript,因此有很多RW->RX分配。

EDR 会更严格地审查与远程进程的交互,因此留在自己的进程中更安全。对于迁移,请使用 DLL 侧载或其他不依赖OpenProcess() 某些东西的技术。

其中包括:

  • VirtualAllocEx() / VirtualFreeEx()

  • 读取进程内存() / 写入进程内存()

  • 创建远程线程()

  • 查询信息处理()/Nt查询信息处理()

暂停进程

一种非常常见的方法是创建一个带有参数的暂停进程CREATE_SUSPEND,然后对其进行处理,然后让其执行/恢复。

CreateProcessA("C:\\Windows\\System32\\calc.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
...
ResumeThread(pi.hProcess);

许多技术都依赖于此功能。目前使用暂停进程似乎不会对 EDR 造成太大影响,但这可能会在未来改变它。

例如,我们可以创建一个处于挂起状态的新进程,并排队 APC 来执行我们的 shellcode,这可能使其对 EDR 不可见(因为它可能在 KAPC 注入之前执行)。

结尾

EDR 智慧

  • 使用威胁检查或 avred 来识别你的资料中哪些部分被 AV 识别,然后对其进行修补

  • 内存扫描需要耗费大量的性能,通常需要触发才能执行

  • 用户模式 AMSI 越来越不重要,因此 AMSI-hooks 修补也很重要

编写加载器时的错误

  • 使用函数调用复制内存

  • 投入超过最低限度的努力来处理熵

  • 投入超过最低限度的努力来处理加密

  • 生成过多遥测数据

  • 线程未在备用内存中启动

  • 再次标记 RX 页面 RW

  • 调用栈不干净

建议的加载器

建议的装载机布局:

  • EXE 文件:所有代码都应包含在 .text 部分 (IMAGE) 中

  • 执行护栏:仅允许其在预期目标上执行(反中间盒)

  • 反模拟:阻止 AV 模拟我们的二进制文件(内存使用情况、CPU 周期数、时间欺骗……)

  • EDR 风水:通过使用非恶意数据和免费数据进行大量 Alloc/Copy/VirtualProtect 循环来调节 EDR

  • 有效载荷:加密(如何加密无所谓)

  • Alloc/Decode/Virtualprotect/Exec:尽可能正常(避免在此处使用 DLL 函数)。避免 RWX。

  • 有效载荷执行:尽可能正常(跳转到有效载荷,避免创建新线程)


感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里

Ots安全
持续发展共享方向:威胁情报、漏洞情报、恶意分析、渗透技术(工具)等,不会回复任何私信,感谢关注。
 最新文章