记录智简魔方(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.logrequest_slowlog_timeout=2s 使超过 2 秒的请求写入慢日志;access log 用 %dus 记录 fpm 侧耗时,两者配合可对应用时位置与时长。nginx 日志记录入口到出口的时间,fpm 的 %dus 记录脚本真实执行时间,两者相减可区分耗时在 PHP 内部还是外部。另一个问题是 systemd 默认 PrivateTmp=true,php-fpm 的 /tmp 为私有挂载,写入 /tmp 的日志不落盘。日志路径必须放在 /var/log/php-fpm,否则配置正确但无日志输出。
七、验证:数字会说话
全部替换完成后逐项验证,数字比感觉可靠:
| 项目 | 结果 |
|---|---|
| login | 201 |
| /v1/clouds | 200,0.18-0.21s |
| /v1/bws | 200 |
| /v1/disks | 200 |
| /v1/areas | 200 |
| /v1/authorize_info | 200 |
| /v1/authorize | 200 |
| /v1/license_module | 200 |
| 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 长任务必须设超时和主动退出,否则定时器叠加会把宿主机拖死。