当前位置:首页 > 编程开发

把停服七年的手游变成单机版:REA 逆向实战

webgou9小时前编程开发8

前言

最近拿到一个 2015 年的手游 APK——《高达战争》,百度渠道包,207MB。这类老网游的宿命都一样:服务器早就停了,域名解析都没有了,但游戏的引擎、美术、玩法逻辑还完整地躺在安装包里。这篇文章记录的是一次完整的目标:把这个包变成本地可运行的单机版,包括破解脚本加密、还原通信协议、搭本地服务、修改存档、注入兑换码,以及最后治理"概率性闪退"的全过程。

全部工作只用了 Python 标准库、jadx、adb 和一个安卓模拟器。没有 Frida,没有 IDA。

方法论上依赖的是自建的 REA(Reverse Engineering Assistant,通用逆向助手工具集)。REA 最初为逆向一款 Electron 桌面应用而建:一个纯 Python 实现的 ASAR 解包器,加上几十个单文件分析脚本——域名扫描、关键词上下文检索、IPC 渠道分析、凭据编解码器与硬编码密钥扫描等等,约定"改脚本第一行的 root 变量即可对新目标开跑"。随着项目积累,REA 从脚本集合演化成一套按目标类型沉淀的 playbook 体系:Electron 应用一条、加固壳 Cocos 游戏一条,每打完一个目标就把可复用的流程、判据和踩坑清单写回对应的 playbook。这次的项目,就是照着 REA 的套路从头走到尾,最后又把经验沉淀成了它的第三条 playbook。

一、五分钟定引擎:看文件指纹

拿到一个游戏 APK,第一件事不是打开反编译器,而是解包看两个地方:lib/ 里的 so 名字和 assets/ 的目录形态。引擎指纹互不混淆:

看哪里Cocos2d-x + LuaCocos Creator JSUnity
lib/*.solibcocos2dlua.solibcocos2djs.solibil2cpp.so + libunity.so
assetssrc/*.lua + res/src/*.jsc + _CCSettingsbin/Data + Assembly-CSharp

这个包是标准的 Cocos2d-x Lua 架构:708 个 Lua 脚本、3256 个资源文件。更有意思的是 assets/config.json 里有 simulator_screen_size 和 consolePort——这是 Cocos Code IDE 时代 runtime 的标志格式,说明这游戏原生支持桌面模拟器运行。后面会看到,这个细节成了整个本地化工作的钥匙。

二、破解脚本加密:钥匙就藏在引擎隔壁

打开 src/main.lua,满屏乱码——但头部 10 个字节是可读的 51hutongnb。这是 cocos2d-x 脚本加密的标准形态:签名头 + XXTEA 密文。

密钥在哪?不需要动态调试。cocos 引擎启动时通过 setXXTEAKeyAndSign 把密钥和签名传进去,两者都是字符串常量,必然并排躺在 libcocos2dlua.so 的只读数据区里。于是:

idx = so.find(b'51hutongnb')        # 先搜签名
seg = so[idx-300:idx+300]           # 挖前后 600 字节
# 输出: ... 923e0288f6673ff08d4bc41d8e7aea12 / 51hutong / 51hutongnb ...
#      → key = "51hutong"(8字节), sign = "51hutongnb"(10字节)

那个 32 位 hex 字符串是干扰项。真正的坑在 XXTEA 实现本身:cocos 用的是 xxtea-c 的变体,加密前会把明文补齐到 4 字节边界,然后在密文末尾追加 4 字节的原始长度。我第一版解密脚本把长度当成了前缀,解出来全是乱码。正确做法是解密后校验尾部长度字段——顺便这也是验证密钥对不对的免费手段。修正后,708 个 Lua 全部解密成明文。

三、读明文:339 个接口和一条登录链

Lua 变成明文之后,剩下的就是 grep 的艺术。几条命令下来,整个游戏的架构就清楚了:

grep -rho '"gundam/[a-zA-Z/_]*"' src --include="*.lua" | sort -u
# → 339 个服务端接口:fuben(副本)/union(军团)/pvp/kuaFuTeam(跨服)…

这是一个典型的客户端-服务器网游:HTTP POST 表单 + MD5 签名(密钥 GUNDAM_2015 硬编码在 global.lua 里),JSON 响应。登录链是:

机型检测 → 版本检查 → 服务器列表 → 登录 → 玩家初始化(返回 201 表示新号)→ 建角色 → 12 个数据同步接口 → 主城

最关键的发现藏在 system.lua:游戏按渠道号决定登录方式,而渠道 00000000_ADWUYOU 走的是 loginType=2 设备 ID 登录——完全绕开百度 SDK。也就是说,游戏自己就带着"无账号登录"的后门,只差有人把渠道号指过去。

四、本地化改造:改一个文件,重签一次名

方案变得非常薄:只改 src/system.lua 一个文件——把渠道强制为 00000000_ADWUYOU,把四个服务器地址全部指向本地——然后用 Python 的 zipfile 把它替换回 APK(顺便丢掉旧签名),jarsigner 重新签名。不需要 apktool,不需要碰引擎 so。

本地服务用 Python 标准库写一个 mock:实现启动链的 20 个接口,其余 319 个统一返回空成功兜底。几个踩过的坑值得记:

  • 模拟器连宿主机:LDPlayer 里 10.0.2.2 不通,要用宿主机的局域网 IP;

  • 阵容(zhenrong)数据结构里 formMap 缺失会在战力计算里报空指针——这种报错从 logcat 的 TestinAgent: reason 里直接能看到完整 Lua 堆栈;

  • guide.stepId="end" 可以跳过强制新手引导。

验证结果:登录、建号、教学战斗、主城、仓库全链路跑通。一款死了七年的网游,在本地活了过来。

五、转折:一份现成的离线包

故事到这里有个转折:又拿到一份别人做好的"离线版"APK。对这种包,正确姿势不是从头分析,而是先 diff 文件清单:

find assets/src -type f | sort > new.txt
diff old.txt new.txt
# → 多出 13 个文件,全在 offline/ 目录,且是明文

读下来发现是一套相当工整的实现:它在场景启动前劫持了 system 模块的全部设备/渠道方法,替换了 network/http_manager.lua,把所有 HTTP 请求转进同进程的 offline/server.lua——一个 300 多行的"进程内游戏服务器",读写本地 JSON 存档,带原子提交和备份。Lua 异常全部被 xpcall 接住写进 offline_error.log。这个细节后面排查闪退时帮了大忙:错误日志为空,直接证明崩溃不在脚本层。

六、改存档与造兑换码

离线包的存档是 /data/data/<包名>/files/gundam_offline_save.json。root 模拟器上直接拉出来用 Python 改:VIP、等级、改造、潜能、关卡星级、每日次数,都是一目了然的字段。唯一要注意的是重装应用后 uid 会变,push 回去必须 chown 回新 uid,否则游戏读不到。

更有意思的是兑换码。游戏原本就有 CDKey 系统(主城 → 头像 → 兑换码按钮),离线包里也实现了处理器。往里面加自己的码只需要几行 Lua:

if code=='VIP666' then
    s.player.vip = math.max(n(s.player.vip), 10)
    r.csSyncMap = {PlayerOut={vip=s.player.vip}}  -- 客户端即时刷新
    return r
end

两个细节:失败提示要复用客户端已有错误码的文案(14001=码不存在、14006=已使用过),自定义 message 客户端根本不显示;通关类的码要"只补未通关的关卡",避免覆盖玩家已有星级。最终交付了四个码:VIP666(VIP10)、SHUX666(等级/改造/潜能满配+百万资源)、TG666(直接通关前 10 关)、GD666(三合一),全部实测可用。

七、最难的部分:概率性闪退

离线包有个遗留问题:某些关卡会闪退,时闪时不闪。这是整个项目里最像侦探工作的一段。

第一步,取证。常规 logcat 抓不到现场,就写了个持续监控脚本挂在 logcat 上,维护一个"崩前 40 行"的环形缓冲,SIGSEGV 一出现就落盘。配合 root 从 /data/tombstones/ 拉 tombstone,凑齐了证据链:

证据推论
offline_error.log 为空不是 Lua 层错误
signal 11 空指针,线程名 GLThread,PC 落在转译层代码区x86 模拟器上 ARM 转译层的渲染崩溃
同一动画 7 次安全加载、7 次出现在崩溃前概率性问题,"拉黑特定资源"方案否决
两个包的资源 MD5 完全一致排除资源损坏
崩溃时内存映射已达 0xffffdfff(4GB 顶满),2301 个映射32 位地址空间耗尽是诱因

结论:2015 年的 32 位游戏 + 大量 Cocos Studio 骨骼动画图集,在 x86 模拟器的转译层上执行,地址空间被挤满后 GL 分配拿到空指针,老引擎不判空直接崩。真机上反而不会发生——转译层是模拟器特有的。

第二步,缓解。根因在环境层修不掉,就打组合拳把概率压下来:每场战斗的敌人统一为同一机体(单套骨骼动画)、上限 3 台;启动时强制低画质(RGBA4444,纹理内存减半);最后在模拟器上跑一个 root 看门狗——进程死亡 2 秒内自动重启,战斗状态有持久化、自动恢复到崩溃的那一场。修复前同类战斗约 50% 崩溃率,之后连续压测 6 场零崩溃。

八、方法论:REA 的三段式闭环

回头看,这次的项目之所以顺,是因为每一步都踩在 REA playbook 已经验证过的判据上。REA 的核心是一个三段式闭环:

① 指纹识别定路线(引擎/壳/容器一张对照表,前五分钟决定后面五小时)→ ② 通用脚本组开路(字符串提取、上下文检索、批量解密、文件清单 diff——全是"改一行 root 就能跑"的单文件 Python 脚本)→ ③ 证据驱动的深挖(hook、tombstone、日志相关性,先建证据链再动手)

本项目的完整流水线就是这三段的具体化:

指纹定引擎 → 字符串区挖密钥 → 明文 grep 还原逻辑 → 对新包先 diff 再通读 → 崩溃用 tombstone + 日志相关性说话

几条个人经验:

  • 解包后的前五分钟决定后面五小时——指纹对了,后面全是套路;

  • 密钥、签名、配置这类字符串常量不用逆向二进制,它们总在 so 的字符串区里抱团出现;

  • 对"别人改过的包",diff 文件清单是最快的理解方式;

  • 概率性崩溃一定要先建证据链再动手,黑名单式的直觉方案(拉黑 war_131)就被数据直接否决过;

  • 修不掉的环境问题,把"降低概率 + 快速自动恢复"做扎实,体验上等于修好了。

尾声

最终的交付物是一个 220MB 的离线 APK:VIP/属性/通关进度可调,内置自助兑换码,模拟器上带自动复活。一款停服七年的游戏,现在拔掉网线也能从建号玩到主城。

项目收尾时,整个流程被写回了 REA 的 playbook 库(《Cocos Lua 网游 APK 本地化》篇,与前两篇并列):XXTEA 长度藏在密文末尾、LDPlayer 连宿主要用局域网 IP、重装后存档要 chown 回新 uid、兑换码失败提示要复用客户端错误码、概率性崩溃的三层缓解组合……下次再遇到同类目标,这些坑就可以直接跳过了。工具会过时,但带着判据的流程沉淀不会——这大概就是维护一套 REA 这种"逆向助手"最大的意义。

最后是老生常谈但必须说的:这类操作属于对已停运作品的学习研究与技术存档,不触碰支付、不复原服务器数据、不分发原始资源。游戏代码与美术的版权归原厂商所有。


扫描二维码推送至手机访问。

版权声明:本文由知了博客发布,如需转载请注明出处。

本文链接:https://www.webgou.info/?id=724

分享给朋友:

“把停服七年的手游变成单机版:REA 逆向实战” 的相关文章

Linux平台编程新手入门 C语言中的移位操作

C语言中的移位操作,内容不多…

增加标题栏按钮

增加标题栏按钮: 1.创建GUID,用GUDI工具创建. 2.增加一个键值,键名为刚创建好的GUID HKEY_LOCAL_MACHINE/Software/Microsoft/Internet Explorer/Extensions/{GUID} 3.增加图标,名称 ButtonText - 按钮…

Android查询联系人信息正确操作方法

在Android操作系统中,编程爱好者们可以根据自己不同的需求对其进行一些修改来轻松的完成各种功能。这一开源系统对于各个手机厂商来说无疑是一个发展良机。我们在这里就为大家介绍一个Android查询联系人信息的方法,以帮助大家解决一些问题。下面的Android查询联系人信息的功能主要是实现查询联系人的…

Android录音失真具体解决方案

在6410+WM9714的Android平台上测试MIC IN录音功能,出现一个BUG。在该平台声音播放是完全正常的,但是录音后再播放刚录的声音,会有失真,同样的录音文件在电脑上播放也一样,说明是 Android录音失真的问题。后来通过打印9714的寄存器,发现录音频率是8000HZ,放音频率是4…

MySQL 备份和恢复

本文讨论 MySQL 的备份和恢复机制,以及如何维护数据表,包括最主要的两种表类型:MyISAM 和 Innodb,文中设计的 MySQL 版本为 5.0.22。目前 MySQL 支持的免费备份工具有:mysqldump、mysqlhotcopy,还可以用 SQL 语法进行备份:BACKUP TAB…

总线标准

微机中总线一般分为内部总线,系统总线,外部总线.内部总线是微机内部各外围芯片与处理器之间的总线,用于芯片的一级互联;而系统总线是各个插板与系统板之间的总线,用于插件板的一级互连;外部总线是微机和外部之间的总线,微机做为一种设备通过该总线和其它设备进行信息和数据交换,它用于设备一级的互连.从广义上说,…

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。