原文链接:https://blog.xpnsec.com/rundll32-your-dotnet/
如果大家一直在跟踪渗透测试方面的走向,可以看到现在主流后渗透工具已经从Powershell转移到.NET框架。 由于AMSI、CLM和ScriptBlock日志记录的存在,Powershell环境变得越来越严格,工具开发人员现在开始转而使用C#作为恶意软件和后渗透工具的开发语言。
在本文中,我想与大家分享一种实用技术,虽然这种技术对.NET开发人员来说并不新鲜,但可能有助于红方人员制作他们的拿手工具。具体一点,我想介绍如何导出DLL中的.NET静态方法,也就是说如何使用RunDLL32
来启动我们的.NET程序集(assembly)。
在攻击过程中使用的许多工具和技术都依赖于攻击者制作的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被成功加载。
通过前文对底层过程的分析,了解其工作原理后,我们可以做些什么使这个过程对开发人员更加友好? 如果我们只是想面对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的加载器,澄清这种技术为何能够行之有效。 如果大家已经完成了这个练习,想分享自己的成果,请随时与我联系。