那阵子给「七亿批量远程管理工具」准备发版,照例先过一遍代码混淆——.NET Reactor 7.5,最新版,配置跟上一版一模一样。混完装上去,界面能开、能点,连接、列表都正常,唯独 SFTP 上传下载,队列永远卡在"计算中",0 B/s,0%。不报错,不弹框,安静得像什么都没发生。

最邪门的是排障日志(我们自己加的 sftp-debug.log)压根不生成——连写日志那一步都死了。换回没混淆的同一个版本,一切正常。上一版(1.6.8)用同样的配置混,也好好的。同工具、同配置、同运行时,偏偏这次坏。回头看,这种"每个环节看起来都对"的故障最磨人。

一开始我认定是新代码。1.6.9 恰好动过 SFTP 队列(传输完自动清理),嫌疑最大。可把代码回退几个版本重新编译、重新混淆,还是坏,甚至退到几个月前的代码重混也坏。代码退了,毛病还在——这说明问题不在"哪行新加的",而在代码形态跟混淆器的组合上。

接着上二分法。拿历史版本两两对比混,最后锁在两个相邻版本之间:一个混完正常,另一个混完必坏,两者 diff 只有 SftpForm.cs 一个文件、五处小改动。再把改动拆成"只含单一改动"的版本逐个测,嫌疑最大的两处(异步方法里的异常过滤器)反倒是清白的,最后钉到一处不起眼的排障日志——一个 catch (OperationCanceledException ex) 里用了 ex.StackTrace。但把这行改掉,新版本混完照样坏,后面还有别的同款"触发器"。二分到这儿就贵了:每轮都得真机装一遍。

于是我把整条链搬到本地,做了个全自动复现流水线:.NET 10 SDK 编译,wine 里跑 .NET Reactor 命令行混淆,程序里加个测试钩子——设个环境变量,启动就自动连本机 sshd、自动下个文件。好坏就两条判据:日志文件有没有生成、文件有没有真落盘。一轮从十分钟压到两分钟,还能随便排列组合混淆参数。

有了流水线,直接把混淆配置排成矩阵跑:只开 NecroBit + 符号改名 + 字符串加密,正常;在上面加一条 Control Flow Obfuscation,直接炸——日志里打出 Common Language Runtime detected an invalid program;除流程混淆外全开(压缩、资源加密、prejit、防篡改、强化),也正常。凶手就是它。

根因也清楚了:流程混淆是把方法体里的执行流打乱重排,它得逐个方法改写 IL。碰上异步方法(async/await 状态机)叠复杂异常处理这种代码形态,改出来的 IL 就不合法。.NET 10 运行时校验严,执行到非法 IL 抛 InvalidProgramException;而我们的程序有全局异常兜底(本来是好设计),异常被它吃掉,外面看就是"功能无声无息地死了"。也正因为是逐方法改写,代码形态一变,踩中的方法就跟着变——旧代码没踩中,新代码刚好踩中,这才有了"上版好这版坏"的假象。

处置不复杂:混淆配置里把 Control Flow Obfuscation 关掉,其余全留(NecroBit、符号改名、字符串加密、资源加密、prejit、防篡改,逐项验过都安全)。NecroBit 已经让反编译只见空壳,流程混淆本就是锦上添花,损失可以忽略。

另外,混淆产物必须过烟测再发:装一遍、SFTP 传个文件夹、看队列跑完 + 排障日志有生成。这轮要不是有这习惯,带病上线就是用户替我们发现「下载不能用」。现在这步已经成了发「七亿批量远程管理工具」的固定动作。

还有两点是这次逼出来的:一是排障日志模块自己也得留后门,关键路径上至少让"日志写不出来"这件事可见(比如崩溃转储),否则日志和业务一起死,你连它死了都不知道;二是"同配置上版好这版坏"不等于配置没问题,混淆是逐方法加工的,代码形态一变风险就变,别凭"配置没动过"就把它排除掉。已经把 bug 报给 Eziriz 了,等 .NET Reactor 修了再评估开不开回流程混淆。

那轮用到的家伙:ilspycmd 反编译对比(混淆产物方法体全是空壳、NoInlining 标记,NecroBit 特征)、strings 双编码扫中文残留看字符串加密覆没覆盖住、wine + .NET Reactor Console 命令行批量混淆、测试钩子 + 本机 sshd + 落盘断言全自动判定。

写过这么多坑,感觉"静默失败"最毒的还不是找不到原因,是每个环节看起来都正常。这时候别猜,把判定标准做成机器能断言的(日志在不在、文件在不在),让证据自己说话。这类"看着能用、其实早死了"的毛病,之前聊批量运维那篇里也碰过,症状不一样,路子同一个。想装来玩玩的话,七亿批量远程管理工具 官网就能下。