DecoyMini 技术交流社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 4020|回复: 0

[技术前沿] C# 中的系统间接调用

[复制链接]

188

主题

35

回帖

30

荣誉

Rank: 9Rank: 9Rank: 9

UID
2
积分
354
精华
1
沃币
2 枚
注册时间
2021-6-24

论坛管理

发表于 2022-9-16 15:12:52 | 显示全部楼层 |阅读模式

一、介绍


在进行 C# tradecraft 开发时,想知道 C# 中是否有类似 SysWhispers 的实现,我从 SECFORCE 找到了一个名为 SharpWhispers 的优秀项目。通过提供函数 (SharpASM) 来执行汇编并重新实现 "按系统调用地址排序" 方式来查找系统调用号 (SSN),这让我的生活变得更加轻松,就像 SysWhispers2 所做的那样。

随着越来越多地使用直接系统调用作为针对 EDR API 挂钩的规避技术,开发了一些检测策略,例如 "系统调用标记" 签名和执行源自 NTDLL 外部的系统调用指令,以识别静态和动态中的异常系统调用使用。

因此,KlezVirus 实施了另一个名为 SysWhispers3 的解决方案来演示间接系统调用技术,该技术可用于绕过上述检测策略。

通过实现间接系统调用,可以享受以下好处:

  • 避免在 payload 中包含系统调用指令
  • 确保系统调用执行始终源自合法的 NTDLL

为了给加载器提供更好的规避能力,我开始在 C# 中实现基于 SharpWhispers 和 SysWhispers3 的间接系统调用,这篇文章将记录我为实现它所做的关键步骤。

二、用 C# 实现间接系统调用


间接系统调用技术旨在将原始系统调用指令替换为指向 NTDLL 的内存地址的跳转指令,该地址存储系统调用指令。

例如,每个 NTDLL API (即 NtAllocateVirtualMemory) 的偏移量 0x12 通常是如下所示的 syscall 指令:



要获取每个 NTDLL API 的 syscall 地址,可以遍历当前进程中加载的 NTDLL,获取每个 NTDLL 导出函数的地址,并分别计算偏移量 0x12 和 0x0f,得到指向 syscall/sysenter 的地址 (相当于syscall 在 32 位操作系统中) 指令。

最初的 SharpWhispers 已经完成了定位导出表目录和每个 NTDLL API 函数的相对虚拟地址的困难部分。我的部分将尝试重新实现与 SysWhispers3 在 CSharp 中所做的类似功能,以获取每个 NTDLL API 的系统调用指令的地址。

最初的 SysWhisper3 实现使用固定偏移量来计算系统调用。但是,如果安装了 EDR 挂钩并且在 他的博客文章 中提到,则可能无法找到系统调用指令。
为确保始终找到系统调用指令,我通过逐字节搜索 NTDLL API 地址旁边的系统调用指令,在静态偏移未能找到系统调用指令的情况下进行了额外搜索。

  1. public static IntPtr SC_Address(IntPtr NtApiAddress)
  2. {
  3.         IntPtr SyscallAddress;

  4. #if WIN64
  5.         byte[] syscall_code =
  6.         {
  7.                 0x0f, 0x05, 0xc3
  8.         };
  9.        
  10.         UInt32 distance_to_syscall = 0x12;
  11.        
  12. #else
  13.         byte[] syscall_code =
  14.         {
  15.                 0x0f, 0x34, 0xc3
  16.         };
  17.        
  18.         UInt32 distance_to_syscall = 0xf;
  19. #endif
  20.        
  21.         // Start with common offset to syscall
  22.         var tempSyscallAddress = NtApiAddress.ToInt64() + distance_to_syscall;
  23.         SyscallAddress = (IntPtr) tempSyscallAddress;
  24.         byte[] AddressData = new byte[3];
  25.         Marshal.Copy(SyscallAddress, AddressData, 0, AddressData.Length);
  26.         if (AddressData.SequenceEqual(syscall_code)){
  27.                 return SyscallAddress;
  28.         }
  29.        
  30.         long searchLimit = 512;
  31.         long regionSize = 0;
  32.         long pageAddress = 0;
  33.         long currentAddress = 0;
  34.        
  35.         // If syscall not found, search the closest one to the current NTDLL API address byte by byte
  36.         PE.MEMORY_BASIC_INFORMATION mem_basic_info = new PE.MEMORY_BASIC_INFORMATION();
  37.         if(Imports.VirtualQueryEx(Imports.GetCurrentProcess(), NtApiAddress, out mem_basic_info, (uint)Marshal.SizeOf(typeof(PE.MEMORY_BASIC_INFORMATION))) != 0)
  38.         {
  39.                 regionSize = mem_basic_info.RegionSize.ToInt64();
  40.                 pageAddress = (long)mem_basic_info.BaseAddress;
  41.                 currentAddress = NtApiAddress.ToInt64();
  42.                 searchLimit = regionSize-(currentAddress-pageAddress)-syscall_code.Length+1;
  43.         }
  44.        
  45.         for (int num_jumps = 1 ; num_jumps < searchLimit ; num_jumps++){
  46.                 tempSyscallAddress = NtApiAddress.ToInt64() + num_jumps;
  47.                 SyscallAddress = (IntPtr) tempSyscallAddress;
  48.                 AddressData = new byte[3];
  49.                 Marshal.Copy(SyscallAddress, AddressData, 0, AddressData.Length);
  50.                 if (AddressData.SequenceEqual(syscall_code)){
  51.                         return SyscallAddress;
  52.                 }
  53.         }
  54.         return IntPtr.Zero;
  55. }
