某游戏辅助完整破解复盘报告(仅用于本人学习记录)

版本:终版 v1.0 日期:2026-10-26 目标:辅游戏辅助

结果:已达成长期运行(x18 包 + 「启动后封 80 端口」),实测连续运行 1 小时以上无报错、无弹窗、无退出。 本报告只记录「怎么做到的」与「为什么这样做」,仅限逆向学习记录。


0. 一句话结论

这个 App 用「脚本运行时 + VirtualAPK 内嵌插件 + native 引擎 libmqm.so + 服务端租约」四层结构做授权。 能破解的"软"部分全在 Java 层 (到期时间、PreStart 回包、强制更新闸门、签名自校验、后台服务启动), 不能破解的"硬"部分是 native 签名的 60 秒心跳租约------它由服务端逐次裁决。 最终路线不是"伪造出一个合法的永久账号",而是:

  1. 改包解掉所有客户端硬闸门(签名自校验 / 后台 startService 崩溃 / 到期时间 / 强制更新);

  2. 让 PreStart 的 1404 在解析层被改判成 200 + 2100 年到期 + 借用真会话的 RunGuid;

  3. 脚本跑起来后封掉 80 端口,让 native 心跳拿不到服务端的明确否定 ------ 引擎拿不到"停止"结论,就一直跑。

一句话:客户端全放行,服务端全断联。


1. 目标画像

项目 值
包名 com.保密
版本 保密
targetSdk / minSdk 26 / 21
宿主框架 某知名应用(无加固、无壳、纯 Java + native 引擎)
插件机制 com.didi.virtualapk 加载内嵌 assets/app-release.apk
引擎 lib/arm64-v8a/libmqm.so(13,001,688 B,内嵌 OpenSSL)
守护进程 assets/DaemonClient.zip → com.cyjh.*.ipc.ClientService(root,app_process64)
脚本资源 assets/script.lc(6,038,916 B,魔数 eflc) / script.uip / script.atc / script.prop
脚本授权标识 script.prop 里 SelID=370620、AppID=0C74B04B-3A0E-484c-800E-F010F4FB555F、Version=1789984173
进程结构 com.*.*.*(主)、:enginfloat(引擎)、:com.zvovj.pydrd:dnebu(root 守护)
原始安装包 77,739,542 字节

assets/script.prop 与同壳的免费脚本(如 RAss)对比:免费脚本没有 SelID/AppID/Version 三个字段,商用脚本才有。这是判断"这个包是否要授权"的第一眼指标。

1.1 接口域名

DomainResult 回包:AppDomainName=http://auth1.*.com/、App2DomainName=http://app.*.com。 实际抓包全部业务走 auth1,路径清单:

复制代码
/CheckTemplate   /StartApp       /Script/PreStart   /Script/Start
复制代码
/Script/Check    /Script/Stop    /EngineHeartBeatV2 /VersionUpdate
复制代码
/Notice/List     /Statistics     /AliCloud/GetStorageToken

导出 ApiDescKeys = 8dbe4df8 / be5aa0ed / d9d54d9f / f79e1471 / 6d31c74e / 462dca7d / 07e95c1d / daa1a7eb,ApiVersionV = 81058250b7deb83d0,请求头 V=81058250b7deb83d0。


2. 防护分层矩阵(L0--L12)

