|
一、介绍
在进行 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 地址旁边的系统调用指令,在静态偏移未能找到系统调用指令的情况下进行了额外搜索。
- public static IntPtr SC_Address(IntPtr NtApiAddress)
- {
- IntPtr SyscallAddress;
- #if WIN64
- byte[] syscall_code =
- {
- 0x0f, 0x05, 0xc3
- };
-
- UInt32 distance_to_syscall = 0x12;
-
- #else
- byte[] syscall_code =
- {
- 0x0f, 0x34, 0xc3
- };
-
- UInt32 distance_to_syscall = 0xf;
- #endif
-
- // Start with common offset to syscall
- var tempSyscallAddress = NtApiAddress.ToInt64() + distance_to_syscall;
- SyscallAddress = (IntPtr) tempSyscallAddress;
- byte[] AddressData = new byte[3];
- Marshal.Copy(SyscallAddress, AddressData, 0, AddressData.Length);
- if (AddressData.SequenceEqual(syscall_code)){
- return SyscallAddress;
- }
-
- long searchLimit = 512;
- long regionSize = 0;
- long pageAddress = 0;
- long currentAddress = 0;
-
- // If syscall not found, search the closest one to the current NTDLL API address byte by byte
- PE.MEMORY_BASIC_INFORMATION mem_basic_info = new PE.MEMORY_BASIC_INFORMATION();
- if(Imports.VirtualQueryEx(Imports.GetCurrentProcess(), NtApiAddress, out mem_basic_info, (uint)Marshal.SizeOf(typeof(PE.MEMORY_BASIC_INFORMATION))) != 0)
- {
- regionSize = mem_basic_info.RegionSize.ToInt64();
- pageAddress = (long)mem_basic_info.BaseAddress;
- currentAddress = NtApiAddress.ToInt64();
- searchLimit = regionSize-(currentAddress-pageAddress)-syscall_code.Length+1;
- }
-
- for (int num_jumps = 1 ; num_jumps < searchLimit ; num_jumps++){
- tempSyscallAddress = NtApiAddress.ToInt64() + num_jumps;
- SyscallAddress = (IntPtr) tempSyscallAddress;
- AddressData = new byte[3];
- Marshal.Copy(SyscallAddress, AddressData, 0, AddressData.Length);
- if (AddressData.SequenceEqual(syscall_code)){
- return SyscallAddress;
- }
- }
- return IntPtr.Zero;
- }
复制代码
然后,在原始的 SYSCALL_ENTRY 对象中进行了编辑,使其具有附加属性来存储每个 NTDLL API 的系统调用指令的地址。
此外,添加了额外的检查 (iswow64()) 以确定它是否是 Windows 64 位 (WoW64),并跳过系统调用指令搜索以最小化负载的开销。
- public struct SYSCALL_ENTRY
- {
- public string Hash;
- public IntPtr Address;
- public IntPtr SyscallAddress;
- }
- ...
- #if !WIN64
- public static bool iswow64()
- {
- byte[] checkiswow64 =
- {
- 0x64, 0xA1, 0xC0, 0x00, 0x00, 0x00, // mov eax, fs:[0xc0]
- 0x85, 0xC0, // test eax, eax
- 0x75, 0x06, // jump if wow64
- 0xB8, 0x00, 0x00, 0x00, 0x00, // mov eax, 0
- 0xC3, // ret
- 0xB8, 0x01, 0x00, 0x00, 0x00, // mov eax, 1
- 0xC3 // ret
- };
- IntPtr iswow64 = SharpASM.callASM(checkiswow64);
- if (iswow64.ToInt64() == 1)
- return true;
- else
- return false;
- }
- #endif
- ...
- public static bool PopulateSyscallList(IntPtr moduleBase)
- {
- ...
- // Check if it is wow64
- bool wow64 = System.Environment.Is64BitOperatingSystem && !System.Environment.Is64BitProcess;
-
- // Check if is a syscall
- if (functionName.StartsWith("Zw"))
- {
- var functionOrdinal = Marshal.ReadInt16((IntPtr)(moduleBase.ToInt64() + ordinalsRva + i * 2)) + ordinalBase;
- var functionRva = Marshal.ReadInt32((IntPtr)(moduleBase.ToInt64() + functionsRva + 4 * (functionOrdinal - ordinalBase)));
- functionPtr = (IntPtr)((long)moduleBase + functionRva);
-
- Temp_Entry.Hash = HashSyscall(functionName);
- Temp_Entry.Address = functionPtr;
- #if WIN64
- Temp_Entry.SyscallAddress = SC_Address(functionPtr);
- #else
- // If wow64, skip syscall instruction search
- if (iswow64())
- Temp_Entry.SyscallAddress = IntPtr.Zero;
- else
- Temp_Entry.SyscallAddress = SC_Address(functionPtr);
- #endif
- // Add syscall to the list
- SyscallList.Add(Temp_Entry);
- }
- ...
- }
复制代码
填充系统调用列表后,每次我想执行系统调用时,都会使用 GetSyscallAddress 函数从系统调用条目列表中随机选择一个系统调用地址。
- public static IntPtr GetSyscallAddress(string FunctionHash)
- {
- var hModule = GetPebLdrModuleEntry("ntdll.dll");
- if (!PopulateSyscallList(hModule)) return IntPtr.Zero;
-
- Random rnd = new Random();
- DWORD index = rnd.Next() % SyscallList.Count;
- return SyscallList[index].SyscallAddress;
- }
复制代码
除了为间接系统调用填充系统调用地址列表的功能,还需要更新系统调用 STUB 程序集。
三、x64 中间接系统调用的系统调用 STUB
与 SysWhispers2/3 系统调用 STUB 实现不同,C# 版本的系统调用 STUB 不会调用汇编代码中的 getSyscallNumber 和 getSyscallAddress 函数。相反,这些函数将单独执行并在之后更新 STUB 模板。因此,由于堆栈没有改变,因此无需处理 CPU 寄存器。
更新后的系统调用 STUB 现在将随机生成的 NTDLL 系统调用地址分配给 R11,然后是跳转指令以实现间接系统调用。 x64 系统调用存根将如下所示:
- static byte[] newSyscallStub =
- {
- 0x4C, 0x8B, 0xD1, // mov r10, rcx
- 0xB8, 0x18, 0x00, 0x00, 0x00, // mov eax, syscall number
- 0x49, 0xBB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // movabs r11,syscall address
- 0x41, 0xFF, 0xE3 // jmp r11
- };
复制代码
四、x86 中间接系统调用的系统调用 STUB
对于 x86 系统调用 STUB,它会比 x64 稍微复杂一些,因为系统调用 STUB 需要更改以支持在 32 位操作系统和 64 位操作系统 (wow64) 上运行系统调用。
SharpWhispers 的原始系统调用 STUB (如下所示) 通过调用 fs:[C0] (KiFastSystemCall) 仅支持在 64 位操作系统中执行 x86。
- static byte[] originalSyscallStub =
- {
- 0x55, // push ebp
- 0x8B, 0xEC, // mov ebp,esp
- 0xB9, 0xAB, 0x00, 0x00, 0x00, // mov ecx,AB ; number of parameters
- // push_argument:
- 0x49, // dec ecx
- 0xFF, 0x74, 0x8D, 0x08, // push dword ptr ss:[ebp+ecx*4+8] ; parameter
- 0x75, 0xF9, // jne <x86syscallasm.push_argument>
- // ; push ret_address_epilog
- 0xE8, 0x00, 0x00, 0x00, 0x00, // call <x86syscallasm.get_eip> ; get eip with ret-pop
- 0x58, // pop eax
- 0x83, 0xC0, 0x15, // add eax,15 ; Push return address
- 0x50, // push eax
- 0xB8, 0xCD, 0x00, 0x00, 0x00, // mov eax,CD ; Syscall number
- // ; Get Address from TIB
- 0x64, 0xFF, 0x15, 0xC0, 0x00, 0x00, 0x00, // call dword ptr fs:[C0] ; call KiFastSystemCall
- 0x8D, 0x64, 0x24, 0x04, // lea esp,dword ptr ss:[esp+4]
- // ret_address_epilog:
- 0x8B, 0xE5, // mov esp,ebp
- 0x5D, // pop ebp
- 0xC3 // ret
- };
复制代码
但是,现有的 STUB 不支持 32 位操作系统,因为 32 位操作系统 Windows 执行系统调用的方式不同。因此将使用另一条名为 "sysenter" 的指令,这是一条类似于 "syscall" 的指令,用于执行 x86 系统调用。从 32 位操作系统中的 WinDBG 可以更轻松地检查该指令。
为了支持 32/64 位操作系统执行,系统调用 STUB 将被重新构造,以首先确定它是否是 wow64 并重定向到正确的指令以执行系统调用。
x86 系统调用 STUB 将分成几个部分。首先,系统调用号将分配给 EAX 寄存器。
- 0xB8, 0xFF, 0x00, 0x00, 0x00, // mov eax, syscall number
复制代码
参考 SysWhispers2,将进行测试以验证 fs:[C0] 是否存在,以确定操作系统的架构 (32/64 位)。
- 0x64, 0x8B, 0x0D, 0xC0, 0x00, 0x00, 0x00, // mov ecx, dword ptr fs:[C0]
- 0x85, 0xC9, // test ecx, ecx
- 0x75, 0x0f, // jne 18 <wow64>
复制代码
TEB 0xC0 偏移量将根据操作系统的体系结构显示不同的结果。
如果地址为零,则表示系统运行的是 32 位操作系统,它会跳转到 NTDLL 中调用 sysenter 指令的指令,实现间接 syscall。
在 sysenter 指令中,EDX 将是用户态返回地址。因此,为 EDX 分配正确的返回值 (即 ESP) 非常重要,以避免将执行返回到意外地址。
- 0xE8, 0x01, 0x00, 0x00, 0x00, // call 1
- 0xC3, // ret
- 0x89, 0xE2, // mov edx, esp
- 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, // mov edi, syscall address
- 0xFF, 0xE7, // jmp edi
复制代码
同时,如果是运行 x86 程序的 x64 系统,会发生跳转,下一条指令会调用 KiFastSystemCall 函数。
- // wow64
- 0x64, 0xFF, 0x15, 0xC0, 0x00, 0x00, 0x00, // call dword ptr fs:[C0] ; call KiFastSystemCall
- 0xC3 // ret
复制代码
上述步骤将生成以下系统调用 STUB,以支持 32/64 位操作系统的 x86 系统调用执行。
- static byte[] bSyscallStub =
- {
- // assign syscall number for later use
- 0xB8, 0xFF, 0x00, 0x00, 0x00, // mov eax, syscall number
-
- // validate the architecture of the operating system
- 0x64, 0x8B, 0x0D, 0xC0, 0x00, 0x00, 0x00, // mov ecx, dword ptr fs:[C0]
- 0x85, 0xC9, // test ecx, ecx
- 0x75, 0x0f, // jne 18 <wow64>
- 0xE8, 0x01, 0x00, 0x00, 0x00, // call 1
- 0xC3, // ret
-
- // x86 syscall for 32-bit OS
- 0x89, 0xE2, // mov edx, esp
- 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, // mov edi, syscall address
- 0xFF, 0xE7, // jmp edi
-
- // x64 syscall for 64-bit OS // wow64
- 0x64, 0xFF, 0x15, 0xC0, 0x00, 0x00, 0x00, // call dword ptr fs:[C0] ; call KiFastSystemCall
- 0xC3 // ret
- };
复制代码
通过在 SharpWispers 中包含上述代码,应该能够为 x64/x86 进程执行间接系统调用。
有了之前获得的系统调用号和系统调用地址,现在准备在 DynamicSyscallInvoke 函数中修改后的系统调用 STUB 模板的相应偏移量中替换它们。
- int syscallNumber = SyscallSolver.GetSyscallNumber(fHash);
- IntPtr syscallAddress = SyscallSolver.GetSyscallAddress(fHash);
-
- #if WIN64
- byte[] syscallNumberByte = BitConverter.GetBytes(syscallNumber);
- syscallNumberByte.CopyTo(bSyscallStub, 4);
- long syscallAddressLong = (long)syscallAddress;
- byte[] syscallAddressByte = BitConverter.GetBytes(syscallAddressLong);
- syscallAddressByte.CopyTo(bSyscallStub, 10);
- #else
- byte[] syscallNumberByte = BitConverter.GetBytes(syscallNumber);
- syscallNumberByte.CopyTo(bSyscallStub, 1);
- int syscallAddressInt = (int)syscallAddress;
- byte[] syscallAddressByte = BitConverter.GetBytes(syscallAddressInt);
- syscallAddressByte.CopyTo(bSyscallStub, 25);
- #endif
复制代码
将上述所有代码与 SharpWhispers 实现结合在一起,我们现在准备好拥有一个可以使用间接系统调用执行 NT API 的 C# 模板。
五、参考
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|