ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

NoFuserEx 反混淆实战:针对 .NET FuserEx 加固的定向脱壳与还原

NoFuserEx 反混淆实战:针对 .NET FuserEx 加固的定向脱壳与还原

简介:NoFuserEx 反混淆工具面向 .NET 程序逆向与安全分析场景,专门用于清理 Fuser 等混淆器生成的类名、方法调用及控制流干扰,还原出更易读的代码结构,适合安全测试工程师、恶意样本分析人员以及有一定 .NET 基础的程序员。压缩包共 10 个文件,以 exe 主程序为核心,配合 dnlib 程序集解析库、pdb 调试符号、xml 接口文档与 config 配置;各文件职责清晰,既支持直接运行反混淆流程,也便于在调试时追踪符号转换和依赖解析细节,整体体积仅 1.76MB,轻量便于携带。目前已有 356 人学习使用。通过实际操作这套工具,读者能直观感受 Fuser 混淆特征,掌握程序集符号还原、流程梳理等关键思路,并将相同方法应用到其他 .NET 混淆样本的分析中去,对日常安全研究和恶意代码排查具有较强的参考价值。

1. NoFuserEx 反混淆工具:照着 FuserEx 实现逆向出来的专用脱壳机

NoFuserEx 是一款专门对付 .NET 程序集 FuserEx 加固的反混淆工具,它不是通用脱壳器,而是围绕 FuserEx 的混淆模板逐层拆出来的定向工具。典型场景是:拿到的 exe 或 dll 被 FuserEx 处理过,dnSpy 里方法名全是乱码,逻辑被塞进巨大的 switch 分发器,字符串字面量全部消失,手工分析无从下手。跑一遍 NoFuserEx 后,方法名、字符串、控制流会被还原到接近原始 C# 代码的形态。适合恶意样本分析、老程序集功能还原、授权校验逻辑研究这三类从业者。前提是你至少会用 dnSpy 打开程序集、能看懂 IL 基础指令。

2. 先看清对手:FuserEx 的混淆特征与 NoFuserEx 的破解思路

2.1 在 dnSpy 里识别 FuserEx 的六类保护

FuserEx 是一套开源 .NET 混淆器,它的加固组合基本固定为六类。第一类是字符串加密,所有字面量被抽离,真正使用字符串的代码位置替换成对解密函数的调用,解密函数每次运行时从资源或静态字段里恢复原文。第二类是控制流混淆,普通 if/for/while 被拍平成一个大 switch,每个 basic block 前面插入"计算下一个状态"的指令序列,看起来像无意义运算加 switch 跳转。第三类是符号重命名,方法名和字段名改成 Unicode 乱码或者同名重复字符,dnSpy 的搜索功能基本报废。第四类是垃圾代码注入,方法体内塞入不可达分支和无效计算,目的是干扰人工单步。第五类是反调试与反篡改,程序集启动时会校验自身哈希,附加调试器就退出。第六类是常量加密,整数常量被加密存储,使用前用恢复函数还原。

在 dnSpy 里识别这些特征有固定的套路。字符串 stub 型方法通常只有三四条 IL 指令:加载参数、调用解密器、返回,方法名随机但方法体极短。控制流混淆最明显,整个方法看不到直接的 compare 和 branch,全是 ldsfld/ldloc 加 switch,代码行数比原始版本膨胀五到十倍。垃圾代码则表现为永远取不到的立即数和多余的 add/sub 组合。识别这些特征的意义在于:你没搞清楚对面用的是哪种保护之前,任何反混淆参数都是蒙着打的。

2.2 为什么 NoFuserEx 选择"定向拆弹"而不是通用扫描

