「剥开那层壳」ThinkPHP 加密面板重装与全量解密替换记

记录智简魔方(ZJMF)云面板 3.9.14 的一次抢救式重装:重装原因、全量解密替换中的问题,以及最终收敛为一键离线包的过程。

加密商业面板软件的源码上锁、行为不透明,出现故障后难以定位。本文记录一次「把壳剥开」的完整过程。

一、为什么要重装:壳里的问题越补越多

智简魔方(ZJMF)云面板 3.9.14 的内核是 ThinkPHP 6.0.3。商业授权模式下,核心控制器、模型与中间件均被 ionCube 编码加密,缺少授权扩展无法运行,也无法阅读和修改源码。加密对运维带来三个直接问题:日志报错对不上源码、单行逻辑无从修改、排障只能停留在行为层面猜测。此前我们使用 crack .so 加授权校验破解的方式维持运行,问题逐步累积:crack 曾把 vendor/topthink/think-helper/src/helper.php 替换成加密版,框架辅助函数被二次加密,基础函数行为不可信;授权校验逻辑藏在加密代码里,接口行为与授权状态耦合,日常维护成本高。

crack .so 是在加密层外再套一层拦截,可用,稳定性差;版本升级或环境变化会引发成串问题。这次重装的目标是把「破解」这个变量从系统里移除:重装官方 3.9.14 获得干净的加密基线,再做全量解密替换,授权逻辑改用纯 PHP 离线方案,彻底摆脱 crack .so。

二、重装之后:三个变化

官方 3.9.14 重装完成后,出现三处变化。

admin 路径改变。 面板后台入口换了新路径,旧地址直接失效,收藏夹、探活脚本、文档里的链接全部要改。

数据库密码改变。 所有依赖旧密码的连接串、备份脚本、监控查询全部失效,必须重新同步新密码。

数据回灌。 从备份 cloud_db_20260826_204239.sql.gz 恢复数据库,恢复后核对:1274 台 VM、7 个节点、62 条迁移记录,与预期一致。

gunzip < cloud_db_20260826_204239.sql.gz | mysql -ucloud -p'<新密码>' cloud

三处变化之外,核心问题是重装回来的是官方 ionCube 加密代码。此前 crack 得到的明文成果全部失效,要继续使用明文,需要把 3.9.14 重新解密。后续工作为全量解密加全量替换,依次经过解密、修复、替换、验证四个环节。

三、全量解密:壳剥开了,坑才刚露头

解密工具能解开 ionCube 编码,解码不等于还原,产物中存在各类语法错误和运行期陷阱。这些问题静态检查不易发现,多数在运行后才暴露。

1. 137 个文件:extends \think\... 前导反斜杠

解密工具在 137 个文件中把基类引用写成了带前导反斜杠的 extends \think\...,语法检查直接报错。逐文件手改工作量过大,改用批量脚本按模式清理:

# 匹配 extends \think\ 并去掉前导反斜杠
grep -rl 'extends \\think\\' app/ | xargs sed -i 's#extends \\think\\#extends think\\#g'

137 个文件一次处理完成,随后执行全量语法检查确认。批量替换前先做 dry-run,列出匹配到的文件与行数,确认后再落盘,避免误伤正常代码。

2. 16 个文件:extends app\BaseController 命名空间问题

另有 16 个文件的基类写成 extends app\BaseController,缺少前导反斜杠,在 app\controller 等命名空间内被解析为当前命名空间下的相对类名,指向不存在的类。逐一改为正确引用后,语法检查通过。

3. PluginModel 常量占位符

PluginModel 中的常量仍是解密工具留下的占位符,插件模型读到的是错误值。占位符的特点是代码可运行、数据错误。补充真实常量定义后,插件相关逻辑恢复正常。

4. Request extends \think\Request

应用自定义的 Request 类需继承 ThinkPHP 的 Request,才能与框架容器正常衔接。解密产物中的继承声明有误,手工修正后,请求对象注入恢复正常。

5. AdminRouteMiddleware 缺 return,后台直接 500

后台路由中间件 AdminRouteMiddleware 缺少一个 return,请求链在中间件处中断,admin 路由返回 500。这类错误在日志中表现为无明确原因的失败,需要逐行对照中间件源码才能定位,属于静态可读、运行即断的典型问题。补上返回语句后恢复。

6. LicenseMiddleware 变量名错误

授权中间件 LicenseMiddleware 中变量名写错,授权校验读到的值始终错误。修正变量名后,中间件按预期放行。

7. LoginController 的前导反斜杠

LoginController 同样修正了 extends \app\BaseController 的前导反斜杠写法,登录链路的基类引用恢复正常。

七类问题中,一类批量解决,其余逐个定位。实际操作中,我们按以下顺序处理:先运行 php -l 全量语法检查,扫清静态可发现的错误;再启动服务做接口冒烟,遇到 500、白屏、数据不符时,回到源码逐行对照。静态检查覆盖语法,冒烟测试覆盖行为,两道检查完成后这些问题全部暴露。结论明确:解密之后、上线之前,两项检查缺一不可,否则问题会在正式流量中触发。

四、授权处理:用纯 PHP 离线方案替代 crack

