aw4ker2024-12-03文章来源:SecHub网络安全社区
2024 年的 EDR 功能和绕过,重点是未检测到的 shellcode 加载程序。
目前,人们非常关注植入物的内存加密:
此外,还有很多工作涉及调用堆栈欺骗:
这很酷,但如果我告诉你这不是绝对必要的呢?请继续阅读。
这是三篇系列文章的一部分:
目标受众是困惑的红队人员。建议具备对抗EDR(anti-EDR)和 maldev 的基本知识。
我不是 EDR 专家。我刚刚阅读了 Matt Hand 和 Elastic Security-Labs 的《逃避 EDR》,您也应该阅读。
我在 HITB BKK 2024 的一次演讲中提到了其中的部分内容:我的第一个和最后一个 shellcode 加载器。
EDR 是“端点检测和响应”( “Endpoint Detection and Response”)。它有一个部署在每台计算机上的Agent代理,用于观察操作系统生成的事件以识别攻击。如果检测到某些内容,它将生成警报并将其发送到 SIEM 或 SOAR,在那里由人工分析师进行查看。“响应”是指在识别威胁后执行的操作,例如隔离主机,这不属于本文的一部分。EPP是终端保护平台(Endpoint Protection Platform),它将尝试中断攻击,而不仅仅是检测攻击。
MDE (Microsoft Defender for Endpoint) 的 UI:
我们可以看到 EDR 检测到了一些东西,并尝试向分析师提供有关事件的更多信息:涉及的进程、它们的参数和哈希、子进程等。最后的分析师必须研判这是误报还是主动攻击。但总的来说,红队人员都希望避免触发任何警报,并试图保持低调。
EDR 试图在疼痛金字塔的较高位置实施检测,主要是在 TTP 上:工具、技术、程序。
即使只知道和理解一个 EDR 也很困难,所以想完全理解所有EDR基本是不可能的。这里写的 EDR 是理想 EDR 的抽象版本。与其说是今天正在做的事情,不如说是可用的 Windows 传感器/监控基础设施在理论上可能发生的事情。最接近的灵感来自我用于测试的 Windows Defender for Endpoint (MDE)。
我不会教您如何绕过特定的 EDR,而是如何从概念上思考攻击面以实施您自己的技术。EDR 的实际内部工作原理大多是未知的(Elastic 除外),并被视为黑盒。虽然我们主要知道 EDR 接收到什么样的信息,但并不清楚这些信息是如何在内部被使用和关联的。
作为一个黑客,我们对系统的输入和输出感兴趣。本文将概述输入。
加载器(loader)将加载 shellcode。shellcode 通常是我们的信标(Beacon),例如 CobaltStrike、Sliver 或 Metasploit。
加载程序包含加密的 shellcode,将其加载到内存中,然后执行它。
┌───────────┐ ┌────────────┐ ┌────────┐
│ │ │ │ │ │
│ Loader ├──►│ C2 Beacon ├───►│ Profit │
│ │ │ Shellcode │ │ │
│ │ │ │ │ │
└───────────┘ └────────────┘ └────────┘
目标是使 EDR 无法检测到初始访问 (IA) 的进程。
执行 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()
或其他函数完成。
有三种主要的检测(加载器)技术:
例如,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 有一个包含已知恶意恶意软件(如 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 将自行读取和解释 .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 的示例体验:
然后:
AV Emulator 将执行/模拟加载器。一段时间后,执行停止,并在内存中发现 Metasploit shellcode 未加密。然后 AV 将在内存中检测它的签名。
检测 Emulator 的可能性是无限的。但通常,仿真不会永远运行,而是受到以下限制:
什么 | 典型限制 |
---|---|
时间 | ? |
指令数量 | ? |
API 调用次数 | ? |
使用的内存量 | ? |
参考:
EDR 接收进程通过操作系统执行的操作的事件:
Process
┌────────────────┐ ┌─────────────┐
│ │ │ │
│ │ │ Windows │
│ │ │ kernel │
├────────────────┤ Syscalls │ │
│ (Hooked) ├───────────────────►│ │
│ │ │ │
│ ntdll.dll ├─────────────────┐ │ │
│ NtApi │ Usermode │ │ │
├────────────────┤ Hooks │ └──────┬──────┘
│ │ │ │
│ │ │ │ kernel
│ │ │ │ callbacks
│ │ │ │
│ │ ▼ ▼
│ │ ┌────────────────────────┐
│ │ │ EDR │
│ │ └────────────────────────┘
└────────────────┘
接收数据的主要渠道有两个:
当添加/删除/更改某些内容时,这些传感器将创建有关系统中发生的事情的事件,例如:
EDR 将包含用于匹配恶意行为事件的规则。规则可以是:
请注意,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 输入为:
我将单独讨论它们中的每一个。
虽然 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):
Windows 操作系统以通知的形式提供有关进程的信息 callback 例程。尤其是关于进程、线程和映像创建。 它是内核自己生成的,没有办法压制这些 就像 UserMode 钩子一样(没有内核权限)。
这些回调是在相关进程和线程的上下文中启动的。 因此,事件包含有关源进程的信息。
内核模式检测有多种不同的来源:
内核回调是:
参考:
一个示例事件是 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
请注意,只有字段 ImageFilename
、CommandLine
、ParentProcessId
直接转换为内核事件的 Image
、CommandLine
、ParentProcessId
。但大多数其他信息是由 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” 的。
一般来说,它可以是具有以下属性的内存区域:
这主要是因为来自漏洞利用或进程注入的 shellcode 通常位于私有内存中。此外,线程应从支持的区域开始。私有RWX 内存更可疑。
以下是一些 IMG 类型的可信内存区域(IMAGE,支持):
以下是一些 PRV 类型的不可信内存区域(PRIVATE,无支持):
内存页的一个属性是写入时复制 (COW)。内存扫描程序能够检查内存页是否被写入,这对于只读 .text 部分和其他部分来说是不常见的,因为这些应该在进程之间共享。这是 Moneta 通过 PSAPI_WORKING_SET_EX_BLOCK
from PSAPI_WORKING_SET_EX_INFORMATION
structure 使用的。首选纯数据攻击,例如 AMSI-patch 或 ETW-patch。
引用:
内存签名扫描将检测内存中的 .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])
。它处于用户模式,可以自由操作。
EPROCESS 是一种内核数据结构,不能直接(有时是间接)操作:
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。
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 具有调用堆栈分析规则来识别:
如果调用来自无支持的区域,则很可能来自 shellcode。
调用堆栈分析通常不适用于所有 API 函数。Elastic 提到了以下内容:
参考:
线程处于休眠状态的原因可能有多种。通过调查状态,以及线程如何通过他的调用堆栈到达那里,我们找到了休眠信标或内存加密的指示器。
清理 NtDelayExecution()
的 (欺骗性) 调用堆栈:
如果正在使用内存加密,则通常通过调用以下任一方式使线程进入休眠状态:
调用这些 sleep 函数的可疑内容:
参考:
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 从大量传感器接收事件,具有各种可信度。此外,所需的许多信息在事件本身中不可用,但必须在内核(KPROCESS、EPROCESS)或进程内存空间本身(例如包含命令行参数、父进程 ID)中或通过内核访问。
许多攻击都依赖于 TOCTOU 漏洞的事实:检查时间、使用时间。
EDR 可以检查新生成的进程是否存在潜在的恶意命令行参数,例如在使用 mimikatz: 时。 mimikatz.exe "privilege::debug" "lsadump::sam"
即使我们重命名mimikatz.exe
,参数 privilege::d ebug
也是一个非常明显的指标,误报率很低。
但在 Windows 中,可以欺骗命令行参数。进程的命令行参数存储在相应进程的 PEB 中。此外,当我们创建新进程时,进程创建函数还将包含(要启动的 exe 的)初始参数。
所以我们基本上有两个命令行参数的地方:
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", ...)
调用参数也没有多大帮助,因为我们可以使用假参数创建处于挂起状态的进程,用正确的参数远程覆盖它们,然后恢复进程。
如果 EDR 将来认为子进程是恶意的,它将向分析师提供从 PEB 获取的信息,包括进程的命令行参数。因此,子进程需要再次覆盖 PEB,作为 “清理”。
因此,进程的命令行参数非常不可信。
在 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 有:
CreateProcess()
调用中的 PPID,目标为子级并比较这些,尤其是 1) 和 2)。或者后来的 1/2 和 3。对于收到的事件,源 PID 的来源 (并不总是完全清楚 (例如 ETW) 。
请注意,InheritedFromUniqueProcessId
存储在 EPROCESS 中,但仍然不可信,因为它可以从用户空间进行设置。
ETW 修补程序将覆盖 ntdll.dll 中的 EtwEventWrite(
),因此``该进程不会再自行发出任何 ETW 事件。这主要适用于 Powershell 和 .NET 相关事件。它通常涉及:
返回 0
) 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的。
引用:
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.dll
的 EDR 的 ntll.dll
补丁。它与 ETW-patch 或 AMSI-AV 补丁基本相同,因为它只是再次修改ntdll.dll
。它可以生成其他监控数据,例如,在从磁盘加载 ntll.dll
的干净版本时。
Process
┌──────────────────────┐
│ │
│ │
├──────────────────────┤
│ │ ntdll.dll RW -> patch -> RX
│ .text ├──────────────┐
│ │ │
├──────────────────────┤ │ ┌─────────┐
│ │ │ │ │
│ │ │ ◄─────┤ EDR │
│ │ │ │ sus? │
├──────────────────────┤ │ │ │
│ ntdll.dll │ │ └─────────┘
│ │ │
│ │◄─────────────┘
│ │
│ │
├──────────────────────┤
│ │
│ │
│ │
└──────────────────────┘
引用:
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 │
│ :-) │
└────────────┘
这里有:
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 中一样。
引用:
与欺骗参数类似,攻击者可能还希望“欺骗”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 │ │
│ ├─────────────►│ │
│ │ │ │
└───────────┘ └───────────┘
还有一些其他技术:
WriteProcessMemory()
覆盖暂停进程的进程内存内存扫描将使用签名(如 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(傀儡)引用:
这类似于图像欺骗,但使用 DLL 的。
模块踩踏将 shellcode 写入远程进程中未使用的 DLL 的 .text 部分,并从那里开始创建新线程。
Event: LoadLibrary("genuine.dll")
▲
│
│
│ genuine.dll
┌───────────┐ │ ┌───────────┐
│ │ Load │ │ │
│ │ DLL │ │ │
│ ├───────────┴─►│ │
│ │ │ │
│ │ ├───────────┤
│ │ Overwrite │ .text │
│ │ Memory │ │
│ ├──────────────┤► │
│ │ ├───────────┤
│ │ │ │
│ │ │ │
│ │ │ │
│ │ Start │ │
│ ├─────────────►│ │
│ │ │ │
└───────────┘ └───────────┘
与图像欺骗相同,它可以通过以下方式检测:
引用:
可以在休眠之前加密所有可疑区域,并在进程恢复时再次解密。这并非微不足道,需要非常小心、奇怪的 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()
的可疑调用堆栈:
清理 NtDelayExecution()
的 (欺骗性) 调用堆栈:
Anti-Detection 依赖于伪造调用堆栈、复制干净的调用堆栈,或者只是隐藏恶意调用堆栈。有许多技术可以检查 callstack 的完整性,通常是通过与其他信息相关联。例如,线程开始地址应源自合理的位置。
在普通线程中,用户模式起始地址通常是线程堆栈中的第三个函数调用 - 在 ntdll 之后!RtlUserThreadStart 和 kernel32!BaseThreadInitThunk 的 InitThunk 中。因此,当线程被劫持时,这在调用堆栈中将很明显对于“早起的鸟儿”APC 注入,调用堆栈的基础将是 ntdll!LdrInitializeThunk,ntdll!NtTestAlert,ntdll!KiUserApcDispatcher 的 Dispatcher 方法,然后是注入的代码。
引用:
攻击者可以选择是否要弄乱自己的进程或系统的另一个进程。这里描述的 Windows 函数大多数也可以在另一个进程上使用,只需先使用 OpenProcess()
即可。
这主要用于工艺注入。迁移到另一个进程(如 teams.exe)非常有用。它的 C2 可以隐藏在应用程序的正常通信中,它的 JavaScript 所以大量的 RW->RX 分配。
EDR 对弄乱远程进程进行了更严格的审查,留在自己的进程中更安全。相反,对于迁移,请使用 DLL 旁加载或其他不依赖于 OpenProcess()
的技术。
这包括:
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() │ │
│ ├─────────────────────────────┤ │
└──────────────┘ └─────────────┘
建议的加载器布局:
┌──────────┐
│ encrypted│
│ Payload │
│ │
└────┬─────┘
│
│
▼
┌───────────┐ ┌──────────────┐ ┌─────────────┐ ┌───────────┐ ┌──────────┐ ┌────────────┐
│ EXE │ │ Execution │ │ Anti │ │EDR │ │ Alloc RW │ │ Payload │
│ File ├───►│ Guardrails ├───►│ Emulation ├───►│conditioner├──►│ Decode/Cp├────►│ Execution │
│ │ │ │ │ │ │ │ │ RX │ │ │
│ │ │ │ │ │ │ │ │ Exec │ │ │
└───────────┘ └──────────────┘ └─────────────┘ └───────────┘ └──────────┘ └────────────┘
检测基于:
未讨论的低级技术:
https://blog.deeb.ch/posts/how-edr-works/