通用反混淆插件走的是启发式扫描路线,靠方法体形态猜测"这里像混淆代码",遇到模板会变的混淆器就很容易翻车。FuserEx 每次迭代都会调整生成代码的模板,启发式规则一版跟不上就会把正常方法误判成混淆代码,或者解密逻辑执行一半抛异常,产出又脏又不能用。NoFuserEx 的处理思路完全不同:它维护一个针对 FuserEx 模板的规则库,先判断"这段代码是 FuserEx 哪个模块生成的",再套对应的恢复逻辑。规则库是特征驱动而不是形态猜测,所以误报率低得多。

这个设计带来的实际差异,我用一个样本说明。同一个被 FuserEx 处理过的程序集,通用插件跑完后字符串还是乱码、switch 还在;NoFuserEx 跑完后字符串还原成明文,switch 分发器被折叠回普通分支。代价是它只认 FuserEx 的模板,换一个混淆器就只能干瞪眼。所以选型上我的建议很直接:如果你手上的样本确认是 FuserEx 加固,直接用 NoFuserEx 效率最高;如果样本来源混杂、不确定混淆器类型,先用 DetectItEasy 之类的工具做类型识别再决定。

保护类型dnSpy 里的表现NoFuserEx 的处理方式
字符串加密方法体只有调用解密器并返回识别 stub,批量还原并回写明文
控制流混淆大 switch 加状态变量符号执行状态计算,折叠回原分支
符号重命名Unicode 乱码方法名按调用关系重命名可读名称
垃圾代码不可达分支、无效运算活性分析后删除死代码
反调试/反篡改启动时自我校验剥离校验并修正元数据
常量加密整数全是加密值还原常量表并替换引用

还要注意一点,规则库是跟着 FuserEx 版本走的。你手里的混淆器版本新、NoFuserEx 规则库旧,识别就会失败。这时先跑一遍检测模式,输出里如果有大量 unknown 标记,说明规则库需要更新,或者样本不是标准模板生成的,属于混合加固,得先处理外层再回来处理 FuserEx。

3. 跑通第一遍:NoFuserEx 的安装、命令行参数与典型用法

3.1 运行环境与准备工作

NoFuserEx 以命令行工具形式分发,主程序依赖 .NET 运行时,资源包通常包含主程序文件、规则库文件和一个用于运行时解密字符串的辅助程序集。我一般把整个解压目录放在纯英文路径下,比如 D:\tools\NoFuserEx,不要放进带空格和中文的路径。原因是后续处理大量样本时你会写批处理脚本,中文路径在部分 Windows 编码环境下会让文件路径解析出错,属于能提前躲掉就别硬踩的坑。

验证环境是否就绪,直接跑一次版本输出:

NoFuserEx.exe -v

正常会输出程序版本号和规则库版本号,规则库通常以日期格式命名,方便你判断新旧。如果提示缺少运行时,先去装对应版本的 .NET Desktop Runtime,装完再回来。这一步卡住的多数原因是电脑只装了 .NET Framework 而没装新版 .NET 运行时,两者的关系在 Windows 上经常被搞混,装错版本也会报同样的错。

3.2 命令行参数说明

NoFuserEx 的参数设计比较直白,核心参数如下表。日常分析最常用的是 -i、-o、-d 三个,其余保持默认即可。

参数作用默认值备注
-i输入程序集路径无必填,支持 exe/dll
-o输出程序集路径输入路径加 _clean 后缀手动指定避免覆盖原文件
-d启用的反混淆模块strings,ctrlflow,junk模块间用逗号分隔
-m控制流还原最大递归深度5深度越大耗时越长
-k是否保留解密 stubfalse调试规则时建议开 true
-p自定义规则库路径内置规则库规则库更新时用
--platform目标平台autoauto/x86/x64/anycpu
-v输出版本信息无诊断用

-d 参数需要特别说明:它控制的是"这一轮做哪些还原"。字符串解密和控制流还原是最常用的两个模块,垃圾代码清理默认开启但影响不大。如果你只想要快速的字符串明文来做关键词搜索,可以只开 strings,速度会快很多;如果要做完整功能还原,再开全量。参数 -m 只在开 ctrlflow 时生效,递归深度调高会显著增加耗时,但某些复杂方法深度不够时还原不干净,需要按样本实际情况反复试。

