用于后开发的.NET仍然存在。它已与大多数C2框架捆绑在一起,移植了通用工具,添加(然后绕过了)AMSI,并且使用很多妙招来启动非托管代码。但是,加载.NET程序集的过程似乎非常一致。
众所周知,像Cobalt Strike中 execute-assembly 这样的工具极大地提高了从内存中加载.NET程序集的可访问性,很多攻击者在Github上发布代码时都以或这或那的方式使用它。基于这种趋势,蓝队自然而然善于寻找遗留在内存中的文件。但是,作为红队,我们仍然发现,不管目标是托管还是非托管进程,在进程内启动.NET代码的方法似乎一成不变。例如,如果我们希望将代码注入到进程中,那么,即使目标已经是加载了CLR的.NET进程,我们通常采用的路径也是相同的:
这已经困扰了我好多年,所以我花了几个晚上研究可以更改签名的潜在方法。我的目标很简单,就是尝试找到一种在.NET进程中直接调用.NET方法的方法,不必费尽心思将Shellcode或rDLL注入非托管空间,而是另辟蹊径,通过CLR接口来加载. NET程序集。(作者写的这个rdll是个什么东东,老夫查了半天没查出来,见笑了……)
这篇文章将探讨实现此目标的一种潜在性,通过利用Windows公开的调试框架,我们可以看到使用调试API在目标进程中调用任意.NET代码所需要的内容。
ICorDebug简介正如我们大多数人在Visual Studio中所见,.NET公开了强大的调试功能,使人们能够在附加的进程中执行代码:
在我脑海中已初步构建一种在.NET进程中执行特定函数的简单方法,但是,有那么一种方法可以模拟此功能,使得.NET进程中的代码可以执行而不必加载shellcode和全部.NET程序集吗?我所希望的是某种类似 DebuggerEvaluateCSharpInThisprocess 的方法,但是,并不存在。。。又但是,我们可以利用一个文档齐全(尽管非常复杂)的API,然后以编程方式来利用.NET调试的功能。
ICorDebug 是.NET调试的切入点,并提供了很多函数,使我们可以控制.NET进程。让我们从设计一个简单的调试器开始,将其附加到我们选择的进程中,从而开始探索此API。
创建一个调试器我们要做的第一件事是新建一个ICorDebug实例。使用与当前.NET注入方法完全相同的调用,我们首先选择.NET框架的安装版本:
if ((hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID *)&metaHost)) != S_OK)
{
return hr;
}
if ((hr = metaHost->EnumerateInstalledRuntimes(&runtime)) != S_OK)
{
return hr;
}
frameworkName = (LPWSTR)LocalAlloc(LPTR, 2048);
if (frameworkName == NULL)
{
return E_OUTOFMEMORY;
}
while (runtime->Next(1, &enumRuntime, 0) == S_OK)
{
if (enumRuntime->QueryInterface<ICLRRuntimeInfo>(&runtimeInfo) == S_OK)
{
if (runtimeInfo != NULL)
{
runtimeInfo->GetVersionString(frameworkName, &bytes);
wprintf(L"[*] Supported Framework: %s\\n", frameworkName);
}
}
}
不同之处在于,一旦我们确定了要使用的运行时间后,就会初始化该 ICorDebug 接口实例;而不是像通常那样,从注入的DLL直接运行.NET代码,然后再请求一个 ICLRRuntimeHost 实例。此处的主要区别在于,我们把要执行的shellcode附加到另一个.NET进程,而不需要将之注入非托管空间再加载CLR那么麻烦。
我们使用以下内容来创建一个 ICorDebug 实例:
// Create our debugging interface
ICorDebug *debug;
ICorDebugProcess *process;
if ((hr = runtimeInfo->GetInterface(CLSID_CLRDebuggingLegacy, IID_ICorDebug, (LPVOID *)&debug)) != S_OK)
{
return hr;
}
// Initialise the debugger
debug->Initialize();
// Attach to an existing process by PID
debug->DebugActiveProcess(1234, false, &process);
现在我们已经初始化了接口,这里暂停一下喝杯茶,然后解释一下此调试框架实际上如何与我们的目标流程交互的。首先看看对所公开的各个组件进行高层次概述:
这乍看之下可能有点抽象(我第一次使用API时,阅读了无数遍文档才看懂),但是值得一提的是,调试器API将首先响应从目标进程触发的调试事件。例如,如果引发异常,例如将新程序集加载到目标中或创建了新线程,我们将收到一个事件。并且,每次触发事件时,我们都有机会与进入“stopped”状态的目标进行交互,然后才最终恢复执行并等待其他事件。
通常,当我们与调试的.NET进程进行交互时,该进程需要处于stopped状态。如果与正在运行的进程进行交互的话,就会报错如下: