红队队开发基础-基础免杀(二)
字数 1752 2025-08-27 12:33:43
红队开发基础:系统调用与API调用模式规避技术
引言
本文详细介绍了红队开发中两种基础免杀技术:使用直接系统调用并规避"系统调用标记"和规避常见的恶意API调用模式。这些技术主要用于绕过EDR(终端检测与响应)产品的检测机制。
系统调用基础知识
权限级别与系统调用流程
-
Windows系统有四个权限级别(R0-R3),其中:
- R0:内核态,最高权限
- R3:用户态,最低权限
- R1和R2用于运行设备驱动
-
用户态(R3)到内核态(R0)的转换通过ntdll.dll中的Native API实现
-
Native API函数以"Nt"和"Zw"开头,没有官方文档
系统调用机制
基本系统调用汇编形式:
mov r10, rcx
mov eax, xxh ; xxh为系统调用号
syscall
系统调用号决定了调用哪个内核函数,完整列表参考:
https://j00ru.vexillium.org/syscalls/nt/64/
直接系统调用实现
基础实现步骤
- 定义函数原型(参考MSDN文档)
EXTERN_C NTSTATUS SysNtCreateFile(
PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
PLARGE_INTEGER AllocationSize,
ULONG FileAttributes,
ULONG ShareAccess,
ULONG CreateDisposition,
ULONG CreateOptions,
PVOID EaBuffer,
ULONG EaLength);
- 调用函数示例
RtlInitUnicodeString(&fileName, (PCWSTR)L"\\??\\c:\\temp\\test.txt");
ZeroMemory(&osb, sizeof(IO_STATUS_BLOCK));
InitializeObjectAttributes(&oa, &fileName, OBJ_CASE_INSENSITIVE, NULL, NULL);
SysNtCreateFile(
&fileHandle,
FILE_GENERIC_WRITE,
&oa,
&osb,
0,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_WRITE,
FILE_OVERWRITE_IF,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0);
动态系统调用技术
Hell's Gate(地狱之门)
- 遍历NtDLL导出表
- 根据函数名hash找到函数地址
- 通过特征字节码获取系统调用号
- 动态生成syscall汇编代码
特征字节码识别:
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
SysWhispers2
- 使用Python生成.c源码文件
- 在PE中查找导出表并通过函数hash匹配系统调用号
- 使用INT 2EH替代syscall进行混淆
Halo's Gate(光环之门)
应对Native API被hook的情况:
- syscall有32字节存根
- 在内存中向上向下每32字节搜索未被hook的Native API
- 获取系统调用号并减去移动步数
TartarusGate
增加对hook的判断:
- 检测5字节和7字节hook
- 通过判断函数开头第一个和第四个字节是否为E9(JMP指令)
GetSSN
不同思路获取系统调用号:
- 获取所有Zw开头的函数地址
- 按地址排序
- 排序后的序号即为系统调用号
弱化syscall特征
问题
直接使用syscall指令具有明显静态特征,容易被检测
解决方案
-
INT 2EH替代:早期方案,现已被检测
-
EggHunter技术:
- 使用唯一模式(如"w00tw00t")替换syscall指令
- 运行时在内存中搜索并替换为syscall
- 示例:
DB 77h ; "w" DB 0h ; "0" DB 0h ; "0" DB 74h ; "t" DB 77h ; "w" DB 0h ; "0" DB 0h ; "0" DB 74h ; "t" -
RIP重定向:
- 搜索内存中ntdll的syscall地址
- 直接jmp到该地址,使RIP指向ntdll
SysWhispers3
整合多种技术:
# 普通SysWhispers,32位模式
py .\syswhispers.py --preset all -o syscalls_all -m jumper --arch x86
# 使用WOW64的32位模式(仅特定函数)
py .\syswhispers.py --functions NtProtectVirtualMemory,NtWriteVirtualMemory -o syscalls_mem --arch x86 --wow64
# Egg-Hunting SysWhispers,绕过"syscall标记"
py .\syswhispers.py --preset common -o syscalls_common -m jumper
# Jumping/Jumping Randomized SysWhispers,绕过动态RIP验证
py .\syswhispers.py --preset all -o syscalls_all -m jumper -c mingw
规避恶意API调用模式
Windows API Hook原理
- 获取目标函数地址:
LPVOID lpDllExport = GetProcAddress(hJmpMod, jmpFuncName);
- 修改前7字节为跳转指令:
unsigned char jmpSc[7]{ 0xB8, b[0], b[1], b[2], b[3], 0xFF, 0xE0 };
// 对应汇编:mov eax,xxxx ; jmp eax
- 写入内存:
WriteProcessMemory(hProc, lpDllExport, jmpSc, sizeof(jmpSc), &szWritten);
Windows内存分配规则
- 最小分配粒度:4kB(内存分页大小)
- VirtualAllocEx分配的内存向上取整到AllocationGranularity(64kB)
- 例如在0x40000000分配4kB,整个0x40010000(64kB)区域将不可重新分配
规避EDR检测的技术
-
内存分配策略:
- 分配小块连续内存(<64KB)
- 标记为NO_ACCESS
- 按块大小写入shellcode
-
执行延迟:
- 在各操作之间引入延迟
- 淡化连续执行模式
-
函数劫持:
- 钩住RtlpWow64CtxFromAmd64等函数
- 执行恶意shellcode
DripLoader实现
-
搜索空闲内存区域:
- 预定义64位基址列表
- 使用VirtualQueryEx查找适合shellcode的内存区域
-
计算所需内存块数:
cVmResv = shellcode长度/内存块大小 + 1
-
确保内存分配:
- 使用syscall调用NtAllocateVirtualMemory
- 确保每块不超过64KB,以4KB为单位分配
-
写入内存:
- 以4位为单位分批写入
-
函数劫持:
- 获取函数地址并hook
- 跳转到shellcode起始地址
-
创建进程执行shellcode
总结
本文详细介绍了两种主要的EDR绕过技术:
-
直接系统调用技术:
- 绕过用户态hook
- 多种动态获取系统调用号的方法
- 弱化syscall特征的技术
-
API调用模式混淆:
- 非常规内存分配策略
- 执行流程混淆
- 函数劫持技术
这些技术需要结合使用,并根据目标环境的特点进行调整,才能有效绕过现代EDR产品的检测机制。