吉沃运营专员 发表于 2022-9-16 15:12:52

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]
查看完整版本: C# 中的系统间接调用