层 防护点 位置 是否可绕 绕法
L0 服务端租约(60s 心跳) native libmqm.so + 服务端 否(伪造)/ 是(断联) 封 80 端口,不给"明确否定"
L1 响应签名(RSA-1024) z2/ge.OooO0o0 用服务器公钥验签 否(无私钥) 改 n4 解析层,绕开验签后的对象
L2 APK 签名自校验 SplashActivity.OooOooo()V 是 ①改包后原地覆盖 base.apk(利用 PMS 缓存安装时签名)②清空该方法
L3 反重打包参数 V z2/pc.OooOO0 对非官方 host 追加随机 V 是 无害;保留原 host 即可
L4 Android 10 后台 startService z2/dg.OooO0O0 + AppContext 是 try-catch + 延迟重试 Runnable
L5 到期时间 z2/t7.OooOO0o(J) → 字段 OooO0O0 是 方法体直写 4102444800
L6 PreStart 1404 服务端回包 是(表现层) n4.OooO0o0 里把 "Code":1404→200
L7 RunGuid 会话 1404 回包 RunGuid:null 是(表现层) 同上,替换成已知 guid 8f596916385d406c834839502ef942c7
L8 强制更新闸门 z2/h7$OooO00o.OooO0OO 是 ①更新分支后补 preStart ②或把 UpdateType/UpgradeMode 改 0
L9 试用到期惩罚 守护进程 Injector.UninstallApp/KillApp 是 改 UninstallApp 返回 1(可选)
L10 心跳失败计数 native 否 只能断网
L11 反调试 / 云手机检测 DomainResult.PhoneConfig 12 条 部分 本机未被拦;检测到也不影响
L12 加密脚本 eflc assets/script.lc 否 必须真会话密钥,无法离线解

结论:加密/签名/心跳这些"真加密"全是服务端 + native 的事;Java 层只是"传声筒 + 展示层"。 所以正确打法不是硬碰签名,而是:

  • 在"传声筒"里改结论(n4 改判);

  • 在"展示层"改时长(t7 钉死);

  • 在"网络层"让心跳收不到否定(封 80)。


3. 协议与密码学细节

3.1 请求/响应信封(Volley 路径)

请求(rd.OooOO0):参数不放 body,放 HTTP 头 :R=<下标>、V=<ApiVersionV>、Data=<base64(DES(...))>。

响应:

复制代码
{"Data":"<base64>", "R": 3}
  • Data 解密:DES/ECB/NoPadding ,密钥按 R % 8 取组;

  • 两组密钥(z2/j3.smali 静态数组):

组 8 个 key 适用
K_APP OooO00o a6cd931d 1dcf44bf 650e59d7 3460d8da 21fbt87f 317e5900 ed1139f0 c99f6f94 url 以 http://api 开头
K_OTHER OooO0OO c6bd901f 0ddf44be 950e5927 6460d8df 11fb887f 117e4900 bd113950 a99f6f97 auth2.*.com 等其余
  • 解出内层 {"SignContent":"<业务JSON>","Signature":"<base64 RSA签名>"};

  • SignContent 里的引号常被转义成 \u0022,解析前要反转义。

3.2 两把 RSA(1024 位)

用途 位置 模数特征 我们有吗
请求签名(客户端私钥,PKCS8) z2/i6.smali / z2/j3.OooOO0O() K70yV8T2TQ46...U3hb 有(apk 内)
响应验签(服务器公钥,SPKI) z2/i6.smali / z2/j3.OooOO0() vP5e0F3nj6mvoElHEpn0VxjQI...30UE 只有公钥

全盘扫 2.33 GB(apk、dex、so、plugin、zip 成员、assets)只找到客户端私钥 一份,服务器私钥不存在 。1024 位不可分解。→ 不可能伪造出一个能过原版验签的回包。

3.3 心跳(两条不同的 JWT)

请求 POST /EngineHeartBeatV2:

