反EDR(Anti-EDR)纲要

aw4ker2024-12-03文章来源:SecHub网络安全社区


(反)EDR 纲要

2024 年的 EDR 功能和绕过,重点是未检测到的 shellcode 加载程序。

目前,人们非常关注植入物的内存加密

  • SWAPPALA / SLE(A)PING
  • Thread Pool / Pool Party 线程池 / 池派对
  • Gargoyle
  • Ekko
  • Cronos
  • Foliage

此外,还有很多工作涉及调用堆栈欺骗

  • ThreadStackSpoofer(线程堆栈欺骗器)
  • CallStackSpoofer(调用 StackSpoofer)
  • AceLdr
  • CallStackMasker(调用 StackMasker)
  • Unwinder
  • TitanLdr

这很酷,但如果我告诉你这不是绝对必要的呢?请继续阅读。

这是三篇系列文章的一部分:

目标受众是困惑的红队人员。建议具备对抗EDR(anti-EDR)和 maldev 的基本知识。

我不是 EDR 专家。我刚刚阅读了 Matt Hand 和 Elastic Security-Labs 的《逃避 EDR》,您也应该阅读。

我在 HITB BKK 2024 的一次演讲中提到了其中的部分内容:我的第一个和最后一个 shellcode 加载器

介绍

什么是 EDR

EDR 是“端点检测和响应”( “Endpoint Detection and Response”)。它有一个部署在每台计算机上的Agent代理,用于观察操作系统生成的事件以识别攻击。如果检测到某些内容,它将生成警报并将其发送到 SIEM 或 SOAR,在那里由人工分析师进行查看。“响应”是指在识别威胁后执行的操作,例如隔离主机,这不属于本文的一部分。EPP是终端保护平台(Endpoint Protection Platform),它将尝试中断攻击,而不仅仅是检测攻击。

MDE (Microsoft Defender for Endpoint) 的 UI:

mdeguioverview.png

我们可以看到 EDR 检测到了一些东西,并尝试向分析师提供有关事件的更多信息:涉及的进程、它们的参数和哈希、子进程等。最后的分析师必须研判这是误报还是主动攻击。但总的来说,红队人员都希望避免触发任何警报,并试图保持低调。

EDR 试图在疼痛金字塔的较高位置实施检测,主要是在 TTP 上:工具、技术、程序。

pyramidofpain.jpg

理想化的 EDR

即使只知道和理解一个 EDR 也很困难,所以想完全理解所有EDR基本是不可能的。这里写的 EDR 是理想 EDR 的抽象版本。与其说是今天正在做的事情,不如说是可用的 Windows 传感器/监控基础设施在理论上可能发生的事情。最接近的灵感来自我用于测试的 Windows Defender for Endpoint (MDE)。

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

作为一个黑客,我们对系统的输入和输出感兴趣。本文将概述输入。

Shellcode 加载器

加载器(loader)将加载 shellcode。shellcode 通常是我们的信标(Beacon),例如 CobaltStrike、Sliver 或 Metasploit。

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

┌───────────┐   ┌────────────┐    ┌────────┐
│           │   │            │    │        │
│  Loader   ├──►│ C2 Beacon  ├───►│ Profit │
│           │   │ Shellcode  │    │        │
│           │   │            │    │        │
└───────────┘   └────────────┘    └────────┘

目标是使 EDR 无法检测到初始访问 (IA) 的进程。

Shellcode 加载器示例

执行 shellcode 时,通常的步骤是:

  • 分配具有读写权限的内存区域
  • shellcode 复制到该区域(也解密它)
  • 内存区域的权限更改为 read-execute
  • 执行 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 ┌──────────┐ ┌───────────────┐ │ │ ┌─────────────────┐ │ Memory Region │ │ │ │ Alloc │ │ │ │ │ │ ├────────►│ │ │ │ └─────────┬───────┘ │ │ │ │ │ │ │ │ │ ┌─────────▼───────┐ │ │ │ Payload │ │ Copy & Decrypt ├─────────► │ │ ├─────►│ │ │ │ │ │ └─────────┬───────┘ │ │ │ │ │ │ │ │ │ ┌─────────▼───────┐ │ │ │ │ │ Make Executable ├────────►│ │ │ │ │ │ │ │ │ │ └─────────┬───────┘ │ │ │ │ │ │ │ │ │ ┌─────────▼───────┐ │ │ │ │ │ Execute ├─────────► │ │ │ │ │ │ │ │ │ └─────────────────┘ │ │ └──────────┘ └───────────────┘