复制代码

然后,在原始的 SYSCALL_ENTRY 对象中进行了编辑,使其具有附加属性来存储每个 NTDLL API 的系统调用指令的地址。

此外,添加了额外的检查 (iswow64()) 以确定它是否是 Windows 64 位 (WoW64),并跳过系统调用指令搜索以最小化负载的开销。

  1. public struct SYSCALL_ENTRY
  2.         {
  3.                 public string Hash;
  4.                 public IntPtr Address;
  5.                 public IntPtr SyscallAddress;
  6.         }
  7. ...
  8. #if !WIN64
  9.         public static bool iswow64()
  10.         {
  11.                 byte[] checkiswow64 =
  12.                 {
  13.                         0x64, 0xA1, 0xC0, 0x00, 0x00, 0x00,                // mov eax, fs:[0xc0]
  14.                         0x85, 0xC0,                                                         // test eax, eax
  15.                         0x75, 0x06,                                                                // jump if wow64
  16.                         0xB8, 0x00, 0x00, 0x00, 0x00,                         // mov eax, 0
  17.                         0xC3,                                                                        // ret
  18.                         0xB8, 0x01, 0x00, 0x00, 0x00,                         // mov eax, 1
  19.                         0xC3                                                                        // ret
  20.                 };
  21.                 IntPtr iswow64 = SharpASM.callASM(checkiswow64);
  22.                 if (iswow64.ToInt64() == 1)
  23.                         return true;
  24.                 else
  25.                         return false;
  26.         }
  27. #endif
  28. ...
  29. public static bool PopulateSyscallList(IntPtr moduleBase)
  30. {
  31. ...
  32.         // Check if it is wow64
  33.         bool wow64 = System.Environment.Is64BitOperatingSystem && !System.Environment.Is64BitProcess;
  34.                        
  35.         // Check if is a syscall
  36.         if (functionName.StartsWith("Zw"))
  37.         {
  38.                 var functionOrdinal = Marshal.ReadInt16((IntPtr)(moduleBase.ToInt64() + ordinalsRva + i * 2)) + ordinalBase;
  39.                 var functionRva = Marshal.ReadInt32((IntPtr)(moduleBase.ToInt64() + functionsRva + 4 * (functionOrdinal - ordinalBase)));
  40.                 functionPtr = (IntPtr)((long)moduleBase + functionRva);
  41.        
  42.                 Temp_Entry.Hash = HashSyscall(functionName);
  43.                 Temp_Entry.Address = functionPtr;
  44. #if WIN64
  45.                 Temp_Entry.SyscallAddress = SC_Address(functionPtr);
  46. #else
  47.                 // If wow64, skip syscall instruction search
  48.                 if (iswow64())
  49.                         Temp_Entry.SyscallAddress = IntPtr.Zero;
  50.                 else
  51.                         Temp_Entry.SyscallAddress = SC_Address(functionPtr);
  52. #endif
  53.                 // Add syscall to the list
  54.                 SyscallList.Add(Temp_Entry);
  55.         }
  56. ...
  57. }
复制代码

填充系统调用列表后,每次我想执行系统调用时,都会使用 GetSyscallAddress 函数从系统调用条目列表中随机选择一个系统调用地址。

  1. public static IntPtr GetSyscallAddress(string FunctionHash)
  2. {
  3.         var hModule = GetPebLdrModuleEntry("ntdll.dll");

  4.         if (!PopulateSyscallList(hModule)) return IntPtr.Zero;
  5.        
  6.         Random rnd = new Random();
  7.         DWORD index = rnd.Next() % SyscallList.Count;
  8.         return SyscallList[index].SyscallAddress;
  9. }
