最近看了很多关于syscall的文章,国外大多数安全研究员使用syscall来绕过edr的hook,使用的语言也五花八门,而我c系列的语言只会一点c#,所以我就用C#来简单实现一个syscall。
本文全文参考以下两篇文章,部分讲解的不如原文清楚,要详细了解的请移步:
- https://jhalon.github.io/utilizing-syscalls-in-csharp-1/
- https://jhalon.github.io/utilizing-syscalls-in-csharp-2/
- https://github.com/jhalon/SharpCall
什么是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的汇编指令为
mov r10,rcx
mov eax,55h
syscall
ret
在syscall指令下发后CPU会跳入内核模式,把函数调用参数从用户模式堆栈复制到内核模式堆栈,执行NtCreateFile的内核版本ZwCreateFile函数,完成后把返回值返回到用户模式,整个系统调用完成。
使用syscall
在cpp中只需要内联asm代码就行,比如我们想编写一个利用NtCreateFile syscall的程序,只需要内联其汇编代码。
mov r10,rcx
mov eax,55h
syscall
ret
而在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# 中的委托(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!NtCreateFile
00007ff8`50fad0b0 ntdll!NtCreateFile (NtCreateFile)
0:001> u 00007ff8`50fad0b0
ntdll!NtCreateFile:
00007ff8`50fad0b0 4c8bd1 mov r10,rcx
00007ff8`50fad0b3 b855000000 mov eax,55h
00007ff8`50fad0b8 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ff8`50fad0c0 7503 jne ntdll!NtCreateFile+0x15 (00007ff8`50fad0c5)
00007ff8`50fad0c2 0f05 syscall
00007ff8`50fad0c4 c3 ret
00007ff8`50fad0c5 cd2e int 2Eh
00007ff8`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#代码。
在SharpSysCall\Native.cs中定义了所有用到的结构体和标识符。
然后定义了一个委托
先定义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
};
接下来是对委托的实现
在实现中,拿到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: Image
Base Address: 00007ff8`50f11000
End Address: 00007ff8`5102c000
Region Size: 00000000`0011b000 ( 1.105 MB)
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
Type: 01000000 MEM_IMAGE
Allocation Base: 00007ff8`50f10000
Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY
Image Path: C:\Windows\SYSTEM32\ntdll.dll
Module Name: ntdll
Loaded Image Name: C:\Windows\SYSTEM32\ntdll.dll
Mapped Image Name:
More info: lmv m ntdll
More info: !lmi ntdll
More info: ln 0x7ff850fad0b0
More info: !dh 0x7ff850f10000
而进程的地址是私有的,一个程序不能修改另一个程序的数据,所以要通过VritualProtect将权限设置为PAGE_EXECUTE_READWRITE。
接下来通过Marshal.GetDelegateForFunctionPointer将指针转化为委托,接下来将委托的执行结果返回。
总结一下流程:
- 定义委托NtCreateFile,使用P/Invoke导入所需的结构、函数、标识符。
- 对委托进行具体实现,在实现中拿到内存地址,通过指针转为委托,调用委托返回结果。
在ProcessMonitor中监视其堆栈确实是syscall直接系统调用。
思考
缺点:
- c#的直接系统调用相比于非托管代码(如cpp)要麻烦的多
- c#反编译简单,更容易被分析
- 受限于windows的系统版本,汇编代码不一样
优点:
- 反hook强
胡言乱语:当Marshal.GetDelegateForFunctionPointer被hook时岂不是无解?
参考
- https://jhalon.github.io/utilizing-syscalls-in-csharp-1/
- https://jhalon.github.io/utilizing-syscalls-in-csharp-2/
- https://github.com/jhalon/SharpCall
- https://github.com/b4rtik/SharpMiniDump/
- https://github.com/FuzzySecurity/BlueHatIL-2020
- https://outflank.nl/blog/2019/06/19/red-team-tactics-combining-direct-system-calls-and-srdi-to-bypass-av-edr/
- https://medium.com/@fsx30/bypass-edrs-memory-protection-introduction-to-hooking-2efb21acffd6