这个简单的配方有很多变体,其中一些专注于远程进程上的 shellcode 注入。通过在目标进程上使用 OpenProcess() 来工作,并将其用作函数调用(如 VirtualAlloc(hProcess, ...)WriteProcessMemory(hProcess, ...) .``使用 hProcess 的跨进程访问受到 EDR 的更多审查。

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

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

EDR 检测

Bubbles Of Bane诅咒泡沫

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

  • 文件扫描:文件的签名 (“yara”) 扫描
  • 内存扫描:签名 (“yara”) 扫描进程内存
  • 监控/行为:进程执行的操作(主要通过操作系统)

例如,Windows Defender 防病毒实现 AV 扫描,而 Windows Defender for Endpoint MDE 是严重依赖监控来执行行为分析的 EDR。如果它觉得需要,它也会扫描进程的内存。

我称之为“诅咒泡沫”:

            ┌───────────────────┐
            │         Memory    │
┌───────────┼─────┐   Scanning  │
│ AV        │     │             │
│ Signature │     │             │
│ Scanning  │     │             │
│       ┌───┼─────┼────────┐    │
│       │   │     │        │    │
│       │   └─────┼────────┼────┘
│       │         │        │     
└───────┼─────────┘        │     
        │                  │     
        │    Telemetry     │     
        │    Behaviour     │     
        │    Analysis      │     
        │                  │     
        └──────────────────┘     

C2 框架开箱即用的大多数 .exe 文件植入都是签名的,因此没有用。因此,第一步是混淆代码,这很困难。有关示例,请参阅 利用 Cobalt Strike Profiles 的强大功能进行 EDR 规避

或者使用加载程序,该加载程序将植入程序作为有效负载携带,并在执行时加载它。大多数情况下,此技术使用 C2 生成的 shellcode (或者,可以使用 C2 或 EXE 生成的 DLL 输出。可以将其转换为 Shellcode 或 DLL,例如使用 Donut)。使用 loader 的优点是有效负载可以加密,因此唯一需要从 AV 文件签名扫描中混淆的是实际的 loader 本身。

Public loader 通常迟早会签名。但是它们很容易用 Windows 理解的所有语言(C、.net C#、vba、vbs、powershell、jscript…正如本文将展示的那样,简单的自编写加载器非常有效。

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

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

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

例如,如果我们使用加载程序绕过 AV,并简单地为 shellcode 分配一个内存区域,则不会为 EDR 生成太多监控数据。但是 payload 将被内存扫描器检测到。如果我们引入内存加密来绕过内存扫描器,那么我们会生成更多的监控数据,这反过来又可用于检测内存加密。

使用 Ekko 内存加密的 Bubbles of Bane诅咒泡沫:

            ┌───────────────────┐
            │         Memory    │
┌───────────┼─────┐   Scanning  │
│ AV        │     │             │
│ Signature │     │             │
│ Scanning  │     │             │
│       ┌───┼─────┼────────┐    │
│       │   │     │ [EKKO] │    │
│       │   └─────┼────────┼────┘
│       │         │        │     
└───────┼─────────┘        │     
        │                  │     
        │    Telemetry     │     
        │    Behaviour     │     
        │    Analysis      │     
        │                  │     
        └──────────────────┘     

AV 签名扫描

当文件写入磁盘时,AV 将对其进行扫描。AV 有一个包含已知恶意恶意软件(如 yara 规则)的签名数据库。文件写入事件由 OS 生成,并通过 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*)
}

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

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

            ┌───────────────────┐
            │         Memory    │
┌───────────┼─────┐   Scanning  │
│ AV        │     │             │
│ Signature │     │             │
│ Scanning  │     │             │
│       ┌───┼─────┼────────┐    │
│       │   │Obfus│        │    │
│       │   │catio│        │    │
│       │   │n    │        │    │
│       │   └─────┼────────┼────┘
│       │         │        │     
└───────┼─────────┘        │     
        │                  │     
        │    Telemetry     │     
        │    Behaviour     │     
        │    Analysis      │     
        │                  │     
        └──────────────────┘     

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 的示例体验:

  • 编写 loader
  • 插入 Metasploit shellcode
  • 在磁盘上放置时检测到文件

然后:

  • 编写第二个加载程序
  • 使用强 AES 加密 metasploit shellcode
  • 在磁盘上放置时仍可检测到它

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

检测 Emulator 的可能性是无限的。但通常,仿真不会永远运行,而是受到以下限制:

什么 典型限制
时间 ?
指令数量 ?
API 调用次数 ?
使用的内存量 ?

参考:

接收事件

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

 Process                                                 
┌────────────────┐                    ┌─────────────┐    
│                │                    │             │    
│                │                    │  Windows    │    
│                │                    │  kernel     │    
├────────────────┤  Syscalls          │             │    
│ (Hooked)       ├───────────────────►│             │    
│                │                    │             │    
│ ntdll.dll      ├─────────────────┐  │             │    
│ NtApi          │   Usermode      │  │             │    
├────────────────┤   Hooks         │  └──────┬──────┘    
│                │                 │         │           
│                │                 │         │ kernel
│                │                 │         │ callbacks     
│                │                 │         │           
│                │                 ▼         ▼           
│                │          ┌────────────────────────┐   
│                │          │      EDR               │   
│                │          └────────────────────────┘   
└────────────────┘                                        

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

  • 用户模式(挂钩 API)
  • 内核回调(ETW、ETW-TI、内核模式驱动程序)

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

  • 文件
  • 注册表项
  • 进程、线程
  • 内存区域

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

  • Precise/Brittle(精准/特性检测):很好地检测一个特定事物(低假阳性 FP),易于旁路
  • Robust(强相关检测):更通用的检测、更难绕过、更高的 FP、更多的异常

请注意,EDR 本身不会在流程中看到数据修改。或者换句话说,调用 ntdll.dll 的函数 RtlCopyMemory() 的进程可能会生成监控数据,因为ntdll.dll可以挂接。在 for 循环中对逐字节副本执行相同的操作不会导致任何监控。

监控数据是从 hooked ntdll.dll 和 kernel 获取的。UserMode 钩子可以很容易地删除,但这会生成监控数据。kernelspace 事件更值得信赖,并且无法删除。

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

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

                                                                     ┌──────────────┐          
                                                                     │              │          
        ┌─────────────┐ EtwWrite() ┌──────────┐   Kernel callbacks   │              │          
        │ Process     ├───────────►│          ├─────────────────────►│              │          
        │             │            │          │                      │              │          
        │             │            │          │                      │              │          
        ├─────────────┤            │   OS     │   ETW                │              │          
┌───────┤  ntdll.dll  │            │          ├─────────────────────►│              │          
│       │             │ syscall    │          │                      │              │          
│  ┌───►│             ├───────────►│          │   ETW-TI             │    EDR       │          
│  │    ├─────────────┤            │          ├─────────────────────►│              │          
│  │    │             │            └──────────┘                      │              │          
│  │    ├─────────────┤                                              │              │          
│  │    │ amsi.dll    │ pipe                      AMSI               │              │          
│  └────┤             ├─────────────────────────────────────────────►│              │          
│       │             │                                              │              │          
└──────►│             │                                              │              │          
        ├─────────────┤                                              │              │          
        │             │                                              │              │          
        │             │                                              │              │          
        │             │                                              │              │          
        │             │                                              │              │          
        └─────────────┘                                              └──────────────┘          

