DotNET逆向基础
DotNET逆向基础
本文适合
CTF 逆向工程 入门学习者。学完你能:判断程序是否为 .NET,使用 dnSpy/ILSpy 从类、方法、资源和事件处理函数定位校验逻辑,并完成源码级或 IL 级验证
.NET 程序通常编译成中间语言 IL,并保留大量元数据。相比原生 C/C++ 程序,很多 .NET 题更接近“高层代码恢复”。
一句话判断
如果 PE 文件带 CLR/.NET 元数据,或 dnSpy/ILSpy 能直接恢复类、方法和 C# 代码,就按 .NET 逆向处理,优先看字符串、资源和事件处理函数。
.NET 逆向的优势是元数据多;不要一开始按原生汇编读,先利用类型、方法名、资源和 IL。
题目中常见信号
- 文件是
.exe/.dll,但 Detect It Easy、CFF Explorer 或 dnSpy 显示.NET、CLR、MSIL。 - 反编译后能看到 C# 类、命名空间、
Main、Form、Button click 事件。 - 字符串里有
Correct、Wrong、flag、password、System.、.resources。 - GUI 程序关键逻辑不在
Main,而在按钮点击、文本框事件或窗体加载事件。 - 资源里有图片、文本、加密数据、嵌入 DLL 或配置。
- 类名方法名混淆,但字符串解密函数或资源加载函数集中出现。
核心概念
.NET 程序通常由 IL 和元数据组成。元数据保留类、方法、字段、资源和引用关系,让逆向更接近源码阅读。
分析重点:
- 入口和事件:控制台看
Main,GUI 看窗体和按钮事件。 - 字符串和资源:校验提示、密钥、密文常在字符串或
.resources中。 - IL 和 C# 对照:反编译失真时,回到 IL 看真实栈操作和分支。
- 混淆处理:名字混淆不是终点,字符串解密和控制流混淆才需要专门处理。
最小分析流程
- 用 DIE/CFF Explorer/dnSpy 确认是否 .NET。
- 用 dnSpy/ILSpy 搜
flag、correct、wrong、check、verify。 - 如果是 GUI,查看窗体类、按钮点击事件、
InitializeComponent的事件绑定。 - 读取资源文件和配置,寻找 key、密文、嵌入程序集。
- 遇到混淆先找字符串解密函数,必要时用 de4dot 或动态调试。
- 在 dnSpy 下断点观察输入、变量、返回值,或编辑方法/IL 验证成功路径。
最小验证示例
# 先判断类型
file challenge.exe
# Windows 下也可用 Detect It Easy / CFF Explorer 看 CLR Header在 dnSpy 中:
File -> Open challenge.exe
Ctrl+Shift+K 搜索 correct/wrong/flag/check
双击搜索结果进入方法
右键方法 -> Analyze 查看调用关系
F9 下断点,F5 调试如果搜索到:
if (Check(textBox1.Text)) MessageBox.Show("Correct");就进入 Check,读算法或在返回前下断点观察变量。这个过程比直接看原生入口点更高效。
常见利用 / 解题路线
路线总览:
- 源码恢复路线:dnSpy/ILSpy 直接读 C#,还原 check 函数。
- GUI 事件路线:从 Button.Click、TextChanged、Form_Load 找业务入口。
- 资源提取路线:打开
.resources、配置、嵌入文件,提取密文或 key。 - 字符串解密路线:定位运行时解密函数,批量解出字符串后再分析。
- IL patch 路线:把
brfalse改成brtrue或强制返回 true,用来验证成功路径。 - 动态调试路线:dnSpy 下断点看变量、返回值和解密后明文。
.NET 程序特点
.NET 程序常见文件是 Windows PE,但内部包含 CLR 元数据和 IL。
它可能由 C#、VB.NET、F# 等语言编写。
反编译工具通常能恢复较可读的高层代码。
这让 .NET 逆向常常从类、方法、字符串、资源开始。
IL 是什么
IL 是 Intermediate Language,中间语言。
它运行在 CLR 上,最终由 JIT 编译成机器码。
IL 比机器码更接近源代码,包含类型、方法、字段等结构。
IL 使用基于栈的执行模型,指令通过压栈和出栈操作数据。例如 ldc.i4 0x5a 将整数 90 压入计算栈,stloc.0 将栈顶存入局部变量 0。理解 IL 的栈操作有助于读懂反编译结果中的复杂表达式。
IL 指令示例:
// C# 源码: int x = 90 + 6;
// 对应 IL 指令:
ldc.i4 0x5a // 将常量 90 压栈
ldc.i4 0x06 // 将常量 6 压栈
add // 弹出两个值,相加后压栈
stloc.0 // 结果存入局部变量 0常见观察点
分析 .NET 程序时,应按优先级检查以下位置:
- 字符串常量:搜索关键字符串如 "flag"、"correct"、"wrong"、"password",通常能直接定位校验逻辑。
- 窗体事件和按钮点击处理函数:很多 GUI 题的关键逻辑藏在按钮事件里,而不是 Main 函数本身。
- 入口函数 Main:程序启动逻辑,可能包含初始化或参数解析。
- 资源文件:嵌入的图片、配置、加密数据可能隐藏关键信息。
- 配置文件:
.config或嵌入的 XML 可能包含运行时参数。 - 混淆后的控制流:混淆程序需要先识别保护类型再分析。
常见 .NET 分析工具和命令:
# 使用 ildasm 反汇编 .NET 程序
ildasm /text /out=program.il program.exe
# 使用 ilasm 重新编译修改后的 IL
ilasm /exe program.il
# 使用 dotnet-dump 分析 .NET Core 程序
dotnet-dump collect -p <pid>
dotnet-dump analyze <dump-file>混淆
.NET 程序常见混淆包括:
- 名字混淆:类名、方法名变成
a、b、A0等无意义字符,使用 ConfuserEx、Dotfuscator 等工具。 - 字符串加密:明文字符串被加密,运行时通过解密函数还原。找到解密函数后可批量提取所有字符串。
- 控制流混淆:插入死代码、switch 跳转表、opaque predicates,使反编译结果难以阅读。
- 资源加密:嵌入资源被加密,运行时解密加载。
- 动态加载程序集:运行时从字节数组或网络加载 DLL,静态分析看不到被加载的代码。
名字混淆不一定影响逻辑理解,字符串解密点和事件处理函数往往更重要。
用 dnSpy 分析时,可以右键方法选择 "编辑方法" 直接修改 IL 指令或 C# 代码并保存,这是 .NET 逆向相比原生程序的一个优势。
dnSpy 工作流程
dnSpy 是 .NET 逆向的主要工具:
# 启动 dnSpy
# 打开 .exe 或 .dll 文件
# dnSpy 会自动反编译为 C# 代码基本操作
1. 打开文件:File -> Open
2. 导航:双击类名、方法名跳转
3. 搜索:Ctrl+Shift+K(搜索所有)
4. 编辑:右键方法 -> Edit Method (C#)
5. 调试:F5 启动调试,F9 设置断点
6. 保存:File -> Save Module调试技巧
# dnSpy 调试器功能:
# - 设置条件断点
# - 修改变量值
# - 查看调用栈
# - 内存窗口
# - 即时窗口(执行表达式)
# 断点条件示例:
# 条件:password == "admin"
# 当 password 变量等于 "admin" 时断下IL 指令参考
// 常用 IL 指令
// 加载常量
ldc.i4 42 // 加载整数 42
ldc.i4.s 42 // 加载短整数(-128 到 127)
ldc.i8 123456789 // 加载长整数
ldc.r4 3.14 // 加载浮点数
ldc.r8 3.14 // 加载双精度浮点数
ldnull // 加载 null
ldstr "hello" // 加载字符串
// 加载变量
ldloc.0 // 加载局部变量 0
ldloc.1 // 加载局部变量 1
ldarg.0 // 加载参数 0(实例方法是 this)
ldarg.1 // 加载参数 1
// 存储变量
stloc.0 // 存储到局部变量 0
starg.1 // 存储到参数 1
// 算术运算
add // 加法
sub // 减法
mul // 乘法
div // 除法
rem // 取余
neg // 取反
// 位运算
and // 与
or // 或
xor // 异或
not // 非
shl // 左移
shr // 右移
// 比较
ceq // 等于比较
cgt // 大于比较
clt // 小于比较
// 分支
br target // 无条件跳转
brtrue target // 如果栈顶为 true 跳转
brfalse target // 如果栈顶为 false 跳转
beq target // 如果相等跳转
bne.un target // 如果不等跳转
bgt target // 如果大于跳转
blt target // 如果小于跳转
// 方法调用
call method // 调用静态方法
callvirt method // 调用虚方法
ret // 返回
// 对象操作
newobj ctor // 创建对象
ldfld field // 加载字段
stfld field // 存储字段
// 数组操作
newarr type // 创建数组
ldlen // 获取数组长度
ldelem.i4 // 加载数组元素
stelem.i4 // 存储数组元素
// 异常处理
throw // 抛出异常
try // 开始 try 块
catch // 开始 catch 块
finally // 开始 finally 块de4dot 反混淆
# 安装 de4dot
# GitHub: https://github.com/de4dot/de4dot
# 基本用法
de4dot obfuscated.exe -o clean.exe
# 指定混淆器类型
de4dot obfuscated.exe -p confuserex -o clean.exe
# 常见混淆器:
# - ConfuserEx
# - .NET Reactor
# - SmartAssembly
# - Dotfuscator
# - Crypto Obfuscator
# 批量处理
de4dot -d input_dir -r -o output_dirde4dot 功能
1. 名称恢复:将混淆的名称还原为可读形式
2. 字符串解密:解密加密的字符串常量
3. 控制流恢复:去除控制流混淆
4. 资源解密:解密加密的资源文件
5. 代理委托移除:去除委托包装
6. 反反射调用:将反射调用转为直接调用手动反混淆
# 当 de4dot 无法处理时,手动反混淆
# 1. 字符串解密
# 找到解密函数,编写脚本批量解密
def decrypt_strings(assembly_path):
# 使用 dnlib 或 Mono.Cecil 加载程序集
from dnlib.DotNet import ModuleDefMD
assembly = ModuleDefMD.Load(assembly_path)
# 遍历所有方法,找到对解密函数的调用
for method in assembly.methods:
if method.body:
for instr in method.body.instructions:
if instr.opcode.name == 'call':
target = instr.operand
if hasattr(target, 'name') and 'decrypt' in target.name.lower():
# 提取参数并调用解密
print(f"[+] Found call to {target.name} in {method.name}")
# 2. 控制流恢复
# 手动分析 switch 跳转表
# 删除死代码
# 重建 if-else 结构.NET Core 差异
.NET Framework vs .NET Core 主要差异:
1. 运行时:
- .NET Framework:CLR,仅 Windows
- .NET Core:CoreCLR,跨平台
2. 程序集格式:
- .NET Framework:PE + CLR
- .NET Core:可能使用 AssemblyLoadContext
3. 依赖:
- .NET Framework:GAC 全局缓存
- .NET Core:NuGet 包,本地部署
4. 配置:
- .NET Framework:app.config / web.config
- .NET Core:appsettings.json
5. 反编译工具:
- .NET Framework:dnSpy, ILSpy
- .NET Core:dnSpy, ILSpy, dotPeek# .NET Core 程序的分析
# 1. 检查是否是 .NET Core
import pefile
def check_dotnet_core(filepath):
pe = pefile.PE(filepath)
# 检查是否包含 CoreCLR 标记
for entry in pe.DIRECTORY_ENTRY_IMPORT:
if b'coreclr' in entry.dll.lower():
return True
# 或检查 .deps.json 文件
import os
deps_file = filepath.replace('.exe', '.deps.json')
if os.path.exists(deps_file):
return True
return False
# 2. 查找入口点
# .NET Core 可能使用不同的入口模式
# - 传统的 Main 方法
# - ASP.NET Core 的 Startup
# - Top-level statements (C# 9+)IL 指令与 C# 对应
// C# 代码:
// int x = 10 + 20;
// if (x > 25) Console.WriteLine("Yes");
// 对应 IL:
ldc.i4.s 10 // 压入 10
ldc.i4.s 20 // 压入 20
add // 相加,栈顶 = 30
stloc.0 // 存入局部变量 0(x)
ldloc.0 // 加载 x
ldc.i4.s 25 // 压入 25
ble.s ELSE_LABEL // 如果 x <= 25 跳转
// if 分支
ldstr "Yes" // 压入字符串
call void [mscorlib]System.Console::WriteLine(string)
br END_LABEL
ELSE_LABEL:
// else 分支
END_LABEL:
ret完整 .NET 逆向流程
def dotnet_reverse():
# 步骤 1:检查文件类型
# 使用 CFF Explorer 或 PE 工具
# 确认是 .NET 程序
# 步骤 2:用 dnSpy 打开
# 查看程序结构:类、方法、资源
# 步骤 3:检查混淆
# 如果被混淆,先用 de4dot 处理
# 步骤 4:查找关键点
# - 搜索字符串:flag, password, key
# - 查看事件处理函数
# - 查看 Main 方法
# 步骤 5:分析算法
# 读取反编译的 C# 代码
# 理解校验逻辑
# 步骤 6:编写解密脚本
# 使用 Python 或 C# 实现相同算法
# 或直接修改 IL 绕过检查
# 步骤 7:动态调试
# 在 dnSpy 中调试
# 设置断点,观察变量
# 步骤 8:提取 flag
# 在断点处查看寄存器或内存中的 flag
# 或在解密函数返回值处读取
# 示例:在 dnSpy 中,右键变量 -> 添加监视
# 或在 GDB 中:x/s $rdx (解密后的字符串通常在 rdx)
flag = input("Enter observed flag from debugger: ")
print(f"[+] Flag: {flag}")常见失败原因
- 看到 PE 就按普通原生程序分析:先检查 CLR Header;.NET 应优先用 dnSpy/ILSpy。
- 只看
Main:WinForms/WPF 关键逻辑常在事件处理函数。 - 被混淆名字吓住:名字乱不影响字符串、资源、调用关系和事件绑定。
- 反编译代码不可信:切到 IL 看真实分支、常量和方法调用。
- 忽略资源和配置:flag 片段、密文、key、嵌入 DLL 常藏在资源中。
- patch 后不复盘算法:patch 可验证路径,但最终 WP 应解释真实校验逻辑。
- .NET Core 依赖漏掉:同时检查
.deps.json、appsettings.json和旁边 DLL。
迷你案例
一个 Windows 程序打开后是密码框。dnSpy 搜 Wrong,定位到:
private void button1_Click(object sender, EventArgs e) {
if (Check(textBox1.Text)) MessageBox.Show("Correct");
else MessageBox.Show("Wrong");
}Check 中把输入逐字符异或 0x33 后和资源 data 比较。导出资源里的字节数组,写 Python 逆异或得到正确密码。这个案例的闭环是:识别 .NET -> 搜字符串 -> 找按钮事件 -> 读资源和算法 -> 脚本还原。