Building a (slightly) better Melkor

Melkor is a C# POC written by FuzzySec to simulate a TTP employed by InvisiMole. The concept is that post-ex assemblies are loaded into a payload/implant and kept encrypted using DPAPI whilst at rest. They are decrypted on demand and executed in a separate AppDomain. The AppDomain is unloaded once execution completes and only the encrypted assembly remains in memory ready to be used again. As a fan of .NET tradecraft, I wanted to try it out. However, I found that the results were not as promising as I’d hoped and I was still able to find multiple instance of plaintext indictors in memory.
Melkor 是由 FuzzySec 编写的 C# POC,用于模拟 InvisiMele 采用的 TTP。其概念是将 post-ex 组件加载到有效负载/植入物中,并在静止时使用 DPAPI 进行加密。它们按需解密并在单独的应用程序域中执行。执行完成后,将卸载 AppDomain,只有加密的程序集保留在内存中,可供再次使用。作为.NET tradecraft的粉丝,我想尝试一下。但是,我发现结果并不像我希望的那样有希望,我仍然能够在内存中找到多个明文指示器实例。

The test assembly does nothing more than pop a message box, but the string gives us something to search for as an indicator.

public static void DoTheThing()
    MessageBox.Show("Morgoth Bauglir is my name..");

To set the scene, when running Melkor with this assembly, 8 instances of the string “Morgoth Bauglir” remained in memory. The goal of the post is to reduce this count as much as possible. I will through instances that I was able to address and which ones still need a solution.
为了设置场景,当使用此程序集运行 Melkor 时,字符串“Morgoth Bauglir”的 8 个实例保留在内存中。该帖子的目标是尽可能减少此计数。我将通过我能够解决的实例以及哪些仍然需要解决方案的实例。

tl;dr – fuck the garbage collector.
博士 – 去他妈的垃圾收集器。

Building a (slightly) better Melkor

Melkor starts off by reading the assembly from disk and immediately uses DPAPI to encrypt it.
Melkor 首先从磁盘读取程序集,并立即使用 DPAPI 对其进行加密。

// Encrypt module
Console.WriteLine("[>] Reading assembly as Byte[]");
Byte[] bMod = File.ReadAllBytes(@"C:\Tools\Melkor\DemoModule\bin\Release\DemoModule.dll");
Console.WriteLine("[>] DPAPI CryptProtectData -> assembly[]");
hMelkor.DPAPI_MODULE dpMod = hMelkor.dpapiEncryptModule(bMod, "Melkor", 0);
if (dpMod.pMod != IntPtr.Zero)
    Console.WriteLine("    |_ Success");
    Console.WriteLine("    |_ pCrypto : 0x" + String.Format("{0:X}", (dpMod.pMod).ToInt64()));
    Console.WriteLine("    |_ iSize   : " + dpMod.iModSize);
    bMod = null;
    Console.WriteLine("\n[!] Failed to DPAPI encrypt module..");

Console.WriteLine("\n[?] Press enter to continue..");
[>] Reading assembly as Byte[]
[>] DPAPI CryptProtectData -> assembly[]
    |_ Success
    |_ pCrypto : 0x22241BC8E30
    |_ iSize   : 4850

[?] Press enter to continue..

Straight off the bat, we can see two instances of “Morgoth Bauglir” in Process Hacker after the encryption has taken place.
直接开始,我们可以在加密发生后在Process Hacker中看到两个“Morgoth Bauglir”实例。

Building a (slightly) better Melkor

Both of these are due to “oversights(?)” in Melkor. The first is that bMod = null; (line 12 above) removes the reference to the original byte[] but it does not de-allocate the underlying memory, at least not immediately. Dynamic memory allocations such as these are cleaned up by the CLR’s garbage collector (GC) when there are no more references pointing to it. The complication is that since GC runs are expensive, it doesn’t bother doing it right away and it can be an indeterminate length of time until it’s actually performed. An overarching lesson I learned with this project is that you cannot rely on the GC at all when you have these OPSEC concerns in mind.
这两者都是由于梅尔寇的“疏忽(?)”。第一个是 bMod = null; (上面的第 12 行)删除了对原始 byte[] 内存的引用,但它不会取消分配底层内存,至少不会立即取消分配。诸如此类的动态内存分配由 CLR 的垃圾回收器 (GC) 清理,当不再有指向它的引用时。复杂的是,由于 GC 运行成本高昂,因此它不会立即进行,并且在实际执行之前可能是一个不确定的时间长度。我从这个项目中学到的一个总体教训是,当你考虑到这些 OPSEC 问题时,你根本不能依赖 GC。