因此,EDR 输入为:

  • Usermode 钩子 / AMSI
  • 内核回调
  • ETW
  • ETW-TI 系列

我将单独讨论它们中的每一个。

Usermode 钩子

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

 WinAPI                                       NtApi                                 Kernel                  
┌─────────────────────────────────────────┐  ┌───────────────────────────────────┐                          
│                                         │  │                                   │                          
│                                         │  │                                   │                          
│ ┌────────────────┐   ┌────────────────┐ │  │ ┌─────────────────────────┐       │ ┌───────────────────────┐
│ │                │   │                │ │  │ │                         │syscall│ │                       │
│ │ kernel32.dll   ├──►│ kernelbase.dll ├─┼──┤►│ ntdll.dll               ├───────┤►│Kernel                 │
│ │ OpenProcess    │   │ OpenProcess    │ │  │ │ NtOpenProcess           │       │ │NtOpenProcess          │
│ │                │   │                │ │  │ │                         │       │ │                       │
│ └────────────────┘   └────────────────┘ │  │ └─────────────────────────┘       │ └───────────────────────┘
│                                         │  │                                   │                          
│                                         │  │                                   │                          
│ ┌────────────────┐   ┌────────────────┐ │  │ ┌─────────────────────────┐       │ ┌───────────────────────┐
│ │                │   │                │ │  │ │                         │syscall│ │                       │
│ │ kernel32.dll   ├──►│ kernelbase.dll ├─┼──┤►│ ntdll.dll               ├───────┼─►Kernel                 │
│ │ VirtualAllocEx │   │ VirtualAllocEx │ │  │ │ NtAllocateVirtualMemory │       │ │NtAllocateVirtualMemory│
│ │                │   │                │ │  │ │                         │       │ │                       │
│ └────────────────┘   └────────────────┘ │  │ └─────────────────────────┘       │ └───────────────────────┘
│                                         │  │                                   │                          
│                                         │  │                                   │                          
└─────────────────────────────────────────┘  └───────────────────────────────────┘                          
       ▲                                         ▲                                      ▲                   
       │                                         │                                      │                   
       │                                         │                                      │                   
    Usermode Hooks                            Usermode Hooks                         Kernel                 
    Specific                                  Generic                                Callbacks              

ntdll.dll 中的 NtAPI 函数示例,使用 ASM 指令 syscall 执行 syscall

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

典型的 WinAPI 调用,带有钩子:

                                                                                ┌─────────────────┐
                                                                                │                 │
┌───────────────────┐   ┌─────────────────┐   ┌───────────────────┐             │                 │
│                   │   │                 │   │                   │             │      OS         │
│  Application.exe  │   │ kernel32.dll    │   │  ntdll.dll        │  syscall    │                 │
│                   ├──►│                 ├──►│                   ├────────────►│                 │
│  .text            │   │ CreateFile()    │   │  NtCreateFile()   │             │      kernel     │
│                   │   │                 │   │                   │             │                 │
└───────────────────┘   └─────────────────┘   └─────────┬─────────┘             │                 │
                                                        │hook                   │                 │
                                                        │                       │                 │
                                               ┌────────▼────────────────┐      │                 │
                                               │                         │      │                 │
                                               │ amsi.dll                │      │                 │
                                               │                         │      │                 │
                                               │ NtCreateFile_Hook()     │      │                 │
                                               └─────────────────────────┘      │                 │
                                                         │                      └─────────────────┘
                                                         ▼
                                                        EDR

用户空间钩子只是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函数示例:

函数名称 相关攻击者技术
NtOpenProcess 进程注入
NtAllocateVirtualMemory 进程注入
NtWriteVirtualMemory 进程注入
NtCreateThreadEx 进程注入
NtSuspendThread 线程 APC Shellcode 注入
NtResumeThread 线程 APC Shellcode 注入
NtQueueApcThread 线程 APC Shellcode 注入

EDR 以监控数据的形式接收函数调用名称及其参数。

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

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

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