复制代码

除了为间接系统调用填充系统调用地址列表的功能,还需要更新系统调用 STUB 程序集。

三、x64 中间接系统调用的系统调用 STUB


与 SysWhispers2/3 系统调用 STUB 实现不同,C# 版本的系统调用 STUB 不会调用汇编代码中的 getSyscallNumber 和 getSyscallAddress 函数。相反,这些函数将单独执行并在之后更新 STUB 模板。因此,由于堆栈没有改变,因此无需处理 CPU 寄存器。

更新后的系统调用 STUB 现在将随机生成的 NTDLL 系统调用地址分配给 R11,然后是跳转指令以实现间接系统调用。 x64 系统调用存根将如下所示:

  1. static byte[] newSyscallStub =
  2. {
  3.         0x4C, 0x8B, 0xD1,                                           // mov r10, rcx
  4.         0xB8, 0x18, 0x00, 0x00, 0x00,                                      // mov eax, syscall number
  5.         0x49, 0xBB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // movabs r11,syscall address
  6.         0x41, 0xFF, 0xE3                                                    // jmp r11
  7. };
复制代码

四、x86 中间接系统调用的系统调用 STUB


对于 x86 系统调用 STUB,它会比 x64 稍微复杂一些,因为系统调用 STUB 需要更改以支持在 32 位操作系统和 64 位操作系统 (wow64) 上运行系统调用。

SharpWhispers 的原始系统调用 STUB (如下所示) 通过调用 fs:[C0] (KiFastSystemCall) 仅支持在 64 位操作系统中执行 x86。

  1. static byte[] originalSyscallStub =
  2. {
  3.             0x55,                                       // push ebp
  4.             0x8B, 0xEC,                                 // mov ebp,esp
  5.             0xB9, 0xAB, 0x00, 0x00, 0x00,               // mov ecx,AB   ; number of parameters
  6.                                                         // push_argument:
  7.             0x49,                                       // dec ecx
  8.             0xFF, 0x74, 0x8D, 0x08,                     // push dword ptr ss:[ebp+ecx*4+8] ; parameter
  9.             0x75, 0xF9,                                 // jne <x86syscallasm.push_argument>
  10.                                                         // ; push ret_address_epilog
  11.             0xE8, 0x00, 0x00, 0x00, 0x00,               // call <x86syscallasm.get_eip> ; get eip with ret-pop
  12.             0x58,                                       // pop eax
  13.             0x83, 0xC0, 0x15,                           // add eax,15   ; Push return address
  14.             0x50,                                       // push eax  
  15.             0xB8, 0xCD, 0x00, 0x00, 0x00,               // mov eax,CD ; Syscall number
  16.                                                         // ; Get Address from TIB
  17.             0x64, 0xFF, 0x15, 0xC0, 0x00, 0x00, 0x00,   // call dword ptr fs:[C0] ; call KiFastSystemCall
  18.             0x8D, 0x64, 0x24, 0x04,                     // lea esp,dword ptr ss:[esp+4]
  19.                                                         // ret_address_epilog:
  20.             0x8B, 0xE5,                                 // mov esp,ebp
  21.             0x5D,                                       // pop ebp
  22.             0xC3                                        // ret
  23. };
复制代码

但是,现有的 STUB 不支持 32 位操作系统,因为 32 位操作系统 Windows 执行系统调用的方式不同。因此将使用另一条名为 "sysenter" 的指令,这是一条类似于 "syscall" 的指令,用于执行 x86 系统调用。从 32 位操作系统中的 WinDBG 可以更轻松地检查该指令。



为了支持 32/64 位操作系统执行,系统调用 STUB 将被重新构造,以首先确定它是否是 wow64 并重定向到正确的指令以执行系统调用。

x86 系统调用 STUB 将分成几个部分。首先,系统调用号将分配给 EAX 寄存器。

  1. 0xB8, 0xFF, 0x00, 0x00, 0x00,                        // mov eax, syscall number
复制代码

参考 SysWhispers2,将进行测试以验证 fs:[C0] 是否存在,以确定操作系统的架构 (32/64 位)。

  1. 0x64, 0x8B, 0x0D, 0xC0, 0x00, 0x00, 0x00,         // mov ecx, dword ptr fs:[C0]
  2. 0x85, 0xC9,                                        // test ecx, ecx
  3. 0x75, 0x0f,                                        // jne 18 <wow64>