I did play around with calling GC.Collect() and GC.WaitForPendingFinalizers() but that didn’t make a difference. I assume there are some other conditions that are preventing this being cleaned (e.g. code execution still in the same method). In this case, I believe a better approach is to clear the array manually, for which there are a couple of approaches.
我确实玩过打电话 GC.Collect() , GC.WaitForPendingFinalizers() 但这并没有区别。我假设还有其他一些条件阻止了它被清除(例如,代码执行仍然在同一方法中)。在这种情况下,我认为更好的方法是手动清除数组,有几种方法。

One is to walk over the array and zero out each element.

for (var i = 0; i < bMod.Length; i++)
    bMod[i] = 0x00;

Another is to call the Array.Clear method.
另一种是调用该方法 Array.Clear 。

Array.Clear(bMod, 0, bMod.Length);

Testing after this change confirms that we’re down to one result.

Building a (slightly) better Melkor

This second allocation is left over by the dpapiEncryptModule method.
dpapiEncryptModule 这第二个分配由该方法剩余。


    DATA_BLOB oPlainText = makeBlob(bMod);
    DATA_BLOB oCipherText = new DATA_BLOB();
    DATA_BLOB oEntropy = makeBlob(bEntropy);

    Boolean bStatus = CryptProtectData(ref oPlainText, sModName, ref oEntropy, IntPtr.Zero,
        IntPtr.Zero, CRYPTPROTECT_LOCAL_MACHINE, ref oCipherText);

    if (bStatus)
        dpMod.sModName = sModName;
        dpMod.iModVersion = iModVersion;
        dpMod.iModSize = oCipherText.cbData;
        dpMod.pMod = oCipherText.pbData;

    return dpMod;

Amongst other arguments, CryptProtectData takes one DATA_BLOB structure containing the original plaintext content and another to hold the encrypted content. The structure looks like this:
在其他参数中,采用一种 DATA_BLOB 包含原始明文内容的结构, CryptProtectData 另一种结构用于保存加密内容。结构如下所示:

internal struct DATA_BLOB
    public int cbData;
    public IntPtr pbData;

The oPlainText blob (line 4 above) is created using another method called makeBlob.
Blob oPlainText (上面的第 4 行)是使用另一种名为 makeBlob .

    DATA_BLOB oBlob = new DATA_BLOB();

    oBlob.pbData = Marshal.AllocHGlobal(bData.Length);
    oBlob.cbData = bData.Length;
    RtlZeroMemory(oBlob.pbData, bData.Length);
    Marshal.Copy(bData, 0, oBlob.pbData, bData.Length);

    return oBlob;

We can see that some new memory is being allocated using Marshal.AllocHGlobal (line 4 above) and the original byte[] is copied into it (line 7 above). This is unmanaged memory and therefore will never be handled by the GC. Instead, it must be freed manually using Marshal.FreeHGlobal which we can see is never called anywhere. A somewhat dirty workaround is to call Marshal.FreeHGlobal(oPlainText.pbData) in dpapiEncryptModule right before return dpMod.
我们可以看到正在使用(上面的第 4 行)分配 Marshal.AllocHGlobal 一些新内存,并将原始 byte[] 内存复制到其中(上面的第 7 行)。这是非托管内存,因此永远不会由 GC 处理。相反,它必须手动释放,我们可以看到它永远不会 Marshal.FreeHGlobal 在任何地方调用。一个有点肮脏的解决方法是在之前 return dpMod 调用 dpapiEncryptModule Marshal.FreeHGlobal(oPlainText.pbData) 。

