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 许可协议。转载请注明出处!