把 PCL 关掉的下载功能接回来:fixpcl 的逆向分析与 IL 手术实录 - UpXuu's blog
发布于
10 月 20 日,PCL(Plain Craft Launcher 2)发了新正式版,版本号 2.99.0.0。上一个正式版是四天前的 2.14.0.0,2.14 直接蹦到 2.99,本身就透着不对。Release 说明很短:
为遵守相关法律法规,PCL 不再提供 MC 下载功能,非常抱歉 qwq…… 注:与网易微软麻将无关,中国式家长很神奇吧
装原版、装整合包,没了。对一个启动器来说这刀确实切在正中间。我把这些功能接回来了,项目叫 fixpcl,这篇文章讲怎么做的。
限制是怎么实现的
PCL 的仓库是”部分开源”,源码大部分公开,官方二进制由公开仓库加上一批没公开的补丁构建。这个设定让分析有个很舒服的起点:直接 diff 就行。
2.14.0.0 到 2.99.0.0 的代码差异只有 79 行新增、302 行删除,核心就一个新符号:
' ModSecret.vb
Public Const RestrictMcDownload As Boolean = True
所有下载入口都挂在它上面,比如整合包安装,解析完压缩包、确认类型之后是这么一段:
If RestrictMcDownload Then Throw New Exception("为遵守相关法律法规……")
Select Case PackType
Case 0 : Return InstallPackCurseForge(File, Archive, …)
Case 1 : Return InstallPackHMCL(File, Archive, …)
' ……一共 7 条安装链路
End Select
问题出在 Const。VB 编译器对常量做编译期折叠:If True Then Throw,判断直接没了,变成无条件抛异常;If RMD Then A Else B,只留 A;被 Throw 或 Return 挡住的语句,编译器当不可达代码,物理删掉。
反编译官方 exe 确认了这一点。二进制里没有开关字段,限制点后面的代码(文件对话框流程、安装链路调度)真的不存在了。这不是被注释掉的功能,是被裁掉的肢体。
所以”找个字节把 1 改成 0”这条最省事的路直接堵死。没有 1,也没有 0。
但器官还在,只是线被拔了
死代码消除是有边界的:它只删方法内不可达的语句,不删”暂时没人调用”的方法定义。我在官方二进制里逐个清点了一下,发现几乎全部下游实现都完整活着:
- 7 个
InstallPack*安装方法(CurseForge / HMCL / MMC / MCBBS / Modrinth / 带启动器压缩包 / 纯压缩包),一个没少 ModDownload.DlClientListLoader,Mojang 官方版本清单加载器,因为启动流程还在用它所以没被清掉- 整合包安装的闭包类,连
File字段和完整的安装调用体都在——VB 把闭包分配提升到了方法开头,位置在守卫之前,躲过了裁剪 - 资源页的
Install_Click处理器
说白了,作者把 5 个总闸换成了抛异常,楼的线路一根没动。所以补丁的活儿不是”恢复功能”,是”重新合闸”。
顺带的发现:官方版和开源版差的远不止一个开关
ModSecret 这个文件在公开仓库里第一行就写着”由于包含加解密等安全信息,本文件中的部分代码已被删除”,里面全是空壳。官方二进制里的真实实现则是:内嵌的微软 OAuth ClientId、腾讯云 CLS 埋点地址、赞助码的 ECDSA 验签公钥、以 Base64 内嵌的 CurseForge API key、完整的更新和联网配置系统。
还有个发现对后面很关键:官方分发包虽然是单 exe,但 MeloongCore、PCLCS 等 13 个托管依赖是以字节数组的形式内嵌在 PCL.Resources.resources 资源里、运行时经 AssemblyResolve 加载的。这个机制后面救了我一次。
六处 IL 手术
用 dnlib。语义蓝本是 git 历史里的 2.14.0.0 源码,每一处补丁都对照原始代码翻译回 IL,不自己发明逻辑。六处补丁用了五种手法,难度大概是这么排的:
P5 最简单,单指令替换。 修改页的 CardMinecraft.Visibility = Collapsed 编译出来是 ldc.i4.2(枚举值编译期内联),改成 ldc.i4.0 就是 Visible。这是六处里唯一一条”改一个字节”能搞定的,严格说都不算手术。
P3 是整段换一条。 下载页初始化里有个名叫”关闭下载”的假加载器,13 条指令,worker 直接抛异常。整段删掉换成一条 ldsfld DlClientListLoader 就行,类型严丝合缝——真加载器一直活着。
P2 是控制流重建,也是坑最多的一处。 在整合包安装方法里,把 throw 三连原地改写成 ldloc PackType + switch,再插入 7 个 case 块,每块就是照 2.14 源码逐句翻译:打日志、压参数、调用 InstallPackXxx、存结果、leave 跳出 try 区。开发时在这儿翻了一次车:那条 throw 是外层 Try…Finally 的末条指令、被异常处理表的边界引用着,我第一版直接删了它,dnlib 写盘时报错说找到不属于任何方法的指令。因为边界悬空了。改成”改写”而不是”删除”才过去。另一个坑是 switch 的目标数组,拼接后它指向了没真正进方法体的指令对象,也是加了全模块悬空引用扫描才抓到。
P1 是方法体整体重建。 无参安装入口的方法体清空重写:弹文件对话框、把选中的文件存进幸存的闭包、RunInThread 后台安装。最麻烦的是引用导入——主模块里没有任何 Dialogs.SelectFile 的引用(调用点早被编译器清了),只能先从 exe 内嵌资源里把 MeloongCore.Wpf.dll 抠出来加载,再用 dnlib 的 Importer 生成跨程序集方法引用。就是前面说的那个 AssemblyResolve 机制救的场。
P6 是分支重建。 资源详情页恢复”点击安装”按钮,插入类型判断,THEN 分支用两组 ldftn + newobj 构造双委托调三参 ToListItem。这处反编译回读时跟 2.14 源码长得一模一样。
P7 是方法体桩化,禁用自更新。 这处是分析过程中最让我后怕的发现:官方 exe 的更新路径不止设置页那个按钮,还有服务器经 ServerConfig 下发的 slientupdate 命令——静默自更新,没有任何提示。也就是说就算我把下载功能接回来了,作者(或者任何能控制那台腾讯云 COS 的人)推一条命令,就能把所有补丁版用户悄悄换回受限版。好消息是所有更新路径最终都汇进 ModSecret.UpdateStart 一个执行器,方法体换成单条 ret,一个切口覆盖全部向量。整合包打包用的 DownloadLatestPCL 是独立方法,不受影响;公告和下载镜像这些联网功能也照常。
验证
对二进制动手,验证链不能省。写盘前先全模块扫描一遍分支目标、switch 目标和异常处理边界,确认没有悬空引用——前面说的那个 bug 就是这个扫描抓到的。写盘之后重新加载产物,逐项断言六处补丁的标记,比如 UpdateStart 的方法体必须恰好是一条 ret。
更狠的一步是用 ILSpy 把整个补丁版反编译出来人工比对。反编译器其实是最严格的 IL 读者,它读不顺的 IL 大概率真有问题。实测六处全部还原出了等价的高级代码。
最后是确定性:同一个官方 exe 修多少次,产物的 SHA-256 都是同一个值(2f8bd4f4…),说明输出完全由输入决定,也说明那份被逐条验证过的二进制就是工具每次交付的东西。边界情况也测了中文加空格路径、干净目录模拟双击,以及故意让中间步骤失败——任何一环不过都会中止并且不产出文件。
单文件分发
最后的工程问题是怎么把工具送到别人手上。它依赖 dnlib,1.15 MB,又要求”任何设备双击即用、不装任何东西”。最后做出来是个 cmd 和 PowerShell 各读一半的合体文件:
┌ cmd 引导段(纯 ASCII 十几行)┐
│ powershell -Command "以显式 │ ← cmd 只解析这一段,
│ UTF-8 读取自身 #PS_BODY 之后 │ GBK 代码页下也绝对安全
│ 的内容并执行" 然后 exit │
├ #PS_BODY ────────────────────┤
│ PowerShell 修复主体(中文) │ ← PowerShell 用显式 UTF-8
│ dnlib.dll(Base64 内嵌) │ 读取,全程无损
└──────────────────────────────┘
两个运行时读同一个文件的不同部分,谁也别踩谁的编码坑。中间踩的坑基本都是 Windows 特色:PowerShell 5.1 会把无 BOM 的脚本按 GBK 解析,中文全变乱码,脚本必须存成带 BOM 的 UTF-8;PowerShell 访问静态字段得写 $Op::X,$Op.X 拿到的是 null,报错还很难懂;here-string 的闭合标记不包含末尾换行,拼文件时能吞掉一个换行;还有个最隐蔽的——git 默认会把文本文件的 CRLF 规范化成 LF,fixpcl.cmd 被悄悄改掉了 534 个字节,我是后来实测下载镜像的哈希才发现,最后用 .gitattributes 把工具文件钉成字节精确存储。
最后
工具在 GitHub - ImUpXuu/fixpcl,README 里绿色徽章点一下就能下载,配了两个国内可达的镜像和 SHA-256 校验,仓库里有完整的原理、分析和验证文档。下载的游戏文件全部来自 Mojang 官方 CDN,跟 PCL 原来的行为一样。
有几句得说在前面:这东西仅限个人使用,别把补丁后的 exe 拿去分发。PCL 作者面对的压力是真实的,把缝合怪散播出去既违反他的许可,也把本来灰色的东西推进黑色地带。另外,如果你真的在玩 Minecraft,请买正版——工具恢复的是下载通道,不是盗版的理由。
纯从技术上说,这个项目最有意思的地方是它把”编译器就是混淆器”这件事摆到了台面上:一个 Const True 加死代码消除,效果上等于一次轻量混淆。但只要下游实现还留在二进制里,单点守卫就挡不住有源码对照的逆向。对作者来说这可能也是个值得记下的结论——真要防,光裁掉入口是不够的。
不过说实话,我更希望永远不用再做一次这种分析。