绿盟科技
- 关注
0x00 简介
随着攻防博弈的发展,无文件攻击在各项场景中出现频率越来越高,研究无文件攻击的实现与检测对于抵御外部攻击有着重要的意义。.Net程序集的内存加载是无文件攻击实施的手段之一,目前现有的加载技术大多适用于Net FrameWork框架, 本文针对.Net Core和.Net FrameWork中的加载方式和卸载方式进行了分析探讨。
0x01 加载
.Net程序集加载应用很普遍,像冰蝎中aspx马,就是使用Assembly.Load来加载各种功能的程序集从而实现webshell控制。yso.net工具中有条反序列化利用链通过Assembly.load直接加载恶意程序集。asp.net内存马也可以通过Assembly.load注入,还可以通过多阶段加载方式来进行防御规避。
根据微软官方文档,.NET中的程序集加载方式有下面几种。
Assembly.Load | 此方式将程序集加载到当前上下文 |
Assembly.LoadFrom | 此方式将程序集加载到当前上下文 |
Assembly.LoadFile | 此方式将程序集加载到当前上下文 |
Assembly.ReflectionOnlyLoad AssemblyReflectionOnlyLoadFrom | 程序集加载到反射上下文,无法执行代码 |
AppDomain .CreateInstance | 创建指定类型的实例 |
AppDomain.CreateInstanceAndUnwrap | 返回创建的实例的代理, 把其他应用程序域中的变量传到当前的应用程序域 |
Type.GetType | |
AppDomain.Load | |
AssemblyLoadContext.Load | .Net Core中的新增,允许加载同一程序集的不同版本到不同上下文 |
以上加载方式,Assembly.Load和AssemblyLoadContext.Load支持字节流加载。
Assembly.LoadFrom 加载dll文件及依赖项。它还可以从远程加载程序集,支持http、file等协议,但从.Net FrameWork 4后,默认禁止远程加载。此外,如果目标程序集已经加载过,LoadFrom()不会重新进行加载。
Assembly.LoadFile 仅加载dll不加载依赖项。
AssemblyLoadContext.Load是Net core中为替代Appdomain提出的新的进程内隔离解决方案。Load会第一次解析、加载和返回程序集。如果第一次返回null,则加载程序会尝试将程序集加载到默认上下文AssemblyLoadContext.Default中。如果AssemblyLoadContext.Default无法解析程序集,则原始AssemblyLoadContext将有第二次机会解析程序集。运行时引发Resolving事件,我们可以自定义Resolving事件来编写依赖项解析行为。下面这个例子,将Test.dll加载到新的上下文中,Test.dll会打印出当前的上下文名字。
AssemblyLoadContext assemblyLoadContext = new AssemblyLoadContext("New1",true); //true允许unload var assem = assemblyLoadContext.LoadFromStream(new MemoryStream( File.ReadAllBytes("Test.dll"))); aa.GetType("Test.Test").GetMethod("Run").Invoke(null,null); ass
运行结果如下,可以看到,程序集加载到了新的上下文中
如果只是想在其他Appdomain执行方法,可以调用domain.DoCallBack()。CrossAppDomainDelegate是委托类型,签名如下
举个例子,运行该代码,可以把字节码加载到新的Appdomain中
public static void Main(){ AppDomain domain = AppDomain.CreateDomain("MyDomain"); domain.DoCallBack(new CrossAppDomainDelegate(CallbackTest)); } public static void CallbackTest() { Assembly.Load(System.IO.File.ReadAllBytes(@"E.dll")); var s = AppDomain.CurrentDomain.FriendlyName; }
0x02 卸载
在.NET Core 中,System.Runtime.Loader.AssemblyLoadContext类处理程序集的卸载。在Net FrameWork中,如果我们想要卸载单独的程序集,那是做不到的,想要在 .NET Framework 中卸载程序集,必须卸载包含它的所有应用程序域。假设内存加载了一个恶意程序集到新的应用程序域,来看一下应用程序域如何被卸载。卸载应用程序域使用AppDomain.Unload 方法。
下面看一段代码,这段代码的作用是弹窗,编译成程序集E.dll
namespace E { public class E { public void Run() { try { System.Windows.Forms.MessageBox.Show("Run","Title"); } catch (Exception) { } } } }
下面代码读取了E.dll的字节码,新创建一个Appdomain,然后以Load(byte[])方法加载程序集E.dll到新应用程序域当中,反射调用Run方法,最后卸载Appdomain。
byte[] bytes = File.ReadAllBytes("E.dll"); // dll bytes AppDomain domain = AppDomain.CreateDomain("Test"); Assembly assembly = domain.Load(bytes); MethodInfo method = assembly.GetType("E.E").GetMethod("Run"); if (method != null) { object o = assembly.CreateInstance("E.E"); try { method.Invoke(o, null); } catch (TargetInvocationException ex) { Console.WriteLine(ex.ToString()); } }
看起来没什么错误,但一旦运行后,就会报下面异常。
其原因是Appdomain.Load(byte[])加载的程序集,在Appdomain间是无法共享类型信息的。如果默认Appdomain没有加载这个程序集,那么就不会有关于此程序集的类型信息,第二个Appdomian用Load(Byte[])加载后,要把程序集对象传递到默认Appdomain中,但默认Appdomain找不到此对象的信息,所以会报错。解决方式就是使用跨应用程序域访问。
一般来说,有两种方式进行跨域通信,marshalbyrefobject和Serializable,前者是引用传递,后者是值传递。看个引用传递的例子。首先创建Proxy类继承MarshalByObject,MarshalByObject类属于基元类型,在默认Appdomain中存在。然后调用CreateInstanceAndUnwrap得到Proxy类的代理类。CreateInstanceAndUnwrap方法是CreateInstance和Objecthandle.Unwrap方法的结合,Proxy类实际上在MyDomain中实例化,然后通过Objecthandle.Unwrap包装后传递给默认Appdomain,这样便实现了跨AppDomain的对象传递。之后Proxy类的方法调用,实际上也是在Mydomain中执行的。
void Main(){ byte[] classbytes = File.ReadAllBytes("E.dll"); AppDomainSetup domaininfo = new AppDomainSetup(); domaininfo.ApplicationBase = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); System.Security.Policy.Evidence adevidence = AppDomain.CurrentDomain.Evidence; AppDomain currentDomain = AppDomain.CreateDomain("MyDomain", adevidence, domaininfo); Type type = typeof(Proxy); Proxy p = (Proxy)currentDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, type.FullName); p.LoadAssembly(classbytes); AppDomain.Unload(currentDomain); } public class Proxy: MarshalByObject{ public Object LoadAssembly(byte[] bytes) { Console.WriteLine(Appdomain.Currentdomain.FriendlyName); Assembly assembly = AppDomain.CurrentDomain.Load(bytes); MethodInfo method = assembly.GetType("E.E").GetMethod("Run"); if (method != null) { object o = assembly.CreateInstance("E.E"); try { var res = method.Invoke(o, null); return res; } catch (TargetInvocationException ex) { Console.WriteLine(ex.ToString()); } } return null; } }
运行结果如下
0x03 总结
本文主要针对.Net Core和.Net FrameWork框架下的程序集加载技术进行了总结,针对程序集的卸载技术进行了分析,指出了使用无文件加载后,如何正确卸载程序集的方式。
附录 参考链接
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext?view=net-7.0
https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/understanding-assemblyloadcontext
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.remoting.objecthandle?view=net-7.0
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