3.3 第一次完整反混淆:命令与产物检查

拿一个被 FuserEx 处理过的样本做完整还原,我常用的命令是这样:

NoFuserEx.exe ^ -i C:\samples\target.exe ^ -o C:\samples\target_clean.exe ^ -d strings,ctrlflow,junk,names ^ -m 5 ^ --platform auto

逐段解释一下。第一行指定输入样本,第二行指定输出路径,显式写输出路径是为了防止工具自动覆盖原样本,原始样本一旦被覆盖就没有后悔药了。第三行的 -d 开启了四个模块:strings 做字符串还原,ctrlflow 做控制流折叠,junk 做垃圾代码删除,names 做可读重命名。第四行的 -m 5 表示控制流还原时递归分析状态计算的最大深度是 5,对普通方法足够;遇到特别复杂的混淆方法可以调到 8,但耗时翻倍。第五行的 --platform auto 让工具自己读程序集平台标识,避免人为指定错平台导致还原后程序集没法正常加载。

跑完之后别急着关命令行,看日志里的统计信息。重点关注两个数字:还原了多少个字符串常量、折叠了多少个 switch 分发器。如果字符串还原数是 0,说明规则没有命中,这个样本可能不是 FuserEx 标准模板。然后用 dnSpy 打开输出文件,检查一个已知有字符串的方法和原始版本对比。我习惯先看入口方法:原始版本里是 stub 加 switch,还原后应该能看到直接的字符串赋值和 if/else 分支。这一步确认通过,再继续往下分析。

提示:第一次跑全量模块之前,建议先用 -d strings 单独跑一遍输出到一个临时文件,确认字符串解密正常后再叠加 ctrlflow。这样定位问题时能快速排除"到底是哪个模块弄坏了产物"。

4. 核心反混淆流程:从字符串解密到控制流还原的完整链路

4.1 保护类型判定:规则库如何命中混淆代码

NoFuserEx 的第一步不是直接还原,而是对整个程序集做一轮扫描,按规则库的模板匹配每个类型和方法。规则库里每个规则描述的是 FuserEx 某类混淆代码的 IL 特征,比如字符串 stub 的规则是"方法体指令数小于 6、存在对标记为解密器的方法的调用、返回值类型为 string"。命中规则后,工具会把这些位置记录成待处理项,并标注所属模块类型。这一步相当于给整个程序集做了一次体检,体检报告在日志里按类型汇总。

用 Mono.Cecil 写一个简化版的特征扫描,大致是这个思路:

using Mono.Cecil; using Mono.Cecil.Cil; var asm = AssemblyDefinition.ReadAssembly(args[0]); int stubCount = 0; foreach (var type in asm.MainModule.Types) { foreach (var method in type.Methods) { if (!method.HasBody || method.Body.Instructions.Count > 6) continue; foreach (var ins in method.Body.Instructions) { if (ins.OpCode == OpCodes.Call && ins.Operand is MethodReference callee && callee.ReturnType.FullName == "System.String") { stubCount++; break; } } } } Console.WriteLine($"candidate string stubs: {stubCount}");

这段代码的过滤条件是"方法体指令数少于 6 且有调用 string 返回值方法的 call 指令"。指令数上限是经验值,FuserEx 生成的字符串 stub 普遍很短;如果方法体超过 6 条指令还命中 call,很可能是正常代码而非混淆 stub。真正工具的实现比这复杂得多,会验证 callee 的声明类型是否符合解密器特征,还会检查调用约定,不是只看返回类型。扫描通过后,待处理项进入下一步的还原队列。

4.2 字符串解密:静态模拟与运行时调用的取舍