Usermode 钩子可以通过以下方式绕过:

  • 直接系统调用(避免调用 ntdll.dll
  • 间接系统调用(调用 ntdll.dll 函数,但在钩子之后)
  • 修补/恢复 ntdll.dll(完全删除钩子)

Usermode 钩子很容易绕过,因为它们完全位于 “our own” 内存空间中,我们可以随意弄乱它。但是恢复 ntdll.dll 本身会生成监控数据,这就是使用直接系统调用的原因。

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

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

请注意,调用堆栈可以显示链中的哪个函数最初被调用。Usermode 钩子的使用越来越少,并非所有 EDR 都使用( source):

edrhookingoverview.png

内核监控

Windows 操作系统以通知的形式提供有关进程的信息 callback 例程。尤其是关于进程、线程和映像创建。 它是内核自己生成的,没有办法压制这些 就像 UserMode 钩子一样(没有内核权限)。
这些回调是在相关进程和线程的上下文中启动的。 因此,事件包含有关源进程的信息。

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

  • 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-fully-working-edr/

一个示例事件是 PS_CREATE_NOTIFY callback,它为 EDR 提供不同的信息:

笔记
父进程 ID
创建 ThreadId
*文件对象 磁盘上的 .exe
图像文件名 已创建流程的参数
命令行 已创建流程的参数
创建状态

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

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

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

{
  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() 或类似方法创建新的内存区域。

 EXE                                               
 Program                 Process                   
                                                   
┌──────────┐            ┌──────────────┐           
│          │            │              │           
│  Header  ├───────────►│ Header       │           
│          │            │              │           
├──────────┤            ├──────────────┤           
│          │            │              │           
│          │            ├──────────────┤           
│  .text   ├─────┐      │              │     Backed 
│          │     │      │              │     RX    
│          │     └─────►│ .text        │           
├──────────┤            │              │           
│          │            │              │           
│  .data   ├────┐       ├──────────────┤           
│          │    │       │              │           
│          │    │       │              │           
└──────────┘    │       ├──────────────┤           
                │       │              │    Backed  
                │       │              │    RW     
                └──────►│ .data        │           
                        │              │           
                        ├──────────────┤           
                        │              │           
                        │              │           
                        ├──────────────┤           
                        │              │           
                        │ Virtual      │    Unbacked
                        │ Alloc()      │    RW     
                        │              │           
                        └──────────────┘           

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

如果进程通过分配来分配额外的内存,则它是 “unbacked”(未备份) 的。也称为 USER memory 或 PRIVATE。没有文件后端,所以它是 “unbacked” 的。

一般来说,它可以是具有以下属性的内存区域:

  • USER/PRIVATE/Unbacked(用户/私有/未备份):错误、潜在恶意的 shellcode
  • IMAGE/Backed(镜像/已备份):不错,相当值得信赖

这主要是因为来自漏洞利用或进程注入的 shellcode 通常位于私有内存中。此外,线程应从支持的区域开始。私有RWX 内存更可疑。

以下是一些 IMG 类型的可信内存区域(IMAGE,支持):

memoryregionsimage.png

以下是一些 PRV 类型的不可信内存区域(PRIVATE,无支持):

memoryregionsuser.png

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

引用:

  • 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/offensive-security/code-injection-process-injection/ntcreatesection-±ntmapviewofsection-code-injection

内存扫描

内存签名扫描将检测内存中的 .text 或 data 部分(堆栈、堆、.data 等)中的恶意代码。

                        Event       
                          │       
 Process                  ▼       
┌───────────┐        ┌───────────┐
│           │        │           │
│           │        │           │
│           │        │           │
├───────────┤        │           │
│           │  Read  │           │
│ .text     ◄────────┤    EDR    │
│  (bad)    │  Scan  │           │
├───────────┤        │           │
│           │        │           │
│           ◄────────┤           │
│ .data     │        │           │
│  (bad)    │        └───────────┘
│           │                     
└───────────┘                     

它与 AV 签名扫描基本相同;grep 或 yara’ 来防止已知的恶意签名。

内存扫描是性能密集型的。它不是经常完成的,而是取决于触发器。

查询流程信息

EDR 在收到事件时,还将尝试对其进行扩充:

  • 进程信息(如可执行文件名称和命令行参数)
  • 内存扫描(可能)
  • 处理映像文件扫描(很少)
                                                                     ┌──────────────┐          
                                                                     │              │          
        ┌─────────────┐ EtwWrite() ┌──────────┐   Kernel callbacks   │              │          
        │ Process     ├───────────►│          ├─────────────────────►│              │          
        │             │            │          │                      │              │          
        │             │            │          │                      │              │          
        ├─────────────┤            │   OS     │   ETW                │              │          
┌───────┤  ntdll.dll  │            │          ├─────────────────────►│              │          
│       │             │ syscall    │          │                      │              │          
│  ┌───►│             ├───────────►│          │   ETW-TI             │    EDR       │          
│  │    ├─────────────┤            │          ├─────────────────────►│              │          
│  │    │             │            └──────────┘                      │              │          
│  │    ├─────────────┤                                              │              │          
│  │    │ amsi.dll    │ pipe                      AMSI               │              │          
│  └────┤             ├─────────────────────────────────────────────►│              │          
│       │             │                                              │              │          
└──────►│             │                                              │              │          
        ├─────────────┤                                              │              │          
        │             │                                              │              │          
        │             │                                              │              │          
        │  ┌──────────┤                          Process Info        │              │          
        │  │          │◄─────────────────────────────────────────────┤              │          
        │  │ PEB      │                                              │              │          
        │  │ Eprocess │                                              │              │          
        │  │          │                                              └──┬──┬────────┘          
        │  │          │                                                 │  │                   
        │  └──────────┤                          Memory Scan            │  │                   
        │             │◄────────────────────────────────────────────────┘  │                   
        └───────▲─────┘                                                    │                   
                │                                                          │                   
         File   │                                                          │                   
         ┌──────┴────┐                            File Scan                │                   
         │           │◄────────────────────────────────────────────────────┘                   
         │           │                                                                         
         │           │                                                                         
         │           │                                                                         
         └───────────┘                                                                         

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

请注意,EDR 是一个正常的进程,即使 SYSTEM 或 PPL 也是如此,并且具有自己的专用内核驱动程序。凭借其 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])。它处于用户模式,可以自由操作。

  • ImageBase 地址
  • 加载的 DLL
  • 进程参数:
    • 镜像名称
    • 参数
    • 环境变量
    • 工作目录

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

  • 进程创建和退出时间
  • 进程 ID
  • 父进程 ID
  • PEB 地址
  • image filename (图像文件名)
    • 类似于 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 函数时,可以找出导致此调用的父函数。这称为 callstack。

callstackintro.png

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

 Process