复制代码
body: {"Data":"<base64( JWT )>"}
复制代码
JWT header: {"alg":"MD5","typ":"JWT"}     ← HMAC-MD5,密钥在 libmqm.so
复制代码
JWT payload: {"data":"<加密串>"}
复制代码
请求头: Host / Content-Length / Accept: */* / Content-Type: application/json(无 UA)

响应:

复制代码
{"Data":"<JWT>", "R": n}
复制代码
JWT header: {"alg":"ECDSA","typ":"JWT"}
复制代码
JWT payload: {"k":4,"i":2,"s":4,"data":"<288 字节>"}
  • 请求 JWT 由 native 用内嵌 HMAC 密钥签;响应 JWT 由服务端用 ECDSA 签;

  • payload.data 只有 native 能解 ,Java 层没有任何 JWT/ECDSA 代码(grep ECDSA|JWT 只命中 classes2/z2/bx.smali 里的 TLS 套件名);

  • 引擎收到响应后自行判定租约,失败累计 3 次(60 秒/次)→ onStopScript returnCode 105。

3.4 租约的时间常数(判停秒数速查)

现象 秒数 含义
3--5 秒停 个位数 RunGuid 为空 / 无效(会话没建起来)
11 秒、2 秒停 十秒内 进程内已记住"心跳失败"状态,重启脚本不清零
180--200 秒停 约 3 分钟 心跳 3 次失败(有明确否定:1403/无效 JWT)
420 秒(7 分钟)停 420s 服务端租约到点(HeartBeatIntervalSecond=420)
封 80 后 不限 拿不到否定,引擎不累加,一直跑

4. 攻击链(阶段化,可复现)

阶段 0:静态侦察

复制代码
baksmali → smali/classes(9.4MB dex)、smali/classes2(6.6MB dex)
复制代码
解 assets:script.lc / app-release.apk / DaemonClient.zip / AdDex.4.0.1.dex
复制代码
工具:tools/dump_axml.py(二进制 AXML 解析)、tools/scan_keys.py、tools/scan_bin.py

关键发现:dex 里 SignaClass{SignContent,Signature}、BaseInfo{Data,R}、BaseResInfo{code,message} 三件套;所有解析走 z2/n4.OooO0o0(String,Class)。

阶段 1:抓不到包 → 否定 DNS/代理路线

  • Virtual Hosts / iptables DNAT / nginx 反代全试过:App 的请求从没落到我们服务器(浏览器能到,App 不能)。

  • App 的鉴权请求走 UDP 隧道/硬编码 IP 的可能性存在;结论:别在网络层浪费时间。

  • 有效做法:在 App 进程内埋点 (写 /sdcard/_xjl_req.txt、_xjl_resp.txt)。

阶段 2:埋点拿明文

  • 请求侧:z2/rd.OooOO0 里插日志(pc :: URL),后来发现心跳不走这里。

  • 回包侧:z2/n4.OooO0o0 是最佳位置(所有回包解析必经),把 "类名 || JSON" 追加写文件。

  • 结果:拿到已解密验签后的 业务 JSON(ScriptStartRunResponse、SignaClass、VersionUpdateInfo、AppStartupResponse......)。

阶段 3:签名闸门(改包必踩)

  • 原版 apk 改完重签 → 启动一闪退回桌面;

  • 对照实验:原版只重签、零改动也闪 → 说明存在签名自校验;

  • 定位:SplashActivity.OooOooo()V(4963--5515 行,高度混淆:\u06xx 字符串运行时解密 + hashCode xor + sparse-switch 平坦化);另有 z2/u8.OooO0O0 计算 MD5 与硬编码 daeeaed0d3850865bb902b6e07afd541 比较(结果被丢弃,死代码)。

  • 解法(本方案的基石) :不改系统、不装 Xposed,用原地覆盖:

    1. 正常安装官方原版(PMS 记录官方签名);

    2. root 下把改过的 base.apk 直接写到 /data/app/com.*.*.*-*/base.apk;

    3. App 自校验读到的仍是 PMS 缓存的官方签名,运行的却是改过的 dex。

    • 代价:别重启 (重启重扫会判包失效)、别卸载重装。

阶段 4:Android 10 后台 startService 崩溃

  • 现象:两进程 RuntimeException: Unable to create application ... IllegalStateException: Not allowed to start service Intent { cmp=...IpcService }: app is in background。

  • 根因:AppContext.onCreate → MqBridge.init → z2/dg.OooO0O0 → startService(IpcService),主进程处于 cached 状态被系统拒绝。

  • 修法:dg.OooO0O0 里 startService 包 try-catch + 起一个 dg$1 Runnable(3 秒后重试,最多 20 次);AppContext 的 KeepMainService startService 同样 try-catch。

阶段 5:PreStart 1404 → 表现层改判

  • 服务端对"试用耗尽"回 Code:1404、ExpireTime:0、RunGuid:null;

  • 客户端 h7$OooO0O0.OooO00o 解析后:只有 Code==200 才设到期、回调 TRUE;1404/1403 直接冻结流程。

  • 修法:z2/n4.OooO0o0(String,Class) 里对 ScriptStartRunResponse 做字符串改判:

复制代码
"Code":1404          → "Code":200
复制代码
"ExpireTime":0,      → "ExpireTime":4102444800,
复制代码
试用次数已到…          → 请求成功!
复制代码
"RunGuid":null       → "RunGuid":"8f596916385d406c834839502ef942c7"
  • 为什么改 n4 而不是伪造服务器回包 :n4 处理的是"验签通过后的明文",不需要私钥;一处改动覆盖全部 30+ 解析点。

阶段 6:到期时间钉死

  • z2/t7.OooOO0o(J) 是唯一的到期写入点(字段 OooO0O0:J,秒级绝对时间);

  • 直接改成 const-wide v0, 0xF4865700L(=4102444800,2100 年)后 iput-wide。

  • 踩坑 :方法原 .registers 3,参数 this+long 占 3 字,v0 就是 this;const-wide v0 把 this 覆盖成常量 → VerifyError: [0x5] instance field access on object that has non-reference type。 修法:.registers 5,让 p0=v2, p1=v3/p2=v4,局部 v0/v1 空出来。

阶段 7:强制更新闸门(x16 悬浮球不出现的真凶)

  • z2/h7$OooO00o.OooO0OO(VersionUpdateInfo):
复制代码
UpgradeMode == 2 && (UpdateType == 5 || UpdateType == 3)
复制代码
  → b5.OooO0oO(AppVersion)
复制代码
  → c7.OooO0oo().Ooo(context, scriptId, mode).OooO0oO(UpdateUrl)   // 启动下载
复制代码
  → ElfinFloatView.Oooo0OO()                                        // 更新悬浮窗
复制代码
  → return                                                          // ★ 不调 preStart
复制代码
否则 → 日志 "preStart(callback);  --- : " → h7.OooO0OO(callback)     // 正常启动
  • 服务端当天下发 UpdateType=5 / UpgradeMode=2 / AppVersion=1792910556 / PackageSize=6615796 / UpdateUrl=http://ajres1.../*.wqep,于是流程卡在更新分支:引擎起了(onEngineStart),但没有 ENGIN_BOOT_SCRIPT,悬浮球永不出现。

  • 两种修法:

    • x17 :n4 里把 UpdateType 5/3 → 0、UpgradeMode 2 → 0(彻底跳过更新,进度条消失);

    • x18(终版) :不动标志,只在更新分支 return 前补一次 h7.OooO0OO(callback) ------ 更新照下、进度条保留、脚本照起。

阶段 8:心跳与租约

  • 脚本起来后约 200 秒必停(returnCode 105),原因:60 秒一次心跳,3 次都被服务端明确拒绝。

  • 服务端回的是真 ECDSA JWT,payload 是 native 才能解的租约块;伪造不行、重放过期包 4--7 秒判死、挂住/504/RST 都在 180 秒判失败。

  • 唯一有效 :iptables -I OUTPUT -p tcp --dport 80 -j DROP,让心跳发不出去 → 收不到否定 → 引擎不累加失败 → 一直跑。

    • 必须用 DROP ,REJECT 会回 RST 被立即判失败;

    • 只封 80(业务全在 80),443 不动,其它 App 不受影响。

阶段 9:交付形态

  • dist\x18.apk(MD5 0FC0FEEDF39F52506DFD5F6C82EEDF62,78,581,779 B);

  • dist\端口开关7.apk(581,135 B,三个按钮:封 80 / 放行 80 / 刷新状态);

  • 流程:开网 → 覆盖 base.apk → 点启动(看更新进度、等悬浮球)→ 进游戏 → 点「封 80」。


5. 最终补丁清单(x18)

# 文件 方法 改动
1 z2/t7.smali OooOO0o(J)V .registers 5 + 恒写 0xF4865700
2 z2/n4.smali OooO0o0(String,Class)Object .registers 3→6;插入 zlog.OooO0O0 改判 + 回包日志(try-catch 包裹)
3 z2/zlog.smali 新增类 OooO00o(path,text) 追加写;OooO0O0(json,Class) 做 1404→200 / 到期 / RunGuid / 文案替换
4 z2/rd.smali OooOO0(Context,Object,String,Map)V 插入请求日志(调用方类名 :: URL)→ /sdcard/_xjl_req.txt
5 z2/h7$OooO00o.smali OooO0OO(VersionUpdateInfo)V 更新分支 return 前补 h7.OooO0OO(callback)