字符串还原有两条主流路线。静态模拟是在不加载程序集的情况下,用解释器执行解密方法的 IL,把加密数据喂进去算出明文。优点是干净、不触发反调试,缺点是遇到依赖外部状态的解密器时会算错。运行时调用则是在受控的 .NET 进程里加载程序集,直接调用解密方法拿结果,优点是准确,缺点是可能触发程序集里的反调试逻辑。

NoFuserEx 默认的策略是混合模式:先尝试静态模拟,模拟失败的方法标记出来,再用运行时调用补一轮。我一般建议保持默认,不要把运行时调用整个关掉。实际分析中遇到过一种典型情况:某个字符串解密方法读取了一个静态字段,静态字段的值在类型初始化器里被其他函数改过,静态模拟算出来全是乱码,运行时调用一次就全对了。这属于黑匣子行为,不实际跑一遍很难提前判断。

4.3 控制流还原:从 switch 分发器回到普通分支

控制流混淆的还原是整个流程里技术含量最高的一步。FuserEx 的混淆方法通常长这样:一个入口加载代码、一个状态变量、一个巨大 switch,每个 case 里是这个 basic block 的实际逻辑,block 最后一段代码计算下一个状态并跳回 switch。还原思路是符号执行状态变量:从入口开始,跟踪状态变量的所有赋值路径,把"状态值到 case 块"的映射整理出来,再按顺序重构分支结构。

做法上,NoFuserEx 对每个命中 ctrlflow 规则的方法做一次模拟执行。模拟器只跟踪状态变量和少数几个关键值,不完整执行所有指令,遇到 call 指令直接跳过。得到状态转移表之后,把 switch 折叠回条件跳转。有个细节值得注意:原始代码里有些分支是故意不可达的,还原时保留这些分支会让方法体仍然很难看,所以工具会结合活性分析把不可达 case 直接剪掉。剪掉的前提是确认不可达,判断错了就会丢代码,这也是后面排查章节会讲到的常见翻车点。

4.4 元数据重建:还原后最后一步不能省

字符串还原和控制流折叠都会改变方法的指令,原有元数据的引用关系随之失效。NoFuserEx 最后会调用 Mono.Cecil 重新构建程序集元数据:重算方法体大小、修正分支目标、更新异常处理块的偏移、刷新 token 表。这步如果做不干净,输出程序集会直接报 BadImageFormatException。

判断元数据重建是否成功的快捷方式,是用 ilspycmd 把输出文件反编译成 C# 源码,能正常出代码且没有格式错误,基本就说明元数据是健康的:

ilspycmd -p C:\samples\target_clean.exe > C:\samples\target_clean.cs

如果反编译时抛异常,多半是元数据重建出了问题,而不是前面的还原逻辑错了。这时回到原样本重新跑,把 -k 设为 true 保留解密 stub,看问题是否消失,用来缩小排查范围。

5. 常见问题与避坑:输出损坏与还原错误的两类高频翻车点

5.1 程序集输出异常:打不开、报格式错误

现象一:NoFuserEx 跑完没有任何报错,但输出文件双击启动直接崩溃,dnSpy 打开提示 BadImageFormatException。原因是元数据重建环节没处理干净,字符串还原后引用的 token 和新的方法体大小不匹配。解决:先确认你是否开了 -k true 保留解密 stub。保留 stub 时工具不会重写解密调用点,只追加结果,程序集结构变化小,很多元数据问题会消失。如果保留 stub 后能打开,再逐轮关闭模块定位是哪个还原步骤弄坏了结构。

现象二:输出文件能打开,但运行时提示强名称签名验证失败。原因是原程序集带强名称签名,混淆器在签名后修改了代码,反混淆再次修改代码后签名自然失效。解决:这属于预期行为,不是工具 bug。分析场景下直接跳过验证即可;需要交付时用重新签名工具对输出文件重新签名,或者修改程序集属性去掉强名称再重新处理。

