1. 什么是Syscall
Syscall是一种绕过EDR用户态hook的方式,它通过获取系统调用号,并构造syscall stub的汇编指令直接进入内核态API调用,从而避免了用户态hook的检测。在使用这种技术时,不可避免的引入了一些新的检测特征,如syscall stub中引入的硬编码以及基于栈回溯的syscall的调用防的检测等等。不同的syscall项目也提供了针对这类检测的不同绕过方式。此外,各个项目也在获取系统调用号的方式上有一些区别。
基本的syscall stub如下:
1 | mov r10,rcx |
2. HellsGate
1 | https://github.com/am0nsec/HellsGate |
2.1. 遍历PEB获取系统调用号
该项目通过遍历PEB找到ntdll,之后遍历ntdll的导出表,在找到需要的导出函数后,通过匹配syscall stub中的特征字节码定位到该导出函数的系统调用号。
- 遍历PEB获取ntdll的导出表

- 获取导出函数的系统

通过代码可以发现,HellsGate是通过匹配以下汇编指令定位系统调用号的:
1 | mov r10,rcx |
通过反汇编可以发现,该汇编指令对应的机器码如下:

HellsGate正是通过查找这些机器码定位到系统调用号的:
1 | if (*((PBYTE)pFunctionAddress + cw) == 0x4c |
2.2. 硬编码syscall直接系统调用
HellsGate采用直接系统调用,直接构造syscall stub。HellsGate首先通过HellsGate函数获取当前使用函数的系统调用号,之后使用HellDescent函数直接通过syscall指令进行系统调用。

HellDescent的汇编代码如下:
1 | HellDescent proc near ; CODE XREF: main+180↑p |

3. SysWhispers2
1 | https://github.com/jthuraisamy/SysWhispers2 |
SysWhispers2是一个基于模板的项目,该项目可以生成多个函数的syscall模板。同时增加了Random Syscall Jumps的方式来规避一些特征,std是基础的syscall方法,rnd则是规避了特征的方法。

这里以NtAllocateVirtualMemory函数为例,先来看看SysWhispers2进行系统调用的基本流程。
首先,主程序调用NtAllocateVirtualMemory,该函数是头文件中的外部函数,如下图所示:

我们分析SysWhispers2提供的模板,发现该函数的实现是在asm文件中,它首先将该函数计算的哈希传递给currentHash,之后调用WhisperMain函数

WhisperMain的汇编指令如下:
1 | WhisperMain PROC |

WhisperMain首先调用SW2_GetSyscallNumber来获取系统调用号,保存到eax寄存器中,之后调用SW2_GetRandomSyscallAddress来随机获取一个ntdll导出函数中的一个syscall指令的地址。SysWhispers2并没有直接在主程序中调用syscall指令,而是随机获取一个syscall指令的地址后,跳转到该地址执行syscall指令,这样就规避了在主程序中直接系统调用的特征。
3.1. 地址排序获取系统调用号
我们先来看下SysWhispers2获取系统调用号的方式。在前面的分析中,我们发现SysWhispers2是通过SW2_GetSyscallNumber函数来获取系统调用号的,下面详细跟进一下:

可以看到,函数调用了SW2_PopulateSyscallList,继续跟进,发现该函数解析了PEB拿到了ntdll:

之后获取ntdll中所有Zw开头的导出函数的地址:

然后对地址进行排序,对应的序号就是该函数的系统调用号:

3.2. 获取syscall地址间接系统调用
在HellsGate项目中,我们看到它使用的是最基本的直接系统调用,但是这种方式会让主程序成为syscall指令的调用方。而syscall指令通常只出现在ntdll中,这会存在非常明显的特征,杀软/EDR可以通过栈回溯发现syscall调用方,一旦发现syscall指令不是ntdll调用的,很容易判断该程序是恶意的。

正常程序的调用流程如下:

为了规避这种检测,SysWhispers2通过随机获取ntdll中syscall指令的地址,并跳转到该地址执行syscall指令,从而使syscall指令的调用方变成了ntdll:

如上代码,Zw函数起始偏移0x12的位置即是syscall指令,其对应的第一个字节是0x0F。SysWhispers2通过SW2_GetRandomSyscallAddress函数来随机获取一个syscall指令的地址,之后通过call指令调用该地址(位于ntdll)上的syscall指令。
4. FreshyCalls
1 | https://github.com/crummie5/FreshyCalls |
FreshyCalls是使用C++的一些新特性实现的syscall项目,它使用模板通过CallSyscall函数可以实现任意Nt函数的系统调用。这样就不需要像SysWhispers2项目通过python脚本来生成模板,只需要在项目中引入头文件,之后通过CallSyscall函数调用指定的函数即可。
如下,调用NtAllocateVirtualMemory函数:
1 |
|

跟进CallSyscall函数,该函数依次调用GetStubAddr、GetSyscallNumber、FindSyscallInstruction获取函数的syscall stub、系统调用号和syscall指令地址,之后调用InternalCaller来完成函数的系统调用:

4.1. 按地址顺序依次填充map结构
FreshyCalls采用了和SysWhispers2相同的方法获取系统调用号,只是在实现上利用了C++的一些特性。我们来看GetSyscallNumber函数,发现该函数在syscall_map里查找对应函数的系统调用号:

分析代码,发现syscall_map是由ExtractSyscallsNumbers进行填充的:

而ExtractSyscallsNumbers提取的是stub_map里的内容,stub_map是由ExtractStubs函数填充,该函数通过导出表按照地址顺序依次填入Nt函数的地址和函数名。在SysWhispers2中,我们介绍过,导出表地址按照顺序排列即是系统调用号,在ExtractSyscallsNumbers函数中,syscall_map按照地址顺序填入,syscall_no循环递增就是对应函数的系统调用号了:

4.2. 构造trampoline间接系统调用
FreshyCalls也是通过jmp到syscall的地址进行间接系统调用,不同的是,SysWhispers2使用的是随机的syscall地址。另外,FreshyCalls通过mini shellcode的方式避免了引入汇编代码。
FreshyCalls通过InternalCaller函数传递系统调用号给构造好的mini shellcode stub,如果前面的流程中找到syscall指令的地址,也会将该指令地址传递给stub:

FreshyCalls进行系统调用的关键函数是stub,也就是作者构造的mini shellcode,InternalCaller将系统调用号和syscall指令地址传递给stub后,由stub处理最终的系统调用。

在stub中首先做了一些参数处理,将原本保存在rcx rdx中的系统调用号和syscall指令地址提取出来,分别保存在r13 r14两个寄存器中,然后将原本r8 r9中保存的参数存放到rcx rdx中(因为r8 r9中保存的是InternalCaller中的第三、第四个参数,这两个参数刚好是我们要调用的系统函数的第一、第二个参数,根据64位调用约定,第一、第二个参数应存放在rcx rdx寄存器中):
1 | 0x41, 0x55, // push r13 //入栈 |
在处理好传参后,stub将rip偏移0x0C的地址赋值给了r11,也就是实现系统调用的部分,通过call指令执行,将r13中保存的系统调用号交给rax,通过jmp指令跳转到r14中保存的syscall指令地址(该地址位于ntdll中),最终完成间接系统调用:
1 | 0x4C, 0x8D, 0x1D, 0x0C, 0x00, 0x00, 0x00, // lea r11, [rip+0x0C] ---- |
5. PIG-Syscall
1 | https://github.com/evilashz/PigSyscall |
该项目在FreshyCalls的基础上,增加了更多的规避策略,如使用哈希查找函数、对syscall stub进行加密等,其他流程与FreshyCalls相同。
5.1. 异常目录表获取系统调用号
与SysWhispers2、FreshyCalls不同,PIG-Syscall通过异常目录表获取系统调用号,除了使用的表不一样,其他流程与导出表基本相同。此外,PIG-Syscall还将函数名称加密存储在syscall_map中:

5.2. 对syscall stub解密后调用
PIG-Syscall对FreshyCalls项目中的mini shellcode进行了加密,使用InternalCaller调用时再进行解密:

加密后的stub:
1 | ALLOC_ON_CODE unsigned char encrypted_masked_syscall_stub[] = { |

其中,加解密函数CryptPermute是从Exchange邮件编码中提取的,解密后的stub与FreshyCalls一样。
6. HWSyscalls
1 | https://github.com/ShorSec/HWSyscalls |
HWSyscalls利用硬件断点的方式进行间接系统调用,在调用堆栈的处理上更为彻底。
一般的间接系统调用:

HWSyscalls的间接系统调用:

在HWSyscalls前,首先要使用InitHWSyscalls函数进行初始化。初始化包括以下几个内容:
- 获取当前线程句柄,用于获取线程上下文设置断点
- 获取ntdll模块句柄
- 在kernel32或kernelbase中寻找用于返回主程序的Gadget
- 注册异常处理程序,用于触发硬件断点时的处理流程
- 设置硬件断点

FindRetGadget函数用来在kernel32和kernelbase两个模块中查找合适的用于返回主程序的Gadget,在该项目中ADD RSP,68;RET,硬编码为\x48\x83\xC4\x68\xC3:

SetMainBreakpoint函数用于设置初始的硬件断点,该断点设置在函数PrepareSyscall上,该函数用于返回我们实际要调用的Nt函数的地址:

HWSyscallExceptionhandler是注册的异常处理函数,也是这个项目中的核心功能函数,包括查找系统调用号以及间接系统调用。
6.1. 匹配相邻函数的stub
HWSyscalls使用HalosGate相同的方法获取系统调用号,HalosGate是HellsGate的变种,与HellsGate不同的是,HalosGate并不是直接匹配所需函数的stub,而是匹配所需函数相邻的系统函数以避免因为hook导致无法准确获取所需函数的系统调用号:

6.2. 利用硬件断点间接系统调用
由于SetMainBreakpoint将断点设置在PrepareSyscall上,当我们调用该函数寻找系统函数的地址时就会触发断点并进入异常处理函数HWSyscallExceptionHandler。该函数首先判断当前RIP是否为PrepareSyscall,如果是的话,就会提取当前RCX寄存器,即PrepareSyscall中的第一个参数,也就是我们要找的系统函数名称。GetSymbolAddress获取该系统函数的地址,并将断点设置在此处:

我们已经获取到了我们需要的系统函数的地址,调用该函数就会触发第二次的断点,RIP指向我们调用的系统函数的地址。此时,创建一个新的堆栈并将前面FindRetGadget获取到的Gadget地址赋值给RSP作为返回地址:

如果调用的系统函数被hook了,就会调用FindSyscallNumber、FindSyscallReturnAddress获取系统调用号和syscall指令地址:

将RIP设置为syscall指令地址实现间接系统调用:

7. 参考链接
1 | HellsGate |
发布时间: 2023-10-11
最后更新: 2023-12-12
本文标题: Syscall项目笔记
本文链接: https://foxcookie.github.io/2023/10/11/Syscall项目笔记/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!