如何利用RunDLL32调用.NET Assembly

原文链接:https://blog.xpnsec.com/rundll32-your-dotnet/

0x00 简介

如果大家一直在跟踪渗透测试方面的走向,可以看到现在主流后渗透工具已经从Powershell转移到.NET框架。 由于AMSI、CLM和ScriptBlock日志记录的存在,Powershell环境变得越来越严格,工具开发人员现在开始转而使用C#作为恶意软件和后渗透工具的开发语言。

在本文中,我想与大家分享一种实用技术,虽然这种技术对.NET开发人员来说并不新鲜,但可能有助于红方人员制作他们的拿手工具。具体一点,我想介绍如何导出DLL中的.NET静态方法,也就是说如何使用RunDLL32来启动我们的.NET程序集(assembly)。

0x01 托管DLL导出:繁琐版

在攻击过程中使用的许多工具和技术都依赖于攻击者制作的DLL。 如果查看Oddvar的LOLBAS项目,可知我们可以利用许多方法来执行任意DLL,由于启用DLL规则后,AppLocker会警告用户系统性能会有所下降,因此我们经常看到这些工具能够在软件限制策略下隐蔽执行。

在.NET环境中,通常我们遇到的是类assembly,而不是许多工具能够直接使用的DLL。 .NET类assembly无法提供类似RunDLL32等工具所需的导出表信息,以便这些工具来引用其内部方法。 但事实证明,我们有办法能够让兼顾两者,兼得鱼和熊掌。这里我们可以使用.export

首先我们来看一段简单的C#代码,该代码用来弹出一个消息框:

namespace Test
{
   public class TestClass
   {
       public static void TestMethod()
       {
           MessageBox.Show(".NET Assembly Running");
       }
   }
}

将其编译为.NET assembly后,我们可以使用Microsoft的ildasm工具来查看构成可执行文件的中间语言:

这个工具还提供了一些命令行选项,可以将我们的代码反汇编成直观的.il文件:

ildasm.exe /out:TestUnmanaged.il TestUnmanaged.dll

现在我们有了与C#对应的中间语言,我们可以使用.export描述符标记静态方法,该描述符可以告诉汇编器在创建的DLL中创建导出信息,例如:

.class public auto ansi beforefieldinit Test.TestClass
       extends [mscorlib]System.Object
{

  .method public hidebysig static void  TestMethod() cil managed
  {
    .export [1]   // <--- Magic export descriptor

    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldstr      ".NET Assembly Running"
    IL_0006:  call       valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
    IL_000b:  pop
    IL_000c:  ret
  } // end of method TestClass::TestMethod

这里我们导出了TestMethod方法,序号为1。该步骤完成后,我们需要使用以下命令将ilasm重新编译为.NET类assembly:

ilasm.exe TestUnmanaged.il /DLL /output=TestUnamanged.dll

现在如果分析我们生成的DLL文件,观察其导出表,我们可以看到如下信息:

如果尝试使用RunDll32.exe调用该DLL,结果如下所示:

此外我们还可以看到,在调用我们的静态方法时,CLR会被加载到进程中,这样我们就能在之前的非托管进程中使用.NET框架的全部功能:

这样Windows加载器已经帮我们做了许多繁杂的工作,现在我们有一种很好的方法可以将CLR注入非托管进程,这有点类似于Cobalt Strike的execute-assembly所使用的传统COM方法(请参阅我之前关于绕过AppLocker的一篇文章,介绍了这种COM方法的工作原理)。

但这种技术真的拓宽了RunDLL32的使用场景吗?我们来构造一个非常简单的DLL加载器,使用C语言编写,用来加载我们的.NET DLL:

#include <Windows.h>

typedef void(*TestMethod)();

int main()
{
    HMODULE managedDLL = LoadLibraryA("TestUnmanaged.dll");
    TestMethod managedMethod = (TestMethod)GetProcAddress(managedDLL, "TestMethod");
    managedMethod();
}

在程序执行时,我们可以看到clr.dll再次被加载到我们的非托管进程中:

并且我们的.NET方法也能正常执行:

显然,这意味着我们可以将.NET assembly远程注入流程(例如使用Win32 CreateRemoteThread调用来实现)。 现在让我们选择默认情况下没有加载.NET CLR的目标,如iexplore.exe

如果我们观察已加载的DLL,还是可以看到我们的DLL被成功加载。

0x02 托管DLL导出:快速版

通过前文对底层过程的分析,了解其工作原理后,我们可以做些什么使这个过程对开发人员更加友好? 如果我们只是想面对C#,不想涉及ilasm,那么这个是用我们可以使用DllExport

DllExport是Denis Kuzmin开发的一个项目(也就是3F),大家可以访问Github页面下载该项目。 开发人员的体验可能没那么好,这主要是因为该项目提供了修改项目以支持具体的一个管理器(Manager),但一旦我们知道该工具的具体工作原理,那么就可以轻松在Visual Studio中复现相关方法。

首先,我们使用正常步骤,通过NuGet包来安装DllExport,可以看到如下对话框:

此时,系统会提示我们在继续前进之前删除NuGET包。 删除后,我们可以在项目中看到名为DllExport_Configure.bat的一个.bat文件,该文件用来打开.NET DLLExport Manager,提供了许多选项来配置我们的项目,导出非托管DLL:

完成该过程后,现在我们可以访问[DllExport]属性。 这里我们使用前面的示例代码,然后可以使用以下方法导出静态方法:

namespace Test
{
   public class TestClass
   {
       [DllExport]
       public static void TestMethod()
       {
           MessageBox.Show(".NET Assembly Running");
       }
   }
}

然后编译我们的项目:

现在我们已经有一个很不错的方法,可以在我们的类assembly中添加导出函数,接下来让我们看看这种方法是否适用于更成熟的东西,比如harmj0y的SafetyCatz工具。 添加DllExport NuGet之后,我们需要稍微调整源代码才能使其正常工作。

首先,我们需要修改static void Main(string[] args)的入口点,改成我们DLL的导出函数RunSafetyCatz,如下所示:

[DllExport]
    static void RunSafetyCatz()
    {
        string[] args;

        if (!IsHighIntegrity())
        {
        Console.WriteLine("\n[X] Not in high integrity, unable to grab a handle to lsass!\n");
        }
        ...

完成该步骤后,我们需要将输出类型更新为Class Library

现在,可以使用RunDLL32.exe来测试以DLL方式调用SafetyCatz

如果大家希望控制台的输出结果如上图所示,就需要使用Win32的AttachConsole(-1)调用,这部分工作留给大家来完成。

现在我们已经拥有了在非托管进程中加载.NET assembly的一种好方法。 如果时间充裕,我想更深入了解Windows的加载器,澄清这种技术为何能够行之有效。 如果大家已经完成了这个练习,想分享自己的成果,请随时与我联系。

源链接

Hacking more

...