现象三:日志显示处理结束,但生成的文件大小几乎没变。原因通常是规则库没有命中任何保护模块,工具做了空跑。解决:先用 -v 看规则库版本,再用 DetectItEasy 确认样本确实是 FuserEx 加固;如果确认是但没命中,多半是混淆器版本太新,等规则库更新,或者把样本的相关方法导出出来供规则库补充。不要反复调参数硬试,浪费时间也得不到好结果。

5.2 还原结果错误:乱码、丢代码、switch 没折叠

现象四:字符串解密后得到的结果是乱码,但没有报错,日志里能看到 static-sim fail 的标记。原因是该解密方法依赖静态状态,静态模拟没能拿到正确输入。解决:把失败的方法加入运行时调用白名单,重新跑一遍。这种问题排查起来很费时间,我的做法是第一次就跑完整的混合模式,不要贪图速度只开静态模拟。

现象五:控制流还原后方法体变短了,但和原始逻辑对比发现少了真实代码。原因是不可达分支判断过于激进,把还活着的 case 当垃圾剪掉了。解决:把 -d 里的 ctrlflow 模块单独拆出来重跑,同时把 -m 的递归深度从 5 调到 3,降低模拟器的探索范围,减少误判。如果深度调低后逻辑恢复,说明原始深度下模拟器吃进了过多无关路径。这是 ctrlflow 模块最常见的坑,原则是"先还原正确,再追求美观"。

注意:还原产物只要出现过一次丢代码,就别拿它当最终分析基线。宁可退回原始样本重新处理,也不要试图在损坏的产物上继续分析,后面所有结论都会不靠谱。

6. 进阶用法:批量处理、与 dnSpy 联动和三层结果验证

6.1 批量处理与还原结果验证

实际分析很少只处理一个样本,NoFuserEx 支持目录级批量处理,配合批处理脚本可以一次跑完整个样本集。常见做法是写一个 for 循环遍历目录下的 exe 和 dll,逐个处理并输出日志:

for %%f in (C:\samples\*.exe C:\samples\*.dll) do ( NoFuserEx.exe -i "%%f" -o "%%f_clean.exe" -d strings,ctrlflow,junk ^ >> C:\samples\process.log 2>&1 )

这段脚本会把每个样本的处理结果追加到同一个日志文件,跑完后用 findstr 统计每条日志里的还原统计行,自动筛选还原率异常的样本。批量跑之前我强烈建议先拿两个已知样本验证命令参数,确认输出正确再放开全量,否则一批样本全被跑坏,重来成本很高。

还原完对不对,不能只看能不能打开。我一般按三个层面验证:第一层是结构验证,用 ilspycmd 把输出反编译成 C#,确认无语法错误、异常处理块完整;第二层是行为对比,把原样本和还原样本分别在沙箱里跑一遍,对比启动参数、网络请求、文件读写路径这些可观测行为是否一致;第三层是逻辑抽查,挑两三个关键方法,人工比对还原前后 IL,确认 switch 折叠后的分支和原逻辑等价。三层都过了,我才会把还原产物当作分析基线。

6.2 与 dnSpy 联动:还原产物当对照基线

单个方法还原后看不懂时,我的习惯是把 NoFuserEx 的还原结果作为 dnSpy 的对照基线:左边窗口打开原始样本,右边打开还原样本,同步滚动到同一个方法,用 dnSpy 的 IL 视图对照看。原始样本里是乱码字符串和 switch,还原样本里是明文和分支,两边一对照,混淆器做了什么手脚就一目了然。

需要保存还原后程序集时,确保输出路径不要覆盖原样本。我吃过一次亏:当时为了让 dnSpy 直接加载输出文件,把输出路径写到了原样本同目录同名,原样本被覆盖后才想起后面还要对照,只能重新去备份目录里拿回副本。从那以后,我每次批量处理前都强制把输出目录单独建一个文件夹,原样本目录一律只读。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进