版本:终版 v1.0 日期:2026-10-26 目标:
辅游戏辅助结果:已达成长期运行(x18 包 + 「启动后封 80 端口」),实测连续运行 1 小时以上无报错、无弹窗、无退出。 本报告只记录「怎么做到的」与「为什么这样做」,仅限逆向学习记录。
0. 一句话结论
这个 App 用「脚本运行时 + VirtualAPK 内嵌插件 + native 引擎 libmqm.so + 服务端租约」四层结构做授权。 能破解的"软"部分全在 Java 层 (到期时间、PreStart 回包、强制更新闸门、签名自校验、后台服务启动), 不能破解的"硬"部分是 native 签名的 60 秒心跳租约------它由服务端逐次裁决。 最终路线不是"伪造出一个合法的永久账号",而是:
-
改包解掉所有客户端硬闸门(签名自校验 / 后台 startService 崩溃 / 到期时间 / 强制更新);
-
让 PreStart 的 1404 在解析层被改判成 200 + 2100 年到期 + 借用真会话的 RunGuid;
-
脚本跑起来后封掉 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,用原地覆盖:
-
正常安装官方原版(PMS 记录官方签名);
-
root 下把改过的
base.apk直接写到/data/app/com.*.*.*-*/base.apk; -
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$1Runnable(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(MD50FC0FEEDF39F52506DFD5F6C82EEDF62,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. 复盘:走过的弯路(下次别踩)
-
Virtual Hosts / hosts / nginx 反代对鉴权请求无效 ------ 浏览器能到,App 不能;别在 DNS 层耗时间。
-
伪造响应不可行 ------ 服务器私钥不在包里,1024 位 RSA 分解无望;
libmqm.so里的ANY PRIVATE KEY只是 OpenSSL 实现字符串。 -
改包必然触发签名自校验 ------ 但只重签也会闪,说明还有暗桩;最终靠"原地覆盖 base.apk"利用 PMS 缓存绕过,比 patch 干净。
-
.registers必须按参数字数留足 ------this+long是 3 个字,.registers 3时v0 == this,const-wide v0直接炸出 VerifyError。 -
手写 smali 的
if-ne/if-eq极易写反 ------ 端口开关 APK 曾因此所有按钮都执行"封 80"。 -
try 块标签必须精确 ------
.catch {:try_start .. :try_end}但标签叫:try时会报 "label hasn't been placed"。 -
日志文件可能是 GBK ------
_xjl_boot.txt用 UTF-8 读会报 invalid,要用 GBK 解。 -
封网必须 DROP 不能 REJECT ------ RST 会被立刻判失败;DROP 静默丢包才能让心跳"悬空"。
-
Android 10 后台 startService 会崩 ------ 任何"主进程 cached 时 startService"都会 IllegalStateException,得 try-catch + 重试。
-
改包后不能重启、不能卸载重装 ------ 重启会让 PMS 重扫签名判包失效。
-
心跳改错层白费 ------
SetHeartBeatTime是给引擎的;Java 那层 60 秒是TemplateResponse的HeartBeatIntervalSecond,但改了也没用,因为真正的判定在 native。 -
服务端每天/每场密钥不同 ------
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)
终局:客户端全放行,服务端全断联。 客户端负责"能启动、不弹窗、不到期";服务端负责在断网后"无从否证"。