┌──────────────────────────────────────────────────────────────────────┐           ┌─────────────────┐ 
│                                                                      │           │  OS  kernel     │ 
│  ┌───────────────────┐   ┌─────────────────┐   ┌───────────────────┐ │           │                 │ 
│  │                   │   │                 │   │                   │ │           │                 │ 
│  │  Application.exe  │   │ kernel32.dll    │   │  ntdll.dll        │ │syscall    │                 │ 
│  │                   ├──►│                 ├──►│                   ├─┼──────────►│ NtWriteFile()   │ 
│  │  .text            │   │ CreateFile()    │   │  NtCreateFile()   │ │           │                 │ 
│  │                   │   │                 │   │                   │ │           └────┬────────────┘ 
│  └───────────────────┘   └─────────────────┘   └───────────────────┘ │                │              
│                                                                      │                │Notify        
│                             Stack                                    │                │              
│                            ┌──────────────────────────────────┐      │                ▼              
│                            │ Application.exe: SomeFunction()  │      │  Inspect  ┌─────────────────┐ 
│                            │ kernel32.dll: CreateFile()       │◄─────┼───────────┤                 │ 
│                            │ ntdll.dll: NtCreateFile()        │      │           │                 │ 
│                            └──────────────────────────────────┘      │           │                 │ 
│                                                                      │           │     EDR         │ 
│                                                                      │           │                 │ 
│                                                                      │           │                 │ 
└──────────────────────────────────────────────────────────────────────┘           └─────────────────┘ 

使用这种技术可以检测各种攻击和绕过。但它在某种程度上会占用大量性能。

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

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

  • 直接 syscall
  • 基于回调的规避
  • 模块踩踏
  • 从无支持区域加载库
  • 从未支持的区域创建的进程

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

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

  • VirtualAlloc、VirtualProtect
  • MapViewOfFile、MapViewOfFile2
  • VirtualAllocEx 和 VirtualProtectEx
  • QueueUserAPC
  • SetThreadContext 设置
  • WriteProcessMemory、ReadProcessMemory

参考:

  • 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() 的 (欺骗性) 调用堆栈:

callstacksleepspoofed.png

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

  • Kernelbase.dll!休眠
  • ntdll.dll!NtDelayExecution

调用这些 sleep 函数的可疑内容:

  • 对调用堆栈中的虚拟内存的调用
  • 非支持的内存区域中的源

参考:

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

性能影响

EDR 的性能至关重要。如果开发人员机器在安装 10’000 个 NPM 软件包时速度缓慢,人们将转移到保护较少的 Apple,而 Microsoft 不允许这种情况。这是一个问题,以至于 Microsoft 引入了异步 Dev Drive 扫描。

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

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

性能影响 什么
1 事件
3 事件关联
10 查询流程
100 内存扫描
1000 文件扫描

什么会触发内存扫描?

什么 触发扫描? 笔记
VirtualAlloc() 太常见,除非 RWX
WriteProcessMemory() 很常见
memcpy() 对 EDR 不可见
VirtualProtect 虚拟保护 不? RWX 或 RW->RX 可能被触发
CreateRemoteThread() 是的 应触发内存扫描

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

EDR 攻击

EDR 从大量传感器接收事件,具有各种可信度。此外,所需的许多信息在事件本身中不可用,但必须在内核(KPROCESS、EPROCESS)或进程内存空间本身(例如包含命令行参数、父进程 ID)中或通过内核访问。

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

命令行欺骗

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

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

所以我们基本上有两个命令行参数的地方:

  • 在子进程的 PEB 中
  • 在子 create 函数上: 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 会查询现有进程的命令行,并且通常会盲目地信任它:

┌────────────────────┐                  ┌─────────────────┐    
│ Process            │                  │                 │    
│                    │                  │                 │    
│      PEB           │                  │                 │    
│     ┌──────────────┤                  │                 │    
│     │              │                  │    EDR          │    
│     │ ImageName    │◄─────────────────┤                 │    
│     │ CommandLine  │                  │                 │    
│     │              │                  │                 │    
│     └──────────────┤                  │                 │    
│                    │                  │                 │    
└────────────────────┘                  └─────────────────┘    

但这是可以验证的。当父进程调用 CreateProcess() 创建子进程时:

   ┌─────────┐                  ┌──────────┐           ┌───────────┐
   │ Process │                  │          │           │ Child     │
   │         │  CreateProcess() │   OS     │ Spawns    │ Process   │
   │         ├─────────────────►│          ├──────────►│           │
   │         │          ▲       │          │           │           │
   │         │          │       └──────────┘           │PEB        │
   │         │          │                              ├─────────┐ │
   │         │          │        ┌───────┐             │ Command │ │
   │         │          │        │       │       ┌────►│ Line    │ │
   │         │          └────────┤  EDR  ├───────┘     ├─────────┘ │
   │         │                   │       │             │           │
   └─────────┘                   └───────┘             └───────────┘

EDR 可以比较 CreateProcess() 中的命令行,然后比较生成的子进程的 PEB,并在它们不匹配时发出警报。

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

  1. Parent父级:使用虚构参数创建新的暂停进程
  2. EDR:接收带有 false 参数的事件
  3. parent:使用实数参数覆盖 child 的 PEB
  4. 父进程:Continue (start) 子进程(使用实参)
  5. 子进程:再次用虚参数覆盖其 PEB
  6. EDR:查询进程会得到假参数

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

因此,进程的命令行参数非常不可信。

PPID 欺骗

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

可以指示函数 CreateProcessW()STARTUPINFOEX 结构中提供自己的属性,包括子进程的父进程。因此,在创建时,我们可以为子 PID 提供错误的父 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 看到 PPID=y (2) 的 CreateProcess() 调用,以及 PID=x 的启动此调用 (1) 的进程的有效 PID。

  ┌─────────┐                  ┌──────────┐           ┌───────────┐
  │ Process │  CreateProcess() │          │           │ Child     │
  │         │  PPID=y          │   OS     │ Spawns    │ Process   │
  │         ├─────────────────►│          ├──────────►│           │
  │         │          ▲       │          │           │           │
  │         │          │       └──────────┘           │EPROCESS   │
  │ ┌───────┤    1     │2                             ├─────────┐ │
  │ │PID=x  │◄─────────┤        ┌───────┐          3  │ PPID=y  │ │
  │ │       │          │        │       │       ┌────►│         │ │
  │ └───────┤          └────────┤  EDR  ├───────┘     ├─────────┘ │
  │         │                   │       │             │           │
  └─────────┘                   └───────┘             └───────────┘