解密解决的是源码可读性,授权才是加密真正锁定的部分。这次不再使用 crack .so,改用纯 PHP 离线授权:把授权相关函数全部替换为桩实现,使面板在任何环境下都判定自身授权合法,且授权请求不出本机。stub(桩)保留函数名、固定返回值,内部不再执行真实的联网校验。选择纯 PHP 离线方案的另一层考量是:桩代码可读、可控、可随包迁移,不依赖特定版本的 .so,也不受环境变化影响。

  • 桩替换授权核心函数:get_license_info、compareLicense、get_license_data、LicenseLogic 全部用桩实现替代;
  • 离线授权:纯 PHP 返回授权信息,到期时间设为 2099 年;
  • 断网授权:把授权域名写入 /etc/hosts 指向 127.0.0.1,授权请求不出本机;
  • 禁用 .so:crack 用的扩展直接禁用,php-fpm 重启后卸载,不再加载。
echo "127.0.0.1 <授权域名>" >> /etc/hosts

控制器层面,逐个替换为 3.9.14 解密版,原文件一律保留 .bak_enc_3922 备份:

CloudController、LoginController、CheckMiddleware、UpgradeController、BwController、DiskController、ConfigController、FloatipController、ImageController、ThroughController、LicenseController。这批替换覆盖了面板的核心控制器,每个文件都先备份再覆盖,回滚路径始终保留。

同时,vendor/topthink/think-helper/src/helper.php 恢复明文版——crack 曾把它替换成加密版,这次一并归位。

五、主控 cron 事故:负载 106,sshd 被饿死

重装和替换期间,主控还发生一次 cron 事故。事故与面板本身无关,过程具备运维参考价值。

现象是负载升至 106,内存耗尽触发 OOM,sshd 也因资源耗尽无法响应。排查定位到自定义调度服务 crontab-web.service:该服务读取 /home/zjmf/dashboard/crontab-web.json 拉起定时任务,问题出在 JSON 中以下两行:

  • think cron:每分钟执行,一次 fork 16 个进程,且从不退出;
  • think five_minute:每 5 分钟执行,同样从不退出。

PHP 任务未设超时、不主动退出,定时器逐轮叠加,最终拖垮宿主机。这类事故的特点在于单个任务看似无害,叠加后进程数量指数级膨胀。修复分三步:

# 1. 删掉 JSON 里 think cron / think five_minute 两行
# 2. 重启调度服务
systemctl restart crontab-web
# 3. 清掉已堆积的 think cron 进程
pkill -9 -f 'think cr[o]n'

最后那条命令写 [o] 而不是 o,是正则小技巧:让 pattern 匹配不到自己所在的命令行,避免误杀执行命令的 bash。

六、php-fpm 诊断:给耗时留一面镜子

排查接口耗时时,nginx 日志只覆盖到 HTTP 层,php-fpm 内部的慢请求与耗时需要单独记录。诊断配置加在 php-fpm 池配置中:

slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 2s
access.log = /var/log/php-fpm/access.log

request_slowlog_timeout=2s 使超过 2 秒的请求写入慢日志;access log 用 %dus 记录 fpm 侧耗时,两者配合可对应用时位置与时长。nginx 日志记录入口到出口的时间,fpm 的 %dus 记录脚本真实执行时间,两者相减可区分耗时在 PHP 内部还是外部。另一个问题是 systemd 默认 PrivateTmp=true,php-fpm 的 /tmp 为私有挂载,写入 /tmp 的日志不落盘。日志路径必须放在 /var/log/php-fpm,否则配置正确但无日志输出。

七、验证:数字会说话

全部替换完成后逐项验证,数字比感觉可靠:

项目结果
login201
/v1/clouds200,0.18-0.21s
/v1/bws200
/v1/disks200
/v1/areas200
/v1/authorize_info200
/v1/authorize200
/v1/license_module200
images 页会挂 90s(调节点代理,遗留问题)

登录返回 201,云主机列表、带宽、磁盘、区域、授权信息、授权动作、授权模块七个接口全部返回 200,clouds 稳定在 0.18-0.21 秒。images 页挂起 90 秒是节点代理的问题,与授权无关,留作后续优化。

八、沉淀:一键离线包

修复完成只是第一步,可复现才是沉淀。整套成果打包后,同类迁移无需再从零开始。

zjmf_3914_offline_pack.zip(2.9MB,249 个文件)包含解码后的 app、解码后的 index.php、数据库备份、deploy.sh、upload_and_deploy.py 与 README。deploy.sh 和 upload_and_deploy.py 固化部署步骤和操作入口,README 说明每一步操作。压缩包含有完整的重建路径。

./deploy.sh

按 README 步骤执行即可。重装、解密、修复、验证最终收敛为一个 2.9MB 的包,下次同类迁移可直接复用这条流水线。

复盘要点

本次问题主要来自三个方向:加密产物不可信、备份混乱、长任务失控。对应三条纪律:

  • 加密商业软件升级/迁移,先做「全量解码 + 语法检查 + 冒烟」,不要解完直接上线。
  • 备份命名要成体系(例如 .bak_enc_3922),保留原始加密件,回滚才有依据。
  • cron 型 PHP 长任务必须设超时和主动退出,否则定时器叠加会把宿主机拖死。
标签:ThinkPHP, ionCube, 解密