打包链:smali assemble -a 26 → repack.py(替换 classes.dex,跳过 META-INF)→ SignApk.java(apksig v1+v2+v3,test.keystore / dwh / 123456)。

注:classes2.dex 只在需要改 z2/dg.smali(startService 修复)时才重编;x18 的 classes2 与原版完全一致。


6. 版本演进表(走过的路)

版本 关键改动 结果
x t7 到期钉死 能进界面,但启动卡「服务器在线验证中」(VerifyError)
x2 t7+zlog+n4+pc 请求埋点 pc 埋点导致崩溃(同 x5)
x3 t7+回包埋点 成功抓到真实 PreStart 明文
x4 h7 伪造整包 JSON 显示 26731 天,但「该辅助软件不会运行」(旧密钥解不了 eflc)
x5/x5b x4+埋点 x5 崩;x5b 可用
x6 t7+b7 tokenKey 常量 1002 root 进程异常终止(改错字段)
x7 t7+回包日志 1404 停止
x8 h7 内置当天真钥 弹窗+105
x9 n4 层改判 1404 进度:Code 200
x10 +RunGuid 替换 弹窗消失,脚本能跑,7 分钟停
x11 +OnceTryMinute 525600 无效
x12 SetHeartBeatTime 极大值 无效(改错层)
x13/x13b TemplateResponse 心跳间隔 无效
x14 ge 验签恒 true 配合服务端方案用
x15 +rd 请求日志 x10 等效
x16 +守护 UninstallApp + 9999 悬浮球不起(撞上强制更新)
x17 更新标志改 0 悬浮球起、进度条消失
x18 x15+更新分支补 preStart 更新+启动并行,进入终态方案

7. 复现手册(从零到跑起来)

7.1 环境

  • JDK 21、Python 3.11+、smali/dexlib2/util/guava/jcommander/antlr-runtime jar、apksig;

  • 目标机:已 root(Magisk)+ 云手机可用 iptables。

7.2 步骤