We’re now down to zero results with the encrypted assembly in memory.

Building a (slightly) better Melkor

Melkor then moves onto the next step which is to decrypt the assembly and execute it in a new AppDomain.
然后,Melkor 进入下一步,即解密程序集并在新的 AppDomain 中执行它。

// Create AppDomain & load module
Console.WriteLine("[>] DPAPI CryptUnprotectData -> assembly[] copy");
hMelkor.DPAPI_MODULE oMod = hMelkor.dpapiDecryptModule(dpMod);
if (oMod.iModSize != 0)
    Console.WriteLine("    |_ Success");
    Console.WriteLine("\n[!] Failed to DPAPI decrypt module..");

Console.WriteLine("[>] Create new AppDomain and invoke module through proxy..");
hMelkor.loadAppDomainModule("dothething", "Angband", oMod.bMod);

Console.WriteLine("\n[?] Press enter to continue..");
Building a (slightly) better Melkor

Before clearing the MessageBox, we can verify that it is indeed running inside a new AppDomain with the name “Angband”.
在清除 MessageBox 之前,我们可以验证它是否确实在名为“Angband”的新 AppDomain 中运行。

Building a (slightly) better Melkor

And instances of “Morgoth Bauglir” in memory has jumped to 6.
记忆中“魔苟斯鲍格利尔”的实例已跃升至 6 个。

Building a (slightly) better Melkor

It’s expected that we’ll find these in memory whilst the assembly is executing, so let’s see what happens when we clear the box and allow Melkor to unload the AppDomain.
预计在程序集执行时,我们会在内存中找到这些内容,因此让我们看看清除该框并允许 Melkor 卸载 AppDomain 时会发生什么。

[>] Unloading AppDomain
[>] Freeing CryptUnprotectData

[?] Press enter to exit..

The AppDomain is gone.

Building a (slightly) better Melkor