所以 EDR 有:

  1. 父级:PID
  2. 父级:其发出的 CreateProcess() 调用中的 PPID,目标为子级
  3. 子项:其 PPID

并比较这些,尤其是 1) 和 2)。或者后来的 1/2 和 3。对于收到的事件,源 PID 的来源 (并不总是完全清楚 (例如 ETW) 。

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

ETW 补丁

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

  • VirtualProtect .text: RX -> RW
  • 覆盖内存(将函数体替换为返回 0
  • VirtualProtect .text: RW -> RX
   Process                                                    
  ┌──────────────────────┐                                    
  │                      │                                    
  │                      │                                    
  ├──────────────────────┤                                    
  │                      │   ntdll.dll RW -> patch -> RX      
  │ .text                ├──────────────┐                     
  │                      │              │                     
  ├──────────────────────┤              │       ┌─────────┐   
  │                      │              │       │         │   
  │                      │              │ ◄─────┤  EDR    │   
  │                      │              │       │  sus?   │   
  ├──────────────────────┤              │       │         │   
  │ ntdll.dll            │              │       └─────────┘   
  │                      │              │                     
  │  - EtwEventWrite()   │◄─────────────┘                     
  │                      │                                    
  │                      │                                    
  ├──────────────────────┤                                    
  │                      │                                    
  │                      │                                    
  │                      │                                    
  └──────────────────────┘                                                                       

更改 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)中执行的脚本。或者换句话说,应用程序本身要求 OS 通过 AMSI 对它打算执行的某些文件或缓冲区执行 AV 扫描。

要禁用 AMSI 运行时 code scanning,例如 patch amsi.dll!AmsiOpenSession 删除监控数据。备选方案包括 AmsiScanString() / AmsiScanBuffer() .

该过程与 ETW-patch 相同:使代码部分可写,中断功能,再次恢复原始权限。

     Process                                                   
  ┌──────────────────────┐                                   
  │                      │                                   
  │                      │                                   
  ├──────────────────────┤                                   
  │                      │   ntdll.dll RW -> patch -> RX     
  │ .text                ├──────────────┐                    
  │                      │              │                    
  ├──────────────────────┤              │       ┌─────────┐  
  │                      │              │       │         │  
  │                      │              │ ◄─────┤  EDR    │  
  │                      │              │       │  sus?   │  
  ├──────────────────────┤              │       │         │  
  │ ntdll.dll            │              │       └─────────┘  
  │                      │              │                    
  │  - AmsiOpenSession() │◄─────────────┘                    
  │                      │                                   
  │                      │                                   
  ├──────────────────────┤                                   
  │                      │                                   
  │                      │                                   
  │                      │                                   
  └──────────────────────┘                                   

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

这在 powershell 中加载签名的恶意 powershell 脚本时非常有用,否则 AMSI 接口会对其进行扫描。生成混淆 AMSI-AV 补丁的著名站点是 https://amsi.fail

AMSI 钩子修补

AMSI 钩子修补(或 AMSi 修补)只是删除调用 amsi.dll 的 EDR 的 ntll.dll 补丁。它与 ETW-patch 或 AMSI-AV 补丁基本相同,因为它只是再次修改ntdll.dll。它可以生成其他监控数据,例如,在从磁盘加载 ntll.dll 的干净版本时。

       Process                                                      
      ┌──────────────────────┐                                      
      │                      │                                      
      │                      │                                      
      ├──────────────────────┤                                      
      │                      │   ntdll.dll RW -> patch -> RX        
      │ .text                ├──────────────┐                       
      │                      │              │                       
      ├──────────────────────┤              │       ┌─────────┐     
      │                      │              │       │         │     
      │                      │              │ ◄─────┤  EDR    │     
      │                      │              │       │  sus?   │     
      ├──────────────────────┤              │       │         │     
      │ ntdll.dll            │              │       └─────────┘     
      │                      │              │                       
      │                      │◄─────────────┘                       
      │                      │                                      
      │                      │                                      
      ├──────────────────────┤                                      
      │                      │                                      
      │                      │                                      
      │                      │                                      
      └──────────────────────┘                                      

引用:

  • https://github.com/ZeroMemoryEx/Amsi-Killer
  • https://github.com/V-i-x-x/AMSI-BYPASS

AMSI 旁路

AMSI 旁路可以表示如上所述绕过 AMSI-AV 接口。或者它意味着调用 OS 内核函数而不调用其中的 ntdll.dll 钩子。

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

或者对于间接系统调用:在 hook 调用之后重用 ntdll.dll 函数的部分。

在这两种情况下,都会绕过 AMSI 挂钩,并且 EDR 不会获得任何监控数据。

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

                                                                                      ┌─────────────┐       
                                                                                      │             │       
┌───────────────────┐   ┌─────────────────┐   ┌───────────────────┐                   │             │       
│                   │   │                 │   │ ntdll.dll:        │                   │    OS       │       
│  Application.exe  │   │ kernel32.dll    │   │ NtCreateFile()    │                   │             │       
│                   ├──►│                 ├──►│                   │                   │             │       
│                   │   │ CreateFile()    │   │                   │                   │    Kernel   │       
│                   │   │                 │   │                   │                   │             │       
└───────────────────┘   └─────────────────┘   │                   │                   │             │       
                                              │                   │                   │             │       
                                     ┌────────┼───jmp callback    │                   │             │       
                                     │        │                   │          syscall  │             │       
                                     │ ┌──────┼──►syscall         ├─────────────────► │             │       
                                     │ │      │                   │                   │             │       
                                     │ │      │                   │                   │             │       
                                     │ │      └───────────────────┘                   │             │       
                                     │ │                                              │             │       
                                     │ │ ┌─────────────────────────┐                  │             │       
                                     │ └─┤                         │                  │             │       
                                     │   │ amsi.dll:               │                  └─────────────┘       
                                     └──►│ HookedNtCreateFile()    │                                        
                                         └──────────┬──────────────┘                                        
                                                    │ notify                                                
                                                    ▼                                                       
                                              ┌────────────┐                                                
                                              │    EDR     │                                                
                                              │    :-)     │                                                
                                              └────────────┘                                                

