使用C#进行直接系统调用syscall

  • A+
所属分类:安全开发

最近看了很多关于syscall的文章,国外大多数安全研究员使用syscall来绕过edr的hook,使用的语言也五花八门,而我c系列的语言只会一点c#,所以我就用C#来简单实现一个syscall。

本文全文参考以下两篇文章,部分讲解的不如原文清楚,要详细了解的请移步:

1.https://jhalon.github.io/utilizing-syscalls-in-csharp-1/2.https://jhalon.github.io/utilizing-syscalls-in-csharp-2/3.https://github.com/jhalon/SharpCall

什么是syscall

在Windows中,进程处理体系被分为两种:用户模式和内核模式。

使用C#进行直接系统调用syscall
image.png

而两者之间的切换正是syscall在起作用。使用ProcessMonitor观察记事本创建文件的操作

使用C#进行直接系统调用syscall
image.png


可以看到蓝色的就是用户模式(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命令

使用C#进行直接系统调用syscall
image.png


这里看到NtCreateFile的汇编指令为

mov     r10,rcxmov     eax,55hsyscallret

在syscall指令下发后CPU会跳入内核模式,把函数调用参数从用户模式堆栈复制到内核模式堆栈,执行NtCreateFile的内核版本ZwCreateFile函数,完成后把返回值返回到用户模式,整个系统调用完成。

使用syscall

在cpp中只需要内联asm代码就行,比如我们想编写一个利用NtCreateFile syscall的程序,只需要内联其汇编代码。

mov     r10,rcxmov     eax,55hsyscallret

而在C#中没有内联汇编,因为托管代码的原因。

简述下托管代码和非托管代码:C#需要通过.net CLR进行翻译执行,而在CLR中提供了自动垃圾回收、异常处理等,C#代码托管给CLR来运行,叫做托管代码。而cpp是直接编译为系统指令,没有中间商处理,所以叫非托管代码。

尽管没有内联汇编,但是C#仍然提供了一种方式突破托管代码和非托管代码之间的界限:P/Invoke(Platform Invoke)加委托。

P/Invoke

P/Invoke允许C#访问非托管DLL中的结构体、函数等,主要是通过System.Runtime.InteropServices命名空间来操作,先来一个实例,通过该命名空间来调用MessageBox。

using System;using System.Runtime.InteropServices;
public class Program{ [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
public static void Main(string[] args) { MessageBox(IntPtr.Zero, "Hello from unmanaged code!", "Test!", 0); }}

通过P/Invoke的DllImport导入user32.dll里的MessageBox函数来进行调用。

使用C#进行直接系统调用syscall
image.png

委托

C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。

委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。

先看下委托的基本用法,后面配合P/Invoke进行syscall

using System;using System.Runtime.InteropServices;
namespace Program{ public static class Program { // 定义与非托管函数相对应的委托。 private delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
// 导入user32.dll(包含我们需要的功能)并定义与本机函数相对应的方法。 [DllImport("user32.dll")] private static extern int EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
// 定义委托的实现 在这里只输出窗口句柄。 private static bool OutputWindow(IntPtr hwnd, IntPtr lParam) { Console.WriteLine(hwnd.ToInt64()); return true; }
public static void Main(string[] args) { // 调用方法 注意将委托作为第一个参数。 EnumWindows(OutputWindow, IntPtr.Zero); Console.ReadKey(); } }}

代码中定义了一个EnumWindowsProc委托,将委托作为第一个参数传入EnumWindows API函数,查看EnumWindows的函数定义

BOOL EnumWindows(  WNDENUMPROC lpEnumFunc,  LPARAM      lParam);

第一个参数是一个指针,指向程序定义的回调。意思就是可以通过传递OutputWindow函数指针进行调用OutputWindow函数。

现在我们知道,委托类似于cpp中的指针,可以将委托作为参数传递。假如我们通过VirtualAlloc分配一段内存并将其返回给我们的委托,那么我们可以通过Type marshaling来转换传入的数据类型,以在非托管代码和native code之间进行转换,也就意味着我们可以通过这种方式来执行shellcode。

Type marshaling

通过Marshal.GetDelegateForFunctionPointer来将函数指针转为委托。原作者给出的NtOpenProcess的实例。

using System;using System.ComponentModel;using System.Runtime.InteropServices;
namespace SharpCall{ class Syscalls { // NtOpenProcess Syscall ASM static byte[] bNtOpenProcess = { 0x4C, 0x8B, 0xD1, // mov r10, rcx 0xB8, 0x26, 0x00, 0x00, 0x00, // mov eax, 0x26 (NtOpenProcess Syscall) 0x0F, 0x05, // syscall 0xC3 // ret };
public static NTSTATUS NtOpenProcess( // Fill NtOpenProcess Paramters) { // set byte array of bNtOpenProcess to new byte array called syscall byte[] syscall = bNtOpenProcess;
// specify unsafe context unsafe { // create new byte pointer and set value to our syscall byte array fixed (byte* ptr = syscall) { // cast the byte array pointer into a C# IntPtr called memoryAddress IntPtr memoryAddress = (IntPtr)ptr; } } } }}

首先通过WinDBG拿到NtOpenProcess的汇编指令,涉及指针操作的代码需要用到unsafe关键字,fixed关键字用来防止CLR的垃圾回收修改变量地址。当拿到memoryAddress之后我们就可以将其传递给委托使用。即通过Marshal.GetDelegateForFunctionPointer来将函数指针转为委托。

通过syscall实现NtCreateFile

在windbg中拿到的汇编指令如下

0:001> x ntdll!NtCreateFile00007ff8`50fad0b0 ntdll!NtCreateFile (NtCreateFile)0:001> u 00007ff8`50fad0b0ntdll!NtCreateFile:00007ff8`50fad0b0 4c8bd1          mov     r10,rcx00007ff8`50fad0b3 b855000000      mov     eax,55h00007ff8`50fad0b8 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],100007ff8`50fad0c0 7503            jne     ntdll!NtCreateFile+0x15 (00007ff8`50fad0c5)00007ff8`50fad0c2 0f05            syscall00007ff8`50fad0c4 c3              ret00007ff8`50fad0c5 cd2e            int     2Eh00007ff8`50fad0c7 c3              ret

首先看下api

__kernel_entry NTSTATUS NtCreateFile(  PHANDLE            FileHandle,  ACCESS_MASK        DesiredAccess,  POBJECT_ATTRIBUTES ObjectAttributes,  PIO_STATUS_BLOCK   IoStatusBlock,  PLARGE_INTEGER     AllocationSize,  ULONG              FileAttributes,  ULONG              ShareAccess,  ULONG              CreateDisposition,  ULONG              CreateOptions,  PVOID              EaBuffer,  ULONG              EaLength);

返回值是NTSTATUS一个结构体,ACCESS_MASK、OBJECT_ATTRIBUTES等都是结构体,那么需要先在自己代码中定义其结构体。在https://www.pinvoke.net/ 中可以查到函数及结构体的定义,并且给出了c#代码。

SharpSysCallNative.cs[1]中定义了所有用到的结构体和标识符。

然后定义了一个委托


使用C#进行直接系统调用syscall

先定义NtCreateFile的汇编指令字节数组

 static byte[] bNtCreateFile =        {                 0x4C, 0x8B, 0xD1,               // mov r10, rcx            0xB8, 0x55, 0x00, 0x00, 0x00,   // mov eax, 0x55 (NtCreateFile Syscall)            0x0F, 0x05,                     // syscall            0xC3                            // ret        };

接下来是对委托的实现


使用C#进行直接系统调用syscall

在实现中,拿到NtCreateFile的在内存中的地址,而在Windows安全模型中,内存需要分配合适的访问权限。通过windbg可以看到NtCreateFile的权限为PAGE_EXECUTE_READ

0:001> !address 00007ff8`50fad0b0

Mapping file section regions...Mapping module regions...Mapping PEB regions...Mapping TEB and stack regions...Mapping heap regions...Mapping page heap regions...Mapping other regions...Mapping stack trace database regions...Mapping activation context regions...
Usage: ImageBase Address: 00007ff8`50f11000End Address: 00007ff8`5102c000Region Size: 00000000`0011b000 ( 1.105 MB)State: 00001000 MEM_COMMITProtect: 00000020 PAGE_EXECUTE_READType: 01000000 MEM_IMAGEAllocation Base: 00007ff8`50f10000Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPYImage Path: C:WindowsSYSTEM32ntdll.dllModule Name: ntdllLoaded Image Name: C:WindowsSYSTEM32ntdll.dllMapped Image Name: More info: lmv m ntdllMore info: !lmi ntdllMore info: ln 0x7ff850fad0b0More info: !dh 0x7ff850f10000

而进程的地址是私有的,一个程序不能修改另一个程序的数据,所以要通过VritualProtect将权限设置为PAGE_EXECUTE_READWRITE。

接下来通过Marshal.GetDelegateForFunctionPointer将指针转化为委托,接下来将委托的执行结果返回。

在Program.cs中进行调用。

使用C#进行直接系统调用syscall
image.png


总结一下流程:

1.定义委托NtCreateFile,使用P/Invoke导入所需的结构、函数、标识符。2.对委托进行具体实现,在实现中拿到内存地址,通过指针转为委托,调用委托返回结果。

在ProcessMonitor中监视其堆栈确实是syscall直接系统调用。

使用C#进行直接系统调用syscall
image.png

思考

缺点:

1.c#的直接系统调用相比于非托管代码(如cpp)要麻烦的多2.c#反编译简单,更容易被分析3.受限于windows的系统版本,汇编代码不一样

优点:

1.反hook强

胡言乱语:当Marshal.GetDelegateForFunctionPointer被hook时岂不是无解?

参考

1.https://jhalon.github.io/utilizing-syscalls-in-csharp-1/2.https://jhalon.github.io/utilizing-syscalls-in-csharp-2/3.https://github.com/jhalon/SharpCall4.https://github.com/b4rtik/SharpMiniDump/5.https://github.com/FuzzySecurity/BlueHatIL-20206.https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/7.https://medium.com/@fsx30/bypass-edrs-memory-protection-introduction-to-hooking-2efb21acffd6

References

[1] SharpSysCallNative.cs: https://github.com/jhalon/SharpCall/blob/master/Native.cs


分享、点赞、看就是对我们的一种支持!

使用C#进行直接系统调用syscall


本文始发于微信公众号(ChaBug):使用C#进行直接系统调用syscall

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: