规避“系统调用标记”
规避“系统调用标记”
规避常见的恶意API调用模式及使用直接系统调用并规避“系统调用标记”,来bypass edr
直接使用系统调用
在用户态运行的系统要控制系统时,或者要运行系统代码就必须取得R0权限。用户从R3到R0需要借助ntdll.dll中的函数,这些函数分别以“Nt”和“Zw”开头,这种函数叫做Native API,下图是调用过程:
这些nt开头的函数一般没有官方文档,很多都是被逆向或者泄露windows源码的方式流出的。
FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
我们可以通过在内存中找到函数的首地址来调用这些nt开头的函数
在反编译syscall函数之后,可以得到一段非常具有特征的汇编指令
1 |
|
用户调用windows api ReadFile,有些edr会hook ReadFile这个windows api,但实际上最终会调用到NTxxx这种函数。有些函数没有被edr hook就可以绕过。说白了还是通过黑名单机制的一种绕过。找到冷门的wdinwos api并找到对应的底层内核api
这里有个很好的网站sycall系统调用号文档
因为syscall在这里存储的是系统调用号
我们可以在visual studio中直接反编译查看汇编代码与字节码
工具->选项->启用地址级调试
在调试过程中,Debug->window->disassembly
动态进行syscall
我们很多时候使用syscall不是直接调用,不会在代码里硬编码syscall的系统调用号。因为不同的系统调用号是不同的,所以我们需要进行动态syscall
这里我们可以直接使用Hell’s Gate来遍历NtDLL的导出表,根据函数名hash,找到函数的地址。接着使用0xb8获取到系统调用号,之后通过syscall来执行一系列函数
通过TEB获取到dll的地址可以参考:前置知识
接下来我们的步骤是
- 解析pe结构,获取导出表
- 遍历hash表和导出表,找到syscall的函数,通过标记的方式获得系统调用号
- 调用syscall,分配内存,修改内存属性,创建线程
这些在Hell’s Hate已经实现,我们重点来看一下这段代码
1 |
|
为什么匹配这几个字节就能找到syscall调用号
这里有张图很明确
可以看到syscall的汇编语句比较固定
1 |
|
而我们逐个字节进行遍历,直到出现mov r10, rcx和move eax,经过位运算得到syscall调用号
这是我们生成的syscall
1 |
|
SysWhispers2
SysWhispers2 是一个合集,用python生成.c源码文件。这些文件的作用和Hell’s Gate类似,也是在PE中找导出表,之后通过对比函数hash找到syscall调用号。相对Hell’s Gate有更多的函数可供选择,不仅仅是内存相关的几个函数。并且对syscall的asm有一定程度的混淆(使用了INT 2EH替换sycall)
我们在动态使用syscall的时候已经发现了,syscall反编译之后的汇编语句比较固定,那这么这样的话syscall特征非常明显,静态特征就很容易被识别到
对于这个问题,SysWhispers2做出了相应的解决办法
egghunter
在fuzzysecurity的二进制教程中提到过相关技术
先用彩蛋(一些随机的、唯一的、可识别的模式)替换syscall指令,然后在运行时,再在内存中搜索这个彩蛋,并使用ReadProcessMemory和WriteProcessMemory等WINAPI调用将其替换为syscall指令。之后,我们可以正常使用直接系统调用了
我们在内存中使用db表示一个字节,比如我们在内存中.txt段写入”w00tw00t”的字节
1 |
|
接下来遍历全文,去寻找我们埋下的彩蛋
1 |
|
这样做虽然可以绕过静态的检测了但依旧存在问题,理论上syscall行为应该只存在ntdll中,而我们使用syscall是在当前程序中。简单的判断RIP就可以检测出我们的可疑行为
而对与RIP的检测,作者也给出了技术方案,还是比较简单的。在内存中搜索syscall的地址,直接jmp到该位置。即可让RIP指向ntdllpython3 syswhispers.py -p common -a x64 -c msvc -m jumper -v -d -o 1
机器学习特征的edr的检测
https://blog.redbluepurple.io/offensive-research/bypassing-injection-detection
windows api hook
- 找到内存中需要被hook的函数地址
LPVOID lpDllExport = GetProcAddress(hJmpMod, jmpFuncName);
- 找到后将前七个字节改为跳转,如下
1
2
3
4
5
6
7
8
9unsigned char jmpSc[7]{
0xB8, b[0], b[1], b[2], b[3],
0xFF, 0xE0
};
```
3. 机器码对应的汇编指令大概是
```arm
move eax,xxxx
jmp eax - 修改内存实现了劫持对应函执行流程的功能。如果想要维持函数原本的功能,保存原本的七个字节,在shellcode中再次替换这部分内存并jump回来
1
2
3
4
5
6
7WriteProcessMemory(
hProc,
lpDllExport,
jmpSc,
sizeof(jmpSc),
&szWritten
);
Windows 内存分配的一些规则
在windows 10 64位下,内存最小的分配粒度为4kB, systeminfo结构体中,标识了这个变量,为内存分页的大小。
在windows中,所有VirtualAllocEx分配的内存,会向上取整到AllocationGranularity的值,windows10下为64kb,比如:
我们在0x40000000的基址分配了4kB的MEM_COMMIT | MEM_RESERVE的内存,那么整块0x40010000 (64kB)区域将不能被重新分配。
实现原理
很多edr将创建远程线程的行为列为可疑行为,比如windows definder仅仅是做记录但并不报警,产生报警还有其他的判断逻辑
- 与其分配一大块内存并直接将~250KB的implant shellcode写入该内存,不如分配小块但连续的内存,例如<64KB的内存,并将其标记为NO_ACCESS。然后,将shellcode按照相应的块大小写入这些内存页中。
- 在上述的每一个操作之间引入延迟。这将增加执行shellcode所需的时间,但也会淡化连续执行模式。
- 使用钩子,劫持RtlpWow64CtxFromAmd64函数,执行恶意shellcode
参考文章
https://xz.aliyun.com/t/11496
https://klezvirus.github.io/RedTeaming/AV_Evasion/NoSysWhisper/
https://blog.redbluepurple.io/windows-security-research/bypassing-injection-detection