这里有:

  • 直接系统调用:只需自己执行系统调用(使用正确的系统调用编号)
  • 间接 syscall:重用 hooked ntdll.dll 的部分,调用 syscall 但不调用 hook
                  direct                                                                           
                  syscall                                                                        
                ┌────────────────────────────────────────────────────────┐            ┌─────────────┐   
                │                                                        │            │             │   
┌───────────────┴───┐   ┌─────────────────┐   ┌───────────────────┐      │            │             │   
│                   │   │                 │   │ ntdll.dll:        │      │            │    OS       │   
│  Application.exe  │   │ kernel32.dll    │   │ NtCreateFile():   │      │            │             │   
│                   ├──►│                 ├──►│                   │      │            │             │   
│                   │   │ CreateFile()    │   │                   │      │            │    Kernel   │   
│                   │   │                 │   │                   │      │            │             │   
└──────────────┬────┘   └─────────────────┘   │                   │      │   syscall  │             │   
               │                              │                   │      └──────────► │             │   
               │                              │   jmp callback    │                   │             │   
               │                              │                   │          syscall  │             │   
               └──────────────────────────────┼──►syscall         ├─────────────────► │             │   
                indirect                      │                   │                   │             │   
                syscall                       │                   │                   │             │   
                                              └───────────────────┘                   │             │   
                                                                                      │             │   
                                          ┌────────────────────────┐                  │             │   
                                          │amsi.dll                │                  └─────────────┘   
                                          │                        │                             
                                          │HookedNtCreateFile()    │                       
                                          └────────────────────────┘                                        
                                                       no notify                                                
                                                                                                             
                                              ┌────────────┐                                                
                                              │    EDR     │                                                
                                              │    :-(     │                                                
                                              └────────────┘                                                

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

引用:

  • https://eversinc33.com/posts/avoiding-direct-syscalls.html
  • https://www.outflank.nl/blog/2019/06/19/red-team-tactics-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

Image Spoofing 图像欺骗

与欺骗参数类似,攻击者可能还希望“欺骗”exe:启动非恶意 exe(如 notepad.exe),EDR 会记录该 exe,然后将进程内容替换为恶意 exe(如 mimikatz)。这试图欺骗 EDR,使其认为已经启动了非恶意的东西。这绕过了简单的 EDR。

source .exe 文件称为进程的 Image。

Process hollowing(傀儡进程):

                   Event: CreateProcess("notepad.exe")
                        ▲                             
                        │                             
                        │                             
                        │   notepad.exe               
┌───────────┐           │  ┌───────────┐              
│           │ Start     │  │           │              
│           │ Suspended │  │           │              
│           ├───────────┴─►│           │              
│           │              │           │              
│           │              ├───────────┤              
│           │ Overwrite    │ .text     │              
│           │ Memory       │           │              
│           ├──────────────┤►          │              
│           │              ├───────────┤              
│           │              │           │              
│           │              │           │              
│           │              │           │              
│           │ Resume       │           │              
│           ├─────────────►│           │              
│           │              │           │              
└───────────┘              └───────────┘              

还有一些其他技术:

  • Process Hollowing(傀儡进程):使用 WriteProcessMemory() 覆盖暂停进程的进程内存
  • Process Doppelgänging(进程分身):使用事务性 NTFS (TxF) 覆盖文件,启动进程,然后回滚事务以恢复原始文件
  • Process Herpaderping(进程驱散):将恶意代码写入 exe,创建进程,在恶意内容被扫描之前快速将恶意内容替换为非恶意内容
  • Process Ghosting(进程幽灵):创建空文件,半删除它,写入恶意数据,从中创建进程

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

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

或者,EDR 可以收集监控数据来识别操作。或者支持技术,如直接 syscall,例如调用堆栈分析。

技术 使用的 API
Hollowing(傀儡) CreateProcess、NtUnmapViewOfSection、VirtualAllocEx、WriteProcessMemory、SetThreadContext、ResumeThread
Doppelgänging CreateTransaction、CreateFileTransacted、NtCreateProcessEx
Herpaderping NtCreateSection、NtCreateProcessEx、NtCreateThreadEx
Ghosting CreateFileA、NtOpenFile、NtSetInformationFile、NtCreateSection、NtCreateProcess、WriteRemoteMem、NtCreateThreadEx

Hollowing(傀儡)引用:

  • https://www.ired.team/offensive-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 部分,并从那里开始创建新线程。

                   Event: LoadLibrary("genuine.dll")           
                        ▲                                      
                        │                                      
                        │                                      
                        │   genuine.dll                        
┌───────────┐           │  ┌───────────┐                       
│           │ Load      │  │           │                       
│           │ DLL       │  │           │                       
│           ├───────────┴─►│           │                       
│           │              │           │                       
│           │              ├───────────┤                       
│           │ Overwrite    │ .text     │                       
│           │ Memory       │           │                       
│           ├──────────────┤►          │                       
│           │              ├───────────┤                       
│           │              │           │                       
│           │              │           │                       
│           │              │           │                       
│           │ Start        │           │                       
│           ├─────────────►│           │                       
│           │              │           │                       
└───────────┘              └───────────┘     

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

  • 内存签名扫描
  • .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 无法很好地捕获其中的大部分数据。

                                             Event           
                                                 │           
                                                 │           
                                                 │           
 Process                Process                  ▼           
┌───────────┐          ┌───────────┐        ┌───────────┐    
│           │          │           │        │           │    
│           │          │           │        │           │    
│           │          │           │        │           │    
├───────────┤          ├───────────┤        │           │    
│           │          │           │  Read  │           │    
│ .text     ├─────────►│ .text     ◄────────┤    EDR    │    
│           │          │  Encrypted│  Scan  │           │    
├───────────┤          ├───────────┤        │           │    
│           │          │           │        │           │    
│           │          │           ◄────────┤           │    
│ .data     │          │ .data     │        │           │    
│           │          │  Encrypted│        └───────────┘    
│           │          │           │                         
└───────────┘          └───────────┘                         

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

调用堆栈欺骗

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

当使用直接系统调用、间接系统调用或其他恶作剧时,默认情况下,调用堆栈看起来是 “错误的”,这可以通过 EDR 来识别。

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

实际的调用堆栈欺骗通常不会生成监控数据,并且可以非常节省地实现。但是,通过重用现有的 callstack-spoofing 实现,可以通过签名扫描(无论是在磁盘上还是在内存中)来识别它。

NtDelayExecution() 的可疑调用堆栈:

callstacksleepnotspoofed.png

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

callstacksleepspoofed.png

Anti-Detection 依赖于伪造调用堆栈、复制干净的调用堆栈,或者只是隐藏恶意调用堆栈。有许多技术可以检查 callstack 的完整性,通常是通过与其他信息相关联。例如,线程开始地址应源自合理的位置。

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

引用:

  • 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()
  • ReadProcessMemory() / WriteProcessMemory()
  • CreateRemoteThread()
  • QueryInformationProcess() / NtQueryInformationProcess()
 Process                              Child Process
┌──────────────┐                     ┌─────────────┐ 
│              │                     │             │ 
│              │ OpenProcess()       │             │ 
│              ├────────────────────►│             │ 
│              │              handle │             │ 
│       HANDLE │◄────────────────────┤             │ 
│              │                     │             │ 
│              │ VirtualAlloc(handle)│             │ 
│              ├────────────────────►│             │ 
└──────────────┘                     └─────────────┘ 

暂停的进程

一种非常常见的方法是创建一个参数为 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 注入之前执行)。

 Process                                      Child Process