复制代码
# 1) 装官方原版(必须第一步,签名要进 PMS)
复制代码
# 2) 生成改包
复制代码
python build_x18.py            # 产物 dist/**辅助_x18.apk
复制代码
# 3) 覆盖 base.apk
复制代码
su
复制代码
cat /sdcard/Download/x18.apk > $(pm path com.kgyy.ttmy.zxzy | sed s/package://)
复制代码
# 4) 清日志后启动
复制代码
rm -f /sdcard/_xjl_*
复制代码
am start -n com.kgyy.ttmy.zxzy/com.cyjh.elfin.ui.activity.SplashActivity
复制代码
# 5) 等脚本跑起来(boot 里出现 EnginSdk.start()),进游戏后
复制代码
iptables -I OUTPUT -p tcp --dport 80 -j DROP

7.3 判定表

boot 日志 含义 处置
只有 onEngineStart 更新分支卡住 用 x18 或 x17
scriptStartRunResponse.Code ----- 1404 改判没生效 确认装的是 x10 以上
有 EnginSdk.start() 但 3--5 秒 105 RunGuid 没塞进去 检查 zlog 的 RunGuid 替换
约 200 秒 105 心跳三次失败 尽快封 80
封 80 后仍在跑 正常 收工

8. 复盘:走过的弯路(下次别踩)

  1. Virtual Hosts / hosts / nginx 反代对鉴权请求无效 ------ 浏览器能到,App 不能;别在 DNS 层耗时间。

  2. 伪造响应不可行 ------ 服务器私钥不在包里,1024 位 RSA 分解无望;libmqm.so 里的 ANY PRIVATE KEY 只是 OpenSSL 实现字符串。

  3. 改包必然触发签名自校验 ------ 但只重签也会闪,说明还有暗桩;最终靠"原地覆盖 base.apk"利用 PMS 缓存绕过,比 patch 干净。

  4. .registers 必须按参数字数留足 ------ this+long 是 3 个字,.registers 3 时 v0 == this,const-wide v0 直接炸出 VerifyError。

  5. 手写 smali 的 if-ne / if-eq 极易写反 ------ 端口开关 APK 曾因此所有按钮都执行"封 80"。

  6. try 块标签必须精确 ------ .catch {:try_start .. :try_end} 但标签叫 :try 时会报 "label hasn't been placed"。

  7. 日志文件可能是 GBK ------ _xjl_boot.txt 用 UTF-8 读会报 invalid,要用 GBK 解。

  8. 封网必须 DROP 不能 REJECT ------ RST 会被立刻判失败;DROP 静默丢包才能让心跳"悬空"。

  9. Android 10 后台 startService 会崩 ------ 任何"主进程 cached 时 startService"都会 IllegalStateException,得 try-catch + 重试。

  10. 改包后不能重启、不能卸载重装 ------ 重启会让 PMS 重扫签名判包失效。

  11. 心跳改错层白费 ------ SetHeartBeatTime 是给引擎的;Java 那层 60 秒是 TemplateResponse 的 HeartBeatIntervalSecond,但改了也没用,因为真正的判定在 native。

  12. 服务端每天/每场密钥不同 ------ ScriptEncryptKey 按"设备+时段"发,旧密钥解不了新 eflc 脚本,内置密钥的离线包只有当天有效。


9. 防御侧建议(如果我是厂商)

让我想想


10. 附录

10.1 关键代码地图

功能 位置
响应验签 z2/ge.OooO0o0(String,String)Z
回包解包 z2/kc.OooO0Oo(String)Object → n4.OooO0o0 → ge
PreStart 回调 z2/h7$OooO0O0.OooO00o(Object)V
到期写入 z2/t7.OooOO0o(J)V(字段 OooO0O0:J)
版本更新闸门 z2/h7$OooO00o.OooO0OO(VersionUpdateInfo)V
后台服务启动 z2/dg.OooO0O0(Application,...)V
心跳发送 native(libmqm.so),POST /EngineHeartBeatV2
惩罚(杀游戏/卸自身) 守护 dex com/cyjh/event/Injector.UninstallApp / KillApp
签名自校验 com/cyjh/elfin/ui/activity/SplashActivity.OooOooo()V
插件加载 com/didi/virtualapk/PluginManager.loadPlugin(File) ← mobi/oneway/export/e/a.smali:417

10.2 产物清单

文件 字节 MD5
dist/x18.apk(终版) 78,581,779 0FC0FEEDF39F52506DFD5F6C82EEDF62
dist/端口开关7.apk 581,135 ---
原始包 77,739,542 ---

10.3 日志指纹

复制代码
/sdcard/_xjl_boot.txt    运行链路(onEngineStart / ENGIN_BOOT_SCRIPT / Code / onStopScript 105)
复制代码
/sdcard/_xjl_req.txt     请求链路(调用方类名 :: URL)
复制代码
/sdcard/_xjl_all_resp.txt 回包(类名 || 明文 JSON)

终局:客户端全放行,服务端全断联。 客户端负责"能启动、不弹窗、不到期";服务端负责在断网后"无从否证"。

相关推荐
Go Work1 小时前
npm 卡在 Step 7 七分钟,翻上去四行才看到 E404
经验分享·笔记
Hello World . .1 小时前
【嵌入式学习笔记 Day 1】
linux·数据结构·笔记
小向是个Der1 小时前
P02_你的变量住在哪里_教学文档
经验分享·笔记·stm32·单片机
叼烟扛炮1 小时前
Linux 学习笔记(三十一):线程概念、底层分页机制与初体验
linux·运维·服务器·笔记·学习·1024程序员节
星恒随风1 小时前
Linux Socket UDP编程(一):手写UDP回显服务器与客户端,从socket、bind、recvfrom到网络字典服务器完整实战
linux·服务器·网络·笔记
凉、介2 小时前
深入理解 ARMv7-M | 异常/中断处理
笔记·学习·嵌入式·arm
蒸蒸yyyyzwd2 小时前
工具、力扣、中间件学习笔记
笔记·八股
深圳老胡2 小时前
液面探测功能的硬件方案对比:原理、优缺点与选型建议
笔记·单片机
凉、介2 小时前
深入理解 ARMv7-M|处理器模式与寄存器
笔记·学习·嵌入式·arm