复制代码

TEB 0xC0 偏移量将根据操作系统的体系结构显示不同的结果。







如果地址为零,则表示系统运行的是 32 位操作系统,它会跳转到 NTDLL 中调用 sysenter 指令的指令,实现间接 syscall。

在 sysenter 指令中,EDX 将是用户态返回地址。因此,为 EDX 分配正确的返回值 (即 ESP) 非常重要,以避免将执行返回到意外地址。

  1. 0xE8, 0x01, 0x00, 0x00, 0x00,                // call 1
  2. 0xC3,                                        // ret
  3. 0x89, 0xE2,                                // mov edx, esp
  4. 0xBF, 0xFF, 0xFF, 0xFF, 0xFF,                 // mov edi, syscall address
  5. 0xFF, 0xE7,                                // jmp edi
复制代码

同时,如果是运行 x86 程序的 x64 系统,会发生跳转,下一条指令会调用 KiFastSystemCall 函数。

  1.                                                                                                                                         // wow64
  2. 0x64, 0xFF, 0x15, 0xC0, 0x00, 0x00, 0x00,   // call dword ptr fs:[C0] ; call KiFastSystemCall
  3. 0xC3                                             // ret
复制代码

上述步骤将生成以下系统调用 STUB,以支持 32/64 位操作系统的 x86 系统调用执行。

  1. static byte[] bSyscallStub =
  2. {
  3.         // assign syscall number for later use
  4.         0xB8, 0xFF, 0x00, 0x00, 0x00,                        // mov eax, syscall number
  5.        
  6.         // validate the architecture of the operating system
  7.         0x64, 0x8B, 0x0D, 0xC0, 0x00, 0x00, 0x00,         // mov ecx, dword ptr fs:[C0]
  8.         0x85, 0xC9,                                        // test ecx, ecx
  9.         0x75, 0x0f,                                        // jne 18 <wow64>
  10.         0xE8, 0x01, 0x00, 0x00, 0x00,                        // call 1
  11.         0xC3,                                                // ret
  12.        
  13.         // x86 syscall for 32-bit OS
  14.         0x89, 0xE2,                                        // mov edx, esp
  15.         0xBF, 0xFF, 0xFF, 0xFF, 0xFF,                         // mov edi, syscall address
  16.         0xFF, 0xE7,                                        // jmp edi
  17.        
  18.         // x64 syscall for 64-bit OS                                                                        // wow64
  19.         0x64, 0xFF, 0x15, 0xC0, 0x00, 0x00, 0x00,           // call dword ptr fs:[C0] ; call KiFastSystemCall
  20.         0xC3                                                // ret
  21. };
复制代码

通过在 SharpWispers 中包含上述代码,应该能够为 x64/x86 进程执行间接系统调用。

有了之前获得的系统调用号和系统调用地址,现在准备在 DynamicSyscallInvoke 函数中修改后的系统调用 STUB 模板的相应偏移量中替换它们。

  1. int syscallNumber = SyscallSolver.GetSyscallNumber(fHash);
  2. IntPtr syscallAddress = SyscallSolver.GetSyscallAddress(fHash);
  3.        
  4. #if WIN64
  5.         byte[] syscallNumberByte = BitConverter.GetBytes(syscallNumber);
  6.         syscallNumberByte.CopyTo(bSyscallStub, 4);
  7.         long syscallAddressLong = (long)syscallAddress;
  8.         byte[] syscallAddressByte = BitConverter.GetBytes(syscallAddressLong);
  9.         syscallAddressByte.CopyTo(bSyscallStub, 10);
  10. #else
  11.         byte[] syscallNumberByte = BitConverter.GetBytes(syscallNumber);
  12.         syscallNumberByte.CopyTo(bSyscallStub, 1);
  13.         int syscallAddressInt = (int)syscallAddress;
  14.         byte[] syscallAddressByte = BitConverter.GetBytes(syscallAddressInt);
  15.         syscallAddressByte.CopyTo(bSyscallStub, 25);
  16. #endif
复制代码

将上述所有代码与 SharpWhispers 实现结合在一起,我们现在准备好拥有一个可以使用间接系统调用执行 NT API 的 C# 模板。

五、参考



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|小黑屋|DecoyMini 技术交流社区 (吉沃科技) ( 京ICP备2021005070号 )

GMT+8, 2024-12-22 14:50 , Processed in 0.062740 second(s), 26 queries .

Powered by Discuz! X3.4

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表