┌──────────────┐                             ┌─────────────┐  
│              │                             │             │  
│              │ CreateProcessA(suspended)   │             │  
│              ├────────────────────────────►│             │  
│              │                             │             │  
│       HANDLE │◄────────────────────────────┤             │  
│              │                             │             │  
│              │ VirtualAllocEx()            │             │  
│              │ WriteProcessMemory()        │             │  
│              │ QueueUserApc()              │             │  
│              ├────────────────────────────►│             │  
│              │                             │             │  
│              │                             │             │  
│              │ ResumeThread()              │             │  
│              ├─────────────────────────────┤             │  
└──────────────┘                             └─────────────┘  

结尾

智能EDR

  • 使用 threatcheck 或 avred 来识别 AV 识别了您内容的哪一部分,并对其进行修补
  • 内存扫描会占用大量性能,通常需要执行触发器
  • 用户模式 AMSI 的相关性越来越低,因此 AMSI 钩子补丁也越来越少

编写 loader 的错误

  • 使用函数调用复制内存
  • 投入超过最少的精力来处理熵
  • 投入超过最低限度的精力来处理加密
  • 生成过多的监控数据
  • 线程未在支持的内存中启动
  • 再次将 RX 页面标记为 RW
  • 具有不干净的调用堆栈

建议的加载程序

建议的加载器布局:

                                                                        ┌──────────┐                  
                                                                        │ encrypted│                  
                                                                        │ Payload  │                  
                                                                        │          │                  
                                                                        └────┬─────┘                  
                                                                             │                        
                                                                             │                        
                                                                             ▼                        
┌───────────┐    ┌──────────────┐    ┌─────────────┐    ┌───────────┐   ┌──────────┐     ┌────────────┐
│   EXE     │    │  Execution   │    │ Anti        │    │EDR        │   │ Alloc RW │     │  Payload   │
│   File    ├───►│  Guardrails  ├───►│ Emulation   ├───►│conditioner├──►│ Decode/Cp├────►│  Execution │
│           │    │              │    │             │    │           │   │ RX       │     │            │
│           │    │              │    │             │    │           │   │ Exec     │     │            │
└───────────┘    └──────────────┘    └─────────────┘    └───────────┘   └──────────┘     └────────────┘
  • EXE File:所有代码都应包含在 .text 部分 (IMAGE) 中
  • Execution Guardrails:仅允许它在预期目标上执行(反中间设备)
  • Anti-Emulation 反仿真:停止 AV 模拟我们的二进制文件(内存使用率、CPU 周期计数、时间欺骗…
  • EDR Feng-Shui:通过对非恶意数据执行大量 Alloc/Copy/VirtualProtect 循环来调节 EDR,并且免费
  • Payload:加密(如何无关紧要)
  • Alloc/Decode/Virtualprotect/Exec:尽可能正常(避免在此处使用 DLL 函数)。避免使用 RWX。
  • Payload Execution:尽可能正常(jmp 到 payload,避免创建新线程)

未加入部分

检测基于:

  • 文件访问
  • 注册表访问
  • 网络访问

未讨论的低级技术:

  • 软件断点
  • 硬件断点
  • VEH
  • APC 注入

原文来源

https://blog.deeb.ch/posts/how-edr-works/