最近看了很多关于syscall的文章,国外大多数安全研究员使用syscall来绕过edr的hook,使用的语言也五花八门,而我c系列的语言只会一点c#,所以我就用C#来简单实现一个syscall。
本文全文参考以下两篇文章,部分讲解的不如原文清楚,要详细了解的请移步:
在Windows中,进程处理体系被分为两种:用户模式和内核模式。
而两者之间的切换正是syscall在起作用。使用ProcessMonitor观察记事本创建文件的操作
可以看到蓝色的就是用户模式(User Mode),红色的是内核模式(Kernel Mode)。两者之间对于CreateFile进行了切换,从KernelBase.dll!CreateFileW->ntdll.dll!NtCreateFile->ntoskrnl.exe!NtCreateFile。
有两个不同的NtCreateFile函数调用,一个来自ntdll.dll模块,另一个来自ntoskrnl.exe模块,为什么?
ntdll.dll里导出Windows原生API,ntoskrnl里是对其的实现(内核API)。来看一下两种模式之间的切换在CPU中的具体指令。
WinDBG随意Attach一个进程,键入x ntdll!NtCreateFile
命令
这里看到NtCreateFile的汇编指令为
1mov r10,rcx
2mov eax,55h
3syscall
4ret
在syscall指令下发后CPU会跳入内核模式,把函数调用参数从用户模式堆栈复制到内核模式堆栈,执行NtCreateFile的内核版本ZwCreateFile函数,完成后把返回值返回到用户模式,整个系统调用完成。
在cpp中只需要内联asm代码就行,比如我们想编写一个利用NtCreateFile syscall的程序,只需要内联其汇编代码。
1mov r10,rcx
2mov eax,55h
3syscall
4ret
而在C#中没有内联汇编,因为托管代码的原因。
简述下托管代码和非托管代码:C#需要通过.net CLR进行翻译执行,而在CLR中提供了自动垃圾回收、异常处理等,C#代码托管给CLR来运行,叫做托管代码。而cpp是直接编译为系统指令,没有中间商处理,所以叫非托管代码。
尽管没有内联汇编,但是C#仍然提供了一种方式突破托管代码和非托管代码之间的界限:P/Invoke(Platform Invoke)
加委托。
P/Invoke允许C#访问非托管DLL中的结构体、函数等,主要是通过System.Runtime.InteropServices命名空间来操作,先来一个实例,通过该命名空间来调用MessageBox。
1using System;
2using System.Runtime.InteropServices;
3
4public class Program
5{
6 [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
7 private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
8
9 public static void Main(string[] args)
10 {
11 MessageBox(IntPtr.Zero, "Hello from unmanaged code!", "Test!", 0);
12 }
13}
通过P/Invoke的DllImport导入user32.dll里的MessageBox函数来进行调用。
C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。
委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。
先看下委托的基本用法,后面配合P/Invoke进行syscall
1using System;
2using System.Runtime.InteropServices;
3
4namespace Program
5{
6 public static class Program
7 {
8 // 定义与非托管函数相对应的委托。
9 private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
10
11 // 导入user32.dll(包含我们需要的功能)并定义与本机函数相对应的方法。
12 [DllImport("user32.dll")]
13 private static extern int EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
14
15 // 定义委托的实现 在这里只输出窗口句柄。
16 private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
17 {
18 Console.WriteLine(hwnd.ToInt64());
19 return true;
20 }
21
22 public static void Main(string[] args)
23 {
24 // 调用方法 注意将委托作为第一个参数。
25 EnumWindows(OutputWindow, IntPtr.Zero);
26 Console.ReadKey();
27 }
28 }
29}
代码中定义了一个EnumWindowsProc委托,将委托作为第一个参数传入EnumWindows API函数,查看EnumWindows的函数定义
1BOOL EnumWindows(
2 WNDENUMPROC lpEnumFunc,
3 LPARAM lParam
4);
第一个参数是一个指针,指向程序定义的回调。意思就是可以通过传递OutputWindow函数指针进行调用OutputWindow函数。
现在我们知道,委托类似于cpp中的指针,可以将委托作为参数传递。假如我们通过VirtualAlloc分配一段内存并将其返回给我们的委托,那么我们可以通过Type marshaling
来转换传入的数据类型,以在非托管代码和native code
之间进行转换,也就意味着我们可以通过这种方式来执行shellcode。
通过Marshal.GetDelegateForFunctionPointer来将函数指针转为委托。原作者给出的NtOpenProcess的实例。
1using System;
2using System.ComponentModel;
3using System.Runtime.InteropServices;
4
5namespace SharpCall
6{
7 class Syscalls
8 {
9 // NtOpenProcess Syscall ASM
10 static byte[] bNtOpenProcess =
11 {
12 0x4C, 0x8B, 0xD1, // mov r10, rcx
13 0xB8, 0x26, 0x00, 0x00, 0x00, // mov eax, 0x26 (NtOpenProcess Syscall)
14 0x0F, 0x05, // syscall
15 0xC3 // ret
16 };
17
18 public static NTSTATUS NtOpenProcess(
19 // Fill NtOpenProcess Paramters
20 )
21 {
22 // set byte array of bNtOpenProcess to new byte array called syscall
23 byte[] syscall = bNtOpenProcess;
24
25 // specify unsafe context
26 unsafe
27 {
28 // create new byte pointer and set value to our syscall byte array
29 fixed (byte* ptr = syscall)
30 {
31 // cast the byte array pointer into a C# IntPtr called memoryAddress
32 IntPtr memoryAddress = (IntPtr)ptr;
33 }
34 }
35 }
36 }
37}
首先通过WinDBG拿到NtOpenProcess的汇编指令,涉及指针操作的代码需要用到unsafe关键字,fixed关键字用来防止CLR的垃圾回收修改变量地址。当拿到memoryAddress之后我们就可以将其传递给委托使用。即通过Marshal.GetDelegateForFunctionPointer来将函数指针转为委托。
在windbg中拿到的汇编指令如下
10:001> x ntdll!NtCreateFile
200007ff8`50fad0b0 ntdll!NtCreateFile (NtCreateFile)
30:001> u 00007ff8`50fad0b0
4ntdll!NtCreateFile:
500007ff8`50fad0b0 4c8bd1 mov r10,rcx
600007ff8`50fad0b3 b855000000 mov eax,55h
700007ff8`50fad0b8 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
800007ff8`50fad0c0 7503 jne ntdll!NtCreateFile+0x15 (00007ff8`50fad0c5)
900007ff8`50fad0c2 0f05 syscall
1000007ff8`50fad0c4 c3 ret
1100007ff8`50fad0c5 cd2e int 2Eh
1200007ff8`50fad0c7 c3 ret
首先看下api
1__kernel_entry NTSTATUS NtCreateFile(
2 PHANDLE FileHandle,
3 ACCESS_MASK DesiredAccess,
4 POBJECT_ATTRIBUTES ObjectAttributes,
5 PIO_STATUS_BLOCK IoStatusBlock,
6 PLARGE_INTEGER AllocationSize,
7 ULONG FileAttributes,
8 ULONG ShareAccess,
9 ULONG CreateDisposition,
10 ULONG CreateOptions,
11 PVOID EaBuffer,
12 ULONG EaLength
13);
返回值是NTSTATUS一个结构体,ACCESS_MASK、OBJECT_ATTRIBUTES等都是结构体,那么需要先在自己代码中定义其结构体。在https://www.pinvoke.net/ 中可以查到函数及结构体的定义,并且给出了c#代码。
在SharpSysCall\Native.cs中定义了所有用到的结构体和标识符。
然后定义了一个委托
先定义NtCreateFile的汇编指令字节数组
1 static byte[] bNtCreateFile =
2 {
3 0x4C, 0x8B, 0xD1, // mov r10, rcx
4 0xB8, 0x55, 0x00, 0x00, 0x00, // mov eax, 0x55 (NtCreateFile Syscall)
5 0x0F, 0x05, // syscall
6 0xC3 // ret
7 };
接下来是对委托的实现
在实现中,拿到NtCreateFile的在内存中的地址,而在Windows安全模型中,内存需要分配合适的访问权限。通过windbg可以看到NtCreateFile的权限为PAGE_EXECUTE_READ
10:001> !address 00007ff8`50fad0b0
2
3
4Mapping file section regions...
5Mapping module regions...
6Mapping PEB regions...
7Mapping TEB and stack regions...
8Mapping heap regions...
9Mapping page heap regions...
10Mapping other regions...
11Mapping stack trace database regions...
12Mapping activation context regions...
13
14Usage: Image
15Base Address: 00007ff8`50f11000
16End Address: 00007ff8`5102c000
17Region Size: 00000000`0011b000 ( 1.105 MB)
18State: 00001000 MEM_COMMIT
19Protect: 00000020 PAGE_EXECUTE_READ
20Type: 01000000 MEM_IMAGE
21Allocation Base: 00007ff8`50f10000
22Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY
23Image Path: C:\Windows\SYSTEM32\ntdll.dll
24Module Name: ntdll
25Loaded Image Name: C:\Windows\SYSTEM32\ntdll.dll
26Mapped Image Name:
27More info: lmv m ntdll
28More info: !lmi ntdll
29More info: ln 0x7ff850fad0b0
30More info: !dh 0x7ff850f10000
而进程的地址是私有的,一个程序不能修改另一个程序的数据,所以要通过VritualProtect将权限设置为PAGE_EXECUTE_READWRITE。
接下来通过Marshal.GetDelegateForFunctionPointer将指针转化为委托,接下来将委托的执行结果返回。
在Program.cs中进行调用。
总结一下流程:
在ProcessMonitor中监视其堆栈确实是syscall直接系统调用。
缺点:
优点:
胡言乱语:当Marshal.GetDelegateForFunctionPointer被hook时岂不是无解?
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。