But now we have 8(!) instances of “Morgoth Bauglir” in memory.
但现在我们有 8(!记忆中的“魔苟斯鲍格利尔”实例。

Building a (slightly) better Melkor

Some of these stem from similar issues as above, specifically with how data between the DPAPI_MODULE and DATA_BLOB structures is handled.
其中一些源于与上述类似的问题,特别是如何处理 DPAPI_MODULE 和 DATA_BLOB 结构之间的数据。

internal struct DPAPI_MODULE
    public String sModName;
    public int iModVersion;
    public int iModSize;
    public IntPtr pMod;
    public Byte[] bMod;

The dpapiDecryptModule initialises a new DPAPI_MODULE called oMod (line 2 below) and a new DATA_BLOB called oPlainText to hold the decrypted assembly (line 7 below).
初始化 dpapiDecryptModule 一个新 DPAPI_MODULE 调用 oMod (下面的第 2 行)和一个新 DATA_BLOB 调用 oPlainText 来保存解密的程序集(下面的第 7 行)。


    Byte[] bEncrypted = new Byte[oEncMod.iModSize];
    Marshal.Copy(oEncMod.pMod, bEncrypted, 0, oEncMod.iModSize);

    DATA_BLOB oPlainText = new DATA_BLOB();
    DATA_BLOB oCipherText = makeBlob(bEncrypted);
    DATA_BLOB oEntropy = makeBlob(bEntropy);

    String sDescription = String.Empty;
    Boolean bStatus = CryptUnprotectData(ref oCipherText, ref sDescription, ref oEntropy,
        IntPtr.Zero, IntPtr.Zero, 0, ref oPlainText);
    if (bStatus)
        oMod.pMod = oPlainText.pbData;
        oMod.bMod = new Byte[oPlainText.cbData];
        Marshal.Copy(oPlainText.pbData, oMod.bMod, 0, oPlainText.cbData);
        oMod.iModSize = oPlainText.cbData;
        oMod.iModVersion = oEncMod.iModVersion;

    return oMod;

If the decryption is successful it copies the unmanaged data pointer of oPlainText to oMod.pMod (line 17 above) but also makes an entire copy of the data in a new byte[] on oMod.bMod (lines 18/19 above). So that effectively means we now have one managed and one unmanaged copy of the same decrypted assembly. After the AppDomain has been unloaded, a method called hMelkor.freeMod(oMod) is executed which attempts to free the data in the DPAPI_MODULE structure. All this method does is call LocalFree(oMod.pMod) which I think has two problems. The first is that LocalFree free’s up the memory handle but doesn’t erase its content; the second is that that oMod.bMod is never cleared either.
如果解密成功,它将复制 to oMod.pMod 的非 oPlainText 托管数据指针(上面的第 17 行),但也会在新的 byte[] on oMod.bMod 中复制数据的完整副本(上面的第 18/19 行)。因此,这实际上意味着我们现在拥有同一解密程序集的一个托管副本和一个非托管副本。卸载 AppDomain 后,将执行一个名为 hMelkor.freeMod(oMod) 的方法,该方法尝试释放结构中的数据 DPAPI_MODULE 。这种方法所做的只是调用 LocalFree(oMod.pMod) 我认为有两个问题。首先是 LocalFree 免费内存句柄,但不会删除其内容;第二个是这也 oMod.bMod 永远不会被清除。

Melkor is already using RtlZeroMemory in places, so we can use that to rework the method to look something like this:
Melkor 已经在某些地方使用了 RtlZeroMemory ,所以我们可以使用它来重新设计方法,使其看起来像这样:

public static void freeMod(DPAPI_MODULE oMod)
    RtlZeroMemory(oMod.pMod, oMod.iModSize);
    Array.Clear(oMod.bMod, 0, oMod.bMod.Length);

As a side note – there are multiple instances in the project of memory allocations that contain the encrypted data that are not cleared either. I’ve omitted them from the post since they didn’t have an impact on our plaintext string in memory. If this code got a full refactor, then they should be freed as well.
作为旁注 - 内存分配项目中有多个实例,其中包含未清除的加密数据。我从帖子中省略了它们,因为它们对内存中的明文字符串没有影响。如果这段代码得到了完全重构,那么它们也应该被释放。

If we run the tool top to bottom now, we’re down to 4 results by the end.
如果我们现在从上到下运行该工具,到最后我们就会减少到 4 个结果。

Building a (slightly) better Melkor

These are coming from the loadAppDomainModule method and unfortunately, I haven’t found a way to remove them. I believe the issue is that when you have a reference to a new AppDomain in the current AppDomain, then new heap allocations are made to hold the information, including all the assemblies that are loaded within it.
这些来自 loadAppDomainModule 该方法,不幸的是,我还没有找到删除它们的方法。我认为问题在于,当您在当前AppDomain中引用新的AppDomain时,将进行新的堆分配以保存信息,包括其中加载的所有程序集。

For example: 例如:

// create a new AppDomain
AppDomain oDomain = AppDomain.CreateDomain(sAppDomain, null, null, null, false);

ShadowRunnerProxy pluginProxy = (ShadowRunnerProxy)oDomain.CreateInstanceAndUnwrap(
    typeof(ShadowRunnerProxy).Assembly.FullName, typeof(ShadowRunnerProxy).FullName);

// load & execute the assembly
pluginProxy.LoadAssembly(bMod, sMethod);

// unload the AppDomain

We require a reference to the AppDomain, oDomain because we need to pass it to AppDomain.Unload. The AppDomain class/type does not have any properties that tell you the base address or anything that would help in manually freeing that memory. If anybody has more experience with this and can suggest some workarounds, do please reach out.
我们需要对 AppDomain 的引用, oDomain 因为我们需要将其传递给 AppDomain.Unload 。AppDomain 类/类型没有任何属性可以告诉您基址或任何有助于手动释放该内存的属性。如果有人对此有更多经验并且可以建议一些解决方法,请与我们联系。

原文始发于rastamouse:Building a (slightly) better Melkor


版权声明:admin 发表于 2023年9月7日 上午9:32。
转载请注明:Building a (slightly) better Melkor | CTF导航