简介
什么是 EDR
EDR 是“端点检测和响应”的缩写。它是部署在每台机器上的代理,用于观察操作系统生成的事件以识别攻击。如果检测到某些东西,它将生成警报并将其发送到 SIEM 或 SOAR,由人工分析师进行查看。“响应”是指在识别威胁后执行的操作,例如隔离主机,这不是本文的一部分。EPP 是端点保护平台,它将尝试中断攻击,而不仅仅是检测攻击。
MDE (Microsoft Defender for Endpoint) 的 UI:
我们可以看到 EDR 检测到了某些东西,并试图向分析师提供有关该事件的更多信息:涉及的进程、它们的参数和哈希值、子进程等。分析师最终必须决定这是误报还是主动攻击。但一般来说,红队希望避免引起任何警报,并试图保持低调。
EDR 尝试在痛苦金字塔的更高层实施检测,主要是在TTP:工具、技术、程序。
理想化的 EDR
了解和理解哪怕只是一个 EDR 都很困难,而了解和理解所有的 EDR 则是不可能的。这里写的 EDR 是理想 EDR 的抽象版本。与其说是今天正在做的事情,不如说是理论上使用可用的 Windows 传感器/遥测基础设施可以实现什么。最接近的灵感是 Windows Defender for Endpoint (MDE),我用它来进行测试。
我不会教你如何绕过特定的 EDR,而是教你如何从概念上思考攻击面,以实施你自己的技术。EDR 的实际内部工作原理大多未知(Elastic 除外),并且被视为黑盒。虽然我们大多知道 EDR 会收到什么样的信息,但不太清楚这些信息在内部是如何被使用和关联的。
作为一名黑客,我们对系统的输入和输出很感兴趣。本文应该对输入进行概述。
Shellcode 加载器
加载程序将加载 shellcode。shellcode 通常是我们的信标,例如 CobaltStrike、Sliver 或 Metasploit。
加载程序包含加密的 shellcode,将其加载到内存中并执行。
目标是使该过程不会被 EDR 检测到初始访问 (IA)。
Shellcode 加载器示例
在执行shellcode时,通常的步骤如下:
分配具有读写权限的内存区域
将 shellcode复制到该区域(也对其进行解密)
将内存区域的权限更改为读取-执行
执行shellcode
在 C 语言中看起来像这样,但在大多数语言中都类似:
char *shellcode = "\xAA\xBB...";
char *dest = VirtualAlloc(NULL, 0x1234, 0x3000, p_RW);
memcpy(dest, shellcode, 0x1234)
VirtualProtect(dest, 0x1234, p_RX, &result)
(*(void(*)())(dest))(); // jump to dest: execute shellcode
这个简单的配方有很多变体,其中一些专注于远程进程的 shellcode 注入。其工作原理相同,在OpenProcess()目标进程上使用,并将其用作hProcess函数调用(如VirtualAlloc(hProcess, ...)和WriteProcessMemory(hProcess, ...))的参数。EDR 会更严格地审查跨进程访问hProcess。
另一个典型的做法是通过创建新线程来调用 shellcode。无论是CreateThread()在您自己的地址空间中,还是CreateRemoteThread()用于进程注入或模块踩踏。
复制本身,这里由用户空间函数执行memcpy(),也可以用或其他函数完成RtlCopyMemory()。
EDR 检测
检测加载器主要有三种技术:
文件扫描:文件签名(“yara”)扫描
内存扫描:签名(“yara”)扫描进程内存
遥测/行为:进程执行的操作(主要通过操作系统)
例如,Windows Defender Antivirus 可实现 AV 扫描,而 Windows Defender for Endpoint MDE 是一种 EDR,它严重依赖遥测来执行行为分析。如果它觉得有必要,它也会扫描进程的内存。
我把这称为“Bubbles of Bane”:
C2 框架生成的大多数 .exe 文件植入程序都经过签名,因此没有用处。因此,第一步是混淆代码,这很难。有关示例,请参阅利用 Cobalt Strike 配置文件的强大功能来逃避 EDR 。
或者也可以使用加载器,它将植入物作为有效载荷携带并在执行时加载它。这种技术通常使用 C2 生成的 shellcode(或者,可以使用 C2 生成的 DLL 输出或 EXE。可以将其转换为 Shellcode 或 DLL,例如使用 Donut)。使用加载器的优点是可以加密有效载荷,因此唯一需要从 AV 文件签名扫描中混淆的是实际的加载器本身。
公共加载器通常迟早都会被签名。但它们很容易用 Windows 理解的基本上所有语言编写(C、.net C#、vba、vbs、powershell、jscript……)。简单的自写加载器出奇地有效,正如本文将展示的那样。
除了扫描文件之外,EDR 还可以扫描进程的内存。这样可以击败加载程序,因为有效载荷代码必须在内存中解密才能执行。为了避免在内存中检测到,进程需要在休眠时加密其内存区域。这样,当 EDR 扫描进程时,内存中就不应该有任何可疑内容。内存扫描是一项性能密集型操作,只有当 EDR 认为值得时才会执行。这是基于收集的遥测数据(或定期“按需”执行,例如每天一次)。
典型的内存扫描器是 pe-sieve 和 moneta
大多数检测用例都依赖于遥测:Windows 中的重要函数调用会生成事件,这些事件由 EDR 进行处理、关联和分析。例如更改内存区域的权限、创建进程和线程、复制内存等。
例如,如果我们使用加载程序绕过 AV,并简单地为我们的 shellcode 分配一个内存区域,我们就不会为 EDR 生成太多遥测数据。但有效载荷将被内存扫描仪检测到。如果我们引入内存加密来绕过内存扫描仪,那么我们会生成更多的遥测数据,进而可以用来检测内存加密。
带有艾克记忆加密的Bubbles of Bane:
AV 签名扫描
当文件被写入磁盘时,AV 会对其进行扫描。AV 有一个包含已知恶意软件签名的数据库(如 yara 规则)。文件写入事件由操作系统生成,并通过 AMSI 或内核微过滤器传递给 AV。
签名扫描基于文件的静态内容。将解析 PE 标头并扫描 PE 部分的内容。它发生在 EXE 执行之前。一旦检测到,将在执行前删除该文件。
签名看起来类似于 yara 规则:
// https://github.com/Yara-Rules/rules/blob/master/malware/APT_APT17.yar (shortened)
rule APT17_Sample_FXSST_DLL
{
meta:
...
strings:
$x1 = "Microsoft? Windows? Operating System" fullword wide
$x2 = "fxsst.dll" fullword ascii
$y1 = "DllRegisterServer" fullword ascii
$y2 = ".cSV" fullword ascii
$s1 = "VirtualProtect"
$s2 = "Sleep"
$s3 = "GetModuleFileName"
condition:
uint16(0) == 0x5a4d and filesize < 800KB and ( 1 of ($x*) or all of ($y*) ) and all of ($s*)
}
一个通用的解决方案是代码混淆,本文不会介绍它。它通常不能可靠地应用于编译后的代码,但需要纳入编译过程。这意味着每个工具都需要自己实现它。
它将解决我们所有的问题:磁盘或内存中没有签名,不需要加载它,因此没有遥测。
https://retooling.io/blog/an-unexpected-journey-into-microsoft-defenders-signature-world https://avred.r00ted.ch
AV 仿真
AV 组件还将执行目标二进制的模拟。
模拟意味着 AV 将自行读取和解释 .text 部分中的 ASM 指令。它不会在本机执行这些指令,它不是虚拟化执行,也不是 qemu/bochs 完整模拟。它是一种 CPU 模拟,包括常见的 Windows 系统调用和子系统。
伪代码如下:
asm_bytes = [
0xB8, 0x04, 0x00, 0x00, 0x00, # mov eax, 4
0xBB, 0x06, 0x00, 0x00, 0x00, # mov ebx, 6
0x01, 0xD8 # add eax, ebx
]
asm_instructions = disassembler.disasm(asm_bytes);
# asm_instructions = [
# { name = "mov", src = "4", dst="eax" }
# { name = "mov", src = "6", dst="ebx" }
# { name = "add", src = "ebx", dst="eax" }
# ]
for instruction in asm_instructions:
if instruction.name == "add":
register[instruction.dst] += register[instruction.src]
if instruction.name == "mov":
...
AV 仿真为 X86 汇编创建了自己的“解释器”,并重新实现了部分 Windows 操作系统系统调用,以及虚拟文件系统(FileOpen())、虚拟注册表RegOpen()、虚假进程等。该ntdll.dll函数 GetUserNameA()可以实现为始终返回“JohnDoe”。
RedTeamer 的示例经验:
编写加载器
插入 Metasploit shellcode
文件被放到磁盘上时被检测到
然后:
编写第二个加载器
使用强 AES 加密 metasploit shellcode
当放到磁盘上时仍能检测到
AV 模拟器将执行/模拟加载程序。一段时间后,执行停止,并在内存中发现 Metasploit shellcode 未加密。然后 AV 将在内存中检测它的签名。
检测模拟器的可能性是无限的。但一般来说,模拟器不会永远运行,而是受到以下限制:
参考:
Windows Offender:对 Windows Defender 的防病毒模拟器进行逆向工程 (视频)https://i.blackhat.com/us-18/Thu-August-9/us-18-Bulazel-Windows-Offender-Reverse-Engineering-Windows-Defenders-Antivirus-Emulator.pdf
接收事件
EDR 通过操作系统接收进程正在执行的事件:
接收数据的渠道主要有两个:
用户模式(挂钩 API)
内核回调(ETW、ETW-TI、内核模式驱动程序)
当添加/删除/更改某些内容时,这些传感器将创建有关系统正在发生的事情的事件,例如:
文件
注册表项
进程、线程
内存区域
EDR 将包含与恶意行为事件相匹配的规则。规则可以是:
精确/脆弱:能很好地检测某一特定事物(低假阳性FP),容易绕过
稳健性:更通用的检测,更难绕过,FP 更高,更多异常
请注意,EDR 本身无法看到进程内部的数据修改。换句话说,调用函数RtlCopyMemory()的进程ntdll.dll可能会生成遥测数据,因为ntdll.dll可以将其挂钩。在 for 循环中对字节复制执行相同操作不会产生任何遥测数据。
遥测数据可从挂钩ntdll.dll和内核获取。用户模式挂钩可以轻松删除,但这会生成遥测数据。内核空间事件更可靠,无法删除。
请注意,Windows 的主要执行单元是线程,而不是进程。但为了简单起见,我将主要使用进程。
该图形有点过于简单,可以通过更多传感器进行扩展,这些传感器是 EDR 的输入:
因此,EDR 输入为:
用户模式钩子/AMSI
内核回调
ETW
ETW-TI
我将对每一个问题分别进行讨论。
用户模式钩子
虽然 Linux 的官方内核接口是系统调用,但对于 Windows 来说,它的是ntdll.dll。这称为本机 API (NtAPI)。ntdll.dll将为我们调用正确的系统调用。Windows 应用程序接口 (WinAPI),其他 DLL 类似kernel32.dll,都在末尾使用或调用 NtAPI ( ntdll.dll)。请注意,系统调用号可能会因 Windows 版本而异,因此对它们进行硬编码并不可靠。
示例 NtAPI 函数ntdll.dll,使用 ASM 指令执行系统调用syscall:
SysNtCreateFile proc
mov r10, rcx
mov eax, 55h
syscall
ret
SysNtCreateFile endp
典型的 WinAPI 调用,带有一个hook:
用户空间钩子只是ntdll.dll导出函数中的补丁,它在函数执行之前调用另一个 DLL。Windows 提供了直接钩子函数的功能。
Original Function On-Disk: EDR Hooked Function In-Memory:
---------------------- -----------------------
mov r10, rcx mov r10, rcx
>mov eax, 50h jmp 0x7ffaeadea621
test byte ptr [0x7FFE0h], 1 test byte ptr [0x7FFE0h], 1
jne 0x17e76540ea5 jne 0x17e76540ea5
syscall syscall
ret ret
常见挂钩ntdll.dll函数的示例:
EDR 接收函数调用名称及其参数作为遥测数据。
这是通过使用内核回调(PsSetCreateProcessNotifyRoutine)在早期阶段每当有新进程创建时得到通知,然后将 DLL 注入到该进程中(如amsi.dll),通过使用异步过程调用(kKAPC 注入)修补原始ntdll.dll函数以绕道而行来实现的amsi.dll。
ntdll.dll修补后,每个函数调用都会被拦截amsi.dll。
使用 KAPC 进行 EDR 函数挂钩将创建一个执行挂钩的 APC。“Early Bird APC 注入”技术使用相同的 APC 机制,因此可以在执行 KAPC 挂钩之前运行。
可以使用以下方法绕过用户模式钩子:
直接系统调用(避免调用ntdll.dll)
间接系统调用(调用ntdll.dll函数,但在钩子之后)
修补/恢复ntdll.dll(彻底移除挂钩)
用户模式钩子很容易被绕过,因为它们完全位于“我们自己的”内存空间中,我们可以自由地对其进行操作。但恢复ntdll.dll自身会产生遥测数据,这就是为什么要使用直接系统调用来实现这一点的原因。
EDR 不应仅依赖用户模式钩子,而应仅将其用于辅助遥测。但它们提供的信息比内核回调更多。内核回调仅“看到” syscall/ntdll.dll 函数,而不是最初启动的原始函数。这很有用,因为它可以生成更通用的检测,而无需挂钩所有奇怪和不寻常的 DLL 函数。但它可能会产生更多的误报,因为仅使用系统调用来识别“非恶意”行为更加困难。
例如,、 和CreateFileA()都会CreateFileW()在 最后调用。OpenFile()CreateFileTransacted()NtCreateFile()
请注意,调用堆栈可以显示链中最初调用了哪个函数。用户模式钩子越来越少使用,而且并非所有 EDR 都使用用户模式钩子( 来源):
内核遥测
Windows 操作系统以通知回调例程的形式提供有关进程的信息。尤其是有关进程、线程和映像创建的信息。它由内核本身生成,无法像使用用户模式挂钩(没有内核权限)那样抑制它们。
这些回调是在相关进程和线程的上下文中启动的。因此,事件包含有关原始进程的信息。
内核模式检测有多种不同的来源:
ETW(Windows 事件跟踪基础结构)
ETW-TI(线程智能)
内核回调(PsSetCreateProcessNotifyRoutine 等)
NDIS / Minifilter 驱动程序(用于文件系统)
内核回调包括:
PsSetCreateProcessNotifyRoutine:进程创建、终止
PsSetCreateThreadNotifyRoutine:线程创建、删除
PsSetLoadImageNotifyRoutine:Windows 图像加载器
ObRegisterCallbacks:对象管理器回调,如 NtOpenProcess、NtOpenThread、NtOpenFile 等
参考:
https://blog.whiteflag.io/blog/from-windows-drivers-to-a-almost-complete-working-edr/
一个示例事件是PS_CREATE_NOTIFY回调,它向 EDR 提供不同的信息:
Sysmon 可以从内核捕获该事件,并会产生 以下内容:
Process Create:
RuleName: -
UtcTime: 2024-04-28 22:08:22.025
ProcessGuid: {a23eae89-bd56-5903-0000-0010e9d95e00}
ProcessId: 6228
Image: C:\Windows\System32\wbem\WmiPrvSE.exe
FileVersion: 10.0.22621.1 (WinBuild.160101.0800)
Description: WMI Provider Host
Product: Microsoft® Windows® Operating System
Company: Microsoft Corporation
OriginalFileName: Wmiprvse.exe
CommandLine: C:\Windows\system32\wbem\wmiprvse.exe -secured -Embedding
CurrentDirectory: C:\Windows\system32\
User: NT AUTHORITY\NETWORK SERVICE
LogonGuid: {a23eae89-b357-5903-0000-002005eb0700}
LogonId: 0x7EB05
TerminalSessionId: 1
IntegrityLevel: System
Hashes: SHA1=91180ED89976D16353404AC982A422A707F2AE37,MD5=7528CCABACCD5C1748E63E192097472A,SHA256=196CABED59111B6C4BBF78C84A56846D96CBBC4F06935A4FD4E6432EF0AE4083,IMPHASH=144C0DFA3875D7237B37631C52D608CB
ParentProcessGuid: {a23eae89-bd28-5903-0000-00102f345d00}
ParentProcessId: 580
ParentImage: C:\Windows\System32\svchost.exe
ParentCommandLine: C:\Windows\system32\svchost.exe -k DcomLaunch -p
ParentUser: NT AUTHORITY\SYSTEM
请注意,只有字段ImageFilename、CommandLine、ParentProcessId 直接转换为内核事件的Image、CommandLine、ParentProcessId。但大多数其他信息都是由 Sysmon 额外收集的。这些附加信息是通过查询内核(例如通过GetProcessInformation在 上发出 )收集的ProcessId。或者以其他方式收集,例如解析进程的 PEB。并非所有提供的信息都同样值得信赖。
使用 SilkETW 记录的ETWImageLoad事件Microsoft-Windows-kernel-Process:
{
ProviderGuid: "22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716",
ProviderName: "Microsoft-Windows-kernel-Process",
EventName: "ImageLoad",
ThreadID: 9584,
ProcessID: 7536,
ProcessName: "notepad",
YaraMatch: [],
Opcode: 0,
OpcodeName: "Info",
TimeStamp: "2024-07-08T19:06:10.8845667+01:00",
PointerSize: 8,
EventDataLength: 142,
XmlEventData: {
ProviderName: "Microsoft-Windows-kernel-Process",
FormattedMessage: "Process 7’536 had an image loaded with name \Device\HarddiskVolume2\Windows\System32\notepad.exe. ",
EventName: "ImageLoad"
ProcessID: "7’536",
PID: "7536",
TID: "9584",
PName: "",
DefaultBase: "0x7ff631650000",
ImageName: "\Device\HarddiskVolume2\Windows\System32\notepad.exe",
ImageBase: "0x7ff631650000",
ImageCheckSum: "265’248",
ImageSize: "0x38000",
MSec: "9705.0646",
TimeDateStamp: "1’643’917’504",
}
}
内存区域
启动 .exe 时,PE .exe 文件中的各个部分会被完全以块的形式复制到内存中。
.text包含汇编代码,而.data和类似的包含程序的数据。
可以使用或类似方法创建新的内存区域VirtualAlloc()。
来自 PE 映像的内存区域称为备份区域。它们是值得信赖的,因为它们是 AV 在磁盘上扫描的 PE 文件的 1:1 副本。内存区域由磁盘上的文件“备份”。它也可以称为 IMAGE 区域。
如果进程通过分配来分配额外的内存,则该内存为“无备份”。也称为用户内存或私有内存。没有文件后端,因此它是“无备份的”。
一般认为,内存区域具有以下属性:
USER/PRIVATE/Unbacked:不良、潜在恶意的 shellcode
图像/背景:很好,非常值得信赖
这主要是因为漏洞利用或进程注入的 shellcode 通常位于 PRIVATE 内存中。此外,线程应该从备份区域启动。PRIVATE RWX 内存更加可疑。
这里有一些 IMG 类型(IMAGE,backed)的可信内存区域:
这里有一些类型为 PRV (PRIVATE,无备份) 的不可信内存区域:内存区域损坏
内存页的一个属性是写入时复制 (COW)。内存扫描器能够检查内存页是否被写入,这对于只读 .text 部分和其他部分来说并不常见,因为这些部分应该在进程之间共享。Moneta 通过PSAPI_WORKING_SET_EX_BLOCKfromPSAPI_WORKING_SET_EX_INFORMATION结构使用它。首选仅数据攻击,例如针对 AMSI 补丁或 ETW 补丁。
参考:
https://www.trustedsec.com/blog/windows-processes-nefarious-anomalies-and-you-memory-regions
https://www.arashparsa.com/bypassing-pesieve-and-moneta-the-easiest-way-i-could-find/
https://www.outflank.nl/blog/2023/10/05/solving-the-unhooking-problem/
https://www.ired.team/offective-security/code-injection-process-injection/ntcreatesection-+-ntmapviewofsection-code-injection
内存扫描
内存签名扫描将检测内存中的恶意代码,无论是在 .text 还是数据部分(堆栈、堆、.data 等)。
它基本上与 AV 签名扫描相同;针对已知的恶意签名,对内存内容进行 grep 或 yara 检测。
内存扫描对性能要求很高。它不是持续进行的,而是依赖于触发器。
查询进程信息
EDR 在收到事件后,还将尝试丰富它:
进程信息(如可执行文件名称和命令行参数)
内存扫描(可能)
处理图像文件扫描(很少)
EDR 不仅会接收事件,还会主动向操作系统查询更多信息。例如,在接收PS_CREATE_NOTIFY事件时,EDR 将获取有关创建事件的进程的更多信息,例如通过使用GetProcessInformation()或OpenProcess(),访问 PEB、参数或内存区域。或者访问ImageFileName并扫描原始 EXE 映像文件。
请注意,即使经过 SYSTEM 或 PPL,EDR 也是一个正常进程,并且拥有自己的专用内核驱动程序。凭借其 SYSTEM 权限,它可以收集有关几乎所有其他进程的信息。
以下是处理程序函数的一个例子PsSetCreateProcessNotifyRoutine:
void CreateProcessNotifyRoutine(HANDLE ppid, HANDLE pid, BOOLEAN create) {
if (create) {
PEPROCESS process = NULL;
PUNICODE_STRING processName = NULL;
// Retrieve the process name from the EPROCESS structure
PsLookupProcessByProcessId(pid, &process);
SeLocateProcessImageName(process, &processName);
DbgPrint("MyDumbEDR: %d (%wZ) launched", pid, processName);
}
}
处理函数仅接收pid进程的。为了显示图像名称,必须调用一些函数来访问 PEB 或 EPROCESS 结构。
数据存放在PEB(Process Environment Block,进程环境块GS:[0x60])中。它处于用户模式,可以自由操作。
图像基地址
已加载的 DLL
工艺参数:
图片名称
参数
环境变量
工作目录
EPROCESS 是一个内核数据结构,不能直接操作(有时是间接操作):
进程创建和退出时间
进程 ID
父进程 ID
PEB 地址
图像文件名
类似于PEB中的流程参数图像名称
也可以在 SectionObject 中使用
流程信息数据结构
PEB:
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
而ProcessParameters:
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
调用堆栈分析
当进程调用某个 Windows 函数时,可以找出导致此次调用的父函数。这称为调用堆栈。
EDR 可以选择检查启动函数或 API 调用的进程,并分析调用堆栈中是否存在可疑情况:
使用该技术可以检测多种攻击和绕过。但它对性能要求较高。
调用堆栈的起源应该来自备用内存中的内存区域,经过支持 DLL(例如user32.dll),然后 ntdll.dll,最后syscall执行实际指令。
Elastic 具有调用堆栈分析规则来识别:
直接系统调用
基于回调的逃避攻击
模块踩踏
从未支持的区域加载库
从不受支持的区域创建的进程
如果调用来自不受支持的区域,则它很可能来自 shellcode。
调用堆栈分析通常不适用于所有 API 函数。Elastic 提到以下内容:
VirtualAlloc、VirtualProtect
MapViewOfFile、MapViewOfFile2
VirtualAllocEx,VirtualProtectEx
队列用户APC
设置线程上下文
写入进程内存、读取进程内存
参考:
https://www.elastic.co/security-labs/upping-the-ante-detecting-in-memory-threats-with-kernel-call-stacks
https://www.elastic.co/security-labs/doubling-down-etw-callstacks
线程状态分析
线程可能因各种原因而处于休眠状态。通过调查状态以及线程如何由于其调用堆栈而进入休眠状态,我们发现了休眠信标或内存加密的指标。
清理(欺骗)调用堆栈NtDelayExecution():
如果正在使用内存加密,则通常通过调用以下任一方法使线程进入睡眠状态:
Kernelbase.dll!SleepEx
ntdll.dll!NtDelayExecution
对这些睡眠函数的调用有疑点:
调用堆栈中的虚拟内存调用
来源位于非支持内存区域
参考:
https://www.mdsec.co.uk/2022/07/part-1-how-i-met-your-beacon-overview/
性能影响
EDR 的性能至关重要。如果开发人员的机器在安装 10,000 个 NPM 包时速度很慢,人们就会转向 Apple,因为 Apple 的保护措施较少,而 Microsoft 不能允许这种情况发生。这是一个严重的问题,因此 Microsoft 引入了异步Dev Drive扫描。
如果检测可以直接应用于罕见事件(比如打开 lsass.exe 的进程句柄),则性能最不密集的操作。内存扫描可能涉及迭代或 yara 扫描兆字节的 .text 部分,这非常昂贵。扫描文件是最昂贵的,即使使用 SSD 也是如此。
大多数检测都介于两者之间:一个或多个事件包含可疑信息,从而导致更多关联。然后这些事件可能会启动内存扫描。
什么可能触发内存扫描?
VirtualAlloc()并且WriteProcessMemory()通常被称为函数。CreateRemoteThread()不仅不经常被调用,而且它还是潜在恶意行为的更明显指标。
EDR 攻击
EDR 接收来自大量传感器的事件,这些传感器的可信度各不相同。此外,所需的许多信息在事件本身中不可用,而必须在内核(KPROCESS、EPROCESS)或进程内存空间本身(例如包括命令行参数、父进程 ID 的 PEB)中或通过内核访问。
许多攻击都依赖于TOCTOU漏洞的事实:检查时间、使用时间。
命令行欺骗
EDR 可以检查新生成的进程是否存在潜在的恶意命令行参数,例如使用 mimikatz 时:mimikatz.exe "privilege::debug" "lsadump::sam"。即使我们重命名mimikatz.exe,参数privilege::debug也是一个非常明确的指标,误报率很低。
但在 Windows 中,命令行参数是可以伪造的。进程的命令行参数存储在相应进程的 PEB 中。此外,当我们创建新进程时,进程创建函数还将包含初始参数(要启动的 exe 的参数)。
因此我们基本上有两个地方可以放置命令行参数:
在子进程的 PEB 中
在子创建函数中:CreateProcessW(..., "command line args", ...)
在 PEB 中:
typedef struct _PEB {
...
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
...
}
typedef struct _RTL_USER_PROCESS_PARAMETERS {
...
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} *PRTL_USER_PROCESS_PARAMETERS;
由于 PEB 可通过其流程进行修改,因此其中的数据不可信。
EDR 向现有进程查询其命令行,并且通常盲目信任它:
但可以验证一下,当父进程调用CreateProcess()创建子进程时:
EDR 可以比较命令行CreateProcess()和结果子进程的 PEB,如果它们不匹配则发出警报。
拦截函数调用参数实际上CreateProcessW(..., "command line args", ...) 也没有多大帮助,因为我们可以使用虚假参数创建处于暂停状态的进程,然后远程用正确的参数覆盖它们,然后恢复该进程。
父级:使用虚假参数创建新的暂停进程
EDR:接收带有虚假参数的事件
父级:使用真实参数覆盖子级的 PEB
父进程:继续(启动)子进程(使用实参数)
子进程:再次用假参数覆盖其 PEB
EDR:查询进程获取虚假参数
如果 EDR 将来认为子进程是恶意的,它将向分析师提供信息,包括从 PEB 中获取的进程的命令行参数。因此,子进程需要再次覆盖 PEB,作为“清理”。
因此,进程的命令行参数是相当不可信的。
PPID 欺骗
在 Windows 中,与 Linux 不同,父进程和子进程之间没有依赖关系,因为没有fork()。子进程从父进程获取某些属性,包括父进程的 PID。它也将存储在进程的 EPROCESS 结构中。
CreateProcessW()可以指示该函数在STARTUPINFOEX结构中提供其自身的属性,包括子进程的父进程。因此,在创建时,我们可以为子进程指定错误的父进程 PID。
CreateProcessW()界面:
BOOL CreateProcessW(
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo, // PPID spoofing here
[out] LPPROCESS_INFORMATION lpProcessInformation
);
实际的 PPID 欺骗只是设置属性struct STARTUPINFOEX并将其作为lpStartupInfo参数:
{
STARTUPINFOEXA si;
HANDLE fakeParent = OpenProcess(.., <pid of fake parent process>);
..
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &fakeParent, ..);
CreateProcessA(NULL, (LPSTR)"notepad", .., EXTENDED_STARTUPINFO_PRESENT, .., &si.StartupInfo, ..);
}
在哪里:
typedef struct _STARTUPINFOEXA {
STARTUPINFOA StartupInfo;
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; // attributes, one is the ppid
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;
它将存储在EPROCESS内核结构中:
typedef struct _EPROCESS
{
KPROCESS Pcb;
...
HANDLE InheritedFromUniqueProcessId; // PPID
...
}
EDR 可以使用以下命令检索NtQueryInformationProcess():
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation, // PROCESS_BASIC_INFORMATION
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
ULONG_PTR AffinityMask;
KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId; // PID
} PROCESS_BASIC_INFORMATION;
可以检测到 PPID 欺骗,因为在创建进程时,会向 EDR 传递有关新进程的事件。此事件通常位于原始进程的上下文中,或者在其中引用该进程。然后,EDR 可以将结构的内容 STARTUPINFOEX与事件来自的进程进行比较(例如,只需比较两者的 PID)。在这里,EDR 看到CreateProcess()PPID=y 的调用(2),以及发起此调用的进程的有效 PID(1),其 PID=x。
因此,EDR 具有:
父级:PID
父进程:PPID 在其发出的CreateProcess()发往子进程的调用中
子代:其 PPID
并比较它们,尤其是 1) 和 2)。或者后面的 1/2 和 3。对于收到的事件,原始 PID 来自哪里并不总是完全清楚的(例如 ETW)。
请注意,它InheritedFromUniqueProcessId存储在 EPROCESS 中,但仍然不可信任,因为它可以从用户空间进行设置。
ETW 补丁
ETW 补丁将覆盖EtwEventWrite(),ntdll.dll因此该进程将不再自行发出任何 ETW 事件。这主要适用于 Powershell 和 .NET 相关事件。它通常涉及:
VirtualProtect .文本:RX -> RW
覆盖内存(用 替换函数体return 0)
VirtualProtect .文本:RW -> RX
https://jsecurity101.medium.com/understanding-etw-patching-9f5af87f9d7b https://jsecurity101.medium.com/refining-detection-new-perspectives-on-etw-patching-telemetry-e6c94e55a9ad
禁用 AMSI-AV 功能通常由加载程序在执行签名良好的恶意托管代码或 Powershell 脚本之前完成。加载程序正在接受扫描,但运行时加载的 .NET/Powershell 不会被扫描。
这对于在 powershell 中加载签名的恶意 powershell 脚本非常有用,否则该脚本将被 AMSI 接口扫描。一个著名的生成混淆 AMSI-AV 补丁的网站是https://amsi.fail。
AMSI-hooks 修补
ntll.dllAMSI-hook 修补(或 AMSi 修补)只是删除调用 的EDR补丁amsi.dll。它与 ETW 补丁或 AMSI-AV 补丁基本相同,因为它只是ntdll.dll 再次修改。它可以生成额外的遥测数据,例如从磁盘加载干净版本的 时ntll.dll。
参考:
https://github.com/ZeroMemoryEx/Amsi-Killer
https://github.com/Vixx/AMSI-BYPASS
AMSI 绕过
AMSI 绕过既可以指绕过上述的 AMSI-AV 接口,也可以指调用 OS 内核函数而不调用ntdll.dll其中的钩子。
这可以通过使用直接系统调用来完成:如果您知道正确的系统调用号,则可以直接调用它,而无需涉及ntdll.dll。
ntdll.dll或者对于间接系统调用:在钩子调用之后重新使用部分函数。
在这两种情况下,AMSI-hook 都会被绕过,并且 EDR 将不会获得任何遥测数据。
如果这是带有 hooked 的正常函数调用图ntdll.dll:
这里与:
直接系统调用:自己执行系统调用(使用正确的系统调用号)
间接系统调用:重用 hooked 的部分ntdll.dll,调用系统调用但不调用钩子
ntdll.dll或者完全用磁盘中未挂钩的版本替换,就像在 RefleXXion 中一样。
参考:
https://eversinc33.com/posts/avoiding-direct-syscalls.html
https://www.outflank.nl/blog/2019/06/19/red-team-tropics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
https://passthehashbrowns.github.io/hiding-your-syscalls
https://github.com/JustasMasiulis/inline_syscall
https://github.com/jthuraisamy/SysWhispers2
https://github.com/klezVirus/SysWhispers3
https://alice.climent-pommeret.red/posts/direct-syscalls-hells-halos-syswhispers2
图像欺骗
与欺骗参数类似,攻击者可能还想“欺骗”exe:启动 EDR 记录的非恶意 exe(如 notepad.exe),然后用恶意 exe(如 mimikatz)替换进程内容。这会试图欺骗 EDR,让其认为已启动非恶意程序。这可以绕过简单的 EDR。
源 .exe 文件被称为进程的映像。
工艺镂空:
还有一些其他的技术:
Process Hollowing:使用以下代码覆盖已暂停进程的进程内存:WriteProcessMemory()
进程复制:使用事务性 NTFS (TxF) 覆盖文件,启动进程,然后回滚事务,以恢复原始文件
Process Herpaderping:将恶意代码写入 exe,创建进程,在扫描之前快速用非恶意内容替换恶意内容
进程重影:创建空文件,将其半删除,写入恶意数据,然后从中创建进程
内存扫描将使用签名(如 AV)扫描进程的内存。因此,即使注入了真正的进程,仍可以识别出像 CobaltStrike 这样的恶意代码。
或者通过将进程内存内容与 exe 文件内容进行比较。原始 exe 名称存储在 PEB(peb.ProcessParameters.ImagePathName)或内核的 EPROCESS 结构(eprocess.ImageFilename[15],eprocess.SeAuditProcessCreationInfo.ImageFileName)中。将内存内容与文件内容进行比较会耗费大量性能。
或者,EDR 可以收集识别操作的遥测数据。或者使用直接系统调用之类的支持技术,例如使用调用堆栈分析。
挖空参考:
https://www.ired.team/offective-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations
https://github.com/m0n0ph1/Process-Hollowing
https://www.darkrelay.com/post/demystifying-hollow-process-injection
模块踩踏(Module Stomping)
这与图像欺骗类似,但使用 DLL。
模块踩踏将 shellcode 写入远程进程中未使用的 DLL 的 .text 部分,并从那里开始创建新的线程。
与图像欺骗相同,可以通过以下方式检测:
内存签名扫描
.text 部分的内存/文件比较
踩踏遥测
识别支持技术,例如使用遥测的直接/间接系统调用
参考:
https://www.blackhillsinfosec.com/dll-jmping/
https://blog.f-secure.com/hiding-malicious-code-with-module-stomping/
https://blog.f-secure.com/hiding-malicious-code-with-module-stomping-part-2/
https://trustedsec.com/blog/loading-dlls-reflections
https://williamknowles.io/living-dangerously-with-module-stomping-leveraging-code-coverage-analysis-for-injecting-into-legitimately-loaded-dlls/
https://notes.dobin.ch/#root/PBXfEsTWGbEg/yFUsQJlBd3r0/iMYKnoX7AZ7w/W5TwpJ5or9DW-dRWk
内存加密
可以在休眠前加密所有可疑区域,并在进程恢复时再次解密。这并非易事,需要非常小心、奇怪的 Windows 功能以及有效负载(例如信标本身)的支持。它可以创建大量遥测数据,但其中大部分都无法被 EDR 很好地捕获。
信标通常Sleep()会持续一段时间。如果它使用内存加密,则在此期间执行的任何扫描都只会看到加密内存。
调用堆栈欺骗
调用堆栈基本上是一个函数调用层次结构:一个函数列表,每个函数都由其前面的函数调用。当进程调用系统调用(或挂钩ntdll.dll函数)时,EDR 可以检索并分析此列表。
当使用直接系统调用、间接系统调用或其他诡计时,调用堆栈默认看起来“错误”,这可以通过 EDR 识别。
调用堆栈欺骗可确保调用堆栈再次看起来真实。它是一种支持技术:例如,可以使用调用堆栈检测 AMSI 绕过,因此我们需要改进 AMSI 绕过,使调用堆栈看起来更自然。
实际的调用堆栈欺骗通常不会生成遥测数据,并且可以相当安全地实现。但通过重新使用现有的调用堆栈欺骗实现,可以通过签名扫描(无论是在磁盘上还是在内存中)来识别它。
可疑的调用堆栈NtDelayExecution():
清理(欺骗)调用堆栈NtDelayExecution():
反检测依赖于伪造调用堆栈、复制干净的调用堆栈或隐藏恶意调用堆栈。存在许多检查调用堆栈完整性的技术,通常是通过与其他信息关联。例如,线程起始地址应该来自合理的位置。
在普通线程中,用户模式起始地址通常是线程堆栈中的第三个函数调用 - 位于 ntdll!RtlUserThreadStart 和 kernel32!BaseThreadInitThunk 之后。因此,当线程被劫持时,调用堆栈中这一点将显而易见。对于“早起的” APC 注入,调用堆栈的基础将是 ntdll!LdrInitializeThunk、ntdll!NtTestAlert、ntdll!KiUserApcDispatcher,然后是注入的代码。
参考:
https://sabotagesec.com/gotta-catch-em-all-catching-your-favorite-c2-in-memory-using-stack-thread-telemetry/
https://trustedsec.com/blog/windows-processes-nefarious-anomalies-and-you-threads
https://www.mdsec.co.uk/2022/07/part-1-how-i-met-your-beacon-overview/
https://gist.github.com/jaredcatkinson/23905d34537ce4b5b1818c3e6405c1d2
https://whiteknightlabs.com/2024/04/30/sleeping-safely-in-thread-pools/
https://oldboy21.github.io/posts/2024/06/sleaping-issues-swappala-and-Reflective-dll-friends-forever/
https://oldboy21.github.io/posts/2024/05/swappala-why-change-when-you-can-hide/
https://kyleavery.com/posts/avoiding-memory-scanners/
远程进程
攻击者可以选择干扰自己的进程,还是干扰系统中的另一个进程。此处描述的 Windows 函数大多也可用于另一个进程,只需OpenProcess()先使用即可。
这主要用于进程注入。迁移到另一个进程(如teams.exe)非常有用。它的C2可以隐藏在应用程序的正常通信中,它是JavaScript,因此有很多RW->RX分配。
EDR 会更严格地审查与远程进程的交互,因此留在自己的进程中更安全。对于迁移,请使用 DLL 侧载或其他不依赖OpenProcess() 某些东西的技术。
其中包括:
VirtualAllocEx() / VirtualFreeEx()
读取进程内存() / 写入进程内存()
创建远程线程()
查询信息处理()/Nt查询信息处理()
暂停进程
一种非常常见的方法是创建一个带有参数的暂停进程CREATE_SUSPEND,然后对其进行处理,然后让其执行/恢复。
CreateProcessA("C:\\Windows\\System32\\calc.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
...
ResumeThread(pi.hProcess);
许多技术都依赖于此功能。目前使用暂停进程似乎不会对 EDR 造成太大影响,但这可能会在未来改变它。
例如,我们可以创建一个处于挂起状态的新进程,并排队 APC 来执行我们的 shellcode,这可能使其对 EDR 不可见(因为它可能在 KAPC 注入之前执行)。
结尾
EDR 智慧
使用威胁检查或 avred 来识别你的资料中哪些部分被 AV 识别,然后对其进行修补
内存扫描需要耗费大量的性能,通常需要触发才能执行
用户模式 AMSI 越来越不重要,因此 AMSI-hooks 修补也很重要
编写加载器时的错误
使用函数调用复制内存
投入超过最低限度的努力来处理熵
投入超过最低限度的努力来处理加密
生成过多遥测数据
线程未在备用内存中启动
再次标记 RX 页面 RW
调用栈不干净
建议的加载器
建议的装载机布局:
EXE 文件:所有代码都应包含在 .text 部分 (IMAGE) 中
执行护栏:仅允许其在预期目标上执行(反中间盒)
反模拟:阻止 AV 模拟我们的二进制文件(内存使用情况、CPU 周期数、时间欺骗……)
EDR 风水:通过使用非恶意数据和免费数据进行大量 Alloc/Copy/VirtualProtect 循环来调节 EDR
有效载荷:加密(如何加密无所谓)
Alloc/Decode/Virtualprotect/Exec:尽可能正常(避免在此处使用 DLL 函数)。避免 RWX。
有效载荷执行:尽可能正常(跳转到有效载荷,避免创建新线程)
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里