[原创] E 听说安卓 APP 全逆向过程完整复盘
免责声明
本文仅用于合法授权范围内的客户端研究、协议分析与逆向学习。为避免对线上业务造成影响,文中对固定密钥、敏感常量、部分可直接复现生产请求的细节做了脱敏处理,重点分享分析思路、定位方法、验证过程和工程化复刻路径。
一、前言
这篇帖子想分享的不是“某个 sign 怎么算出来”,而是我完整逆向 E 听说安卓 APP 的全过程,作为一名高二学生,对于逆向的理解还是非常的浅,本文仅分享逆向的过程供大家学习,如果有不对或者有更好的方式欢迎大家指出!
很多逆向帖子最后只停留在:
但这次项目我实际走得更深一些,最后落出来的东西已经不是单个 PoC,而是一套围绕 E 听说业务协议构建的自动化控制台,覆盖了:
- 登录与设备校验
- 作业列表获取
- 资源包下载与解密
- 题目与答案解析
- 评测请求还原
- XML 分数提取
- OSS 上传
m/audio/sync-v2 成绩同步
- 声音复刻与 TTS
也就是说,这次逆向真正想解决的问题不是“能不能发一个包”,而是:
能不能把 APP 里的核心业务链路,从登录到结果回传,完整复刻出来。
二、项目整体总结
先给整个仓库做一个一句话总结:
AUTO-ETS 本质上是一套围绕 E 听说业务协议搭出来的自动化中台,把安卓端的认证、作业、资源、评测、上传、同步几条链路全部串起来了。
从能力拆分上,它大致可以分成 5 层:
- 最小协议验证层
负责验证签名、登录和设备校验是否已经跑通
- 资源处理层
负责资源包下载、共享缓存、解压、题型解析
- 评测复刻层
负责鉴权、StreamingRecognize 编码、返回解析、XML 分数提取
- 语音生成层
负责声音复刻与 TTS 合成
- 编排控制层
负责把登录、作业、资源、评测、上传和任务队列串成统一流程
所以这篇文章我会按下面这个顺序讲:
- 先用 Reqable 抓包,看协议长什么样
- 再用 BlackDex32 脱壳提取原始 dex
- 用 JADX 分析请求基类、JNI 入口和关键业务类
- 再用 Frida 动态验证静态分析结果
- 最后对照本地验证工程,看这些结论是怎么真正复刻落地的
三、分析环境与工具
这次逆向我主要用了下面这些工具:
- Reqable
抓包、改包、比对多次请求差异
- BlackDex32
对目标 APP 运行时脱壳,提取真实业务 dex
- JADX
反编译脱壳后的原始 dex,搜索类名、方法名、字符串
- apktool
辅助看清单、资源和原包结构
- Frida
Hook JniUtils、请求构造、请求发送、评测相关函数
- HAR 记录
用于还原音频评测上传的完整调用顺序
- 本地验证工程
对抓到和反到的结论做协议复现与工程化验证
这里特别强调一下 BlackDex32。
本文后面提到的 1/sources 到 10/sources 这些目录,并不是直接对 APK 静态解包得到的,而是:
- 先让 APP 在设备上真实运行
- 通过
BlackDex32 提取脱壳后的原始 dex
- 再把这些 dex 丢进 JADX 进行反编译
这一点非常关键,因为如果目标 APP 带壳,直接解包通常只能看到壳层、壳入口或者不完整类;真正的业务实现往往要靠运行时提取的 dex 才能看全。
四、整体逆向路线
这条路线的核心思想很简单:
- 抓包解决“请求外形”问题
- 脱壳解决“业务代码可读”问题
- 静态分析解决“调用关系”问题
- 动态 Hook 解决“最后一锤定音”问题
- 本地验证层解决“结论是否可用”问题
五、先从抓包入手:Reqable 第一轮观察
1. 为什么先抓登录
我一般做这类项目,第一刀都先切登录,因为登录具备几个优点:
- 参数少
- 返回明确
- 成败路径清晰
- 最容易判断有没有加签、设备校验、风控字段
这次抓登录很快就能看出,请求并不是简单表单,而是一个类似:
的统一结构。
同时能观察到:
password 仍然出现在业务参数层
time 每次请求会变化
sign 会跟着变化
body 看起来更像 Base64,而不是 AES 这类密文
这一步直接把问题方向从“是不是密码加密”修正成了“是不是整包签名”。
2. 继续抓设备校验与作业列表
接着我又抓了:
user/device
m/ecard/list
g/homework/list
这几类接口。
这里出现了两个非常重要的观察结论:
- 不同业务接口复用了相同的请求包装方式
- 登录失败后的“设备绑定”不是单纯弹框逻辑,而是完整的服务端链路
也就是说,登录流程实际更像这样:
flowchart TD
A[请求 user/login] --> B{服务端是否要求设备校验}
B -- 否 --> C[直接返回登录成功]
B -- 是 --> D[请求 user/device]
D --> E[提取 device_code]
E --> F[带 device_code 重试 user/login]
F --> G[成功后再请求 m/ecard/list]
3. 抓包阶段最重要的是提出正确问题
抓到这些包以后,我并没有急着去猜算法,而是先把问题列出来:
sign 的输入是什么
body 是密文还是编码后的明文
head 自己参与不参与签名
- 路由名
r 是放 URL,还是也写进 body
- 响应头里的
Sign 是怎么验的
- 设备校验阶段哪些字段参与重试
- 作业列表和资源链路是不是同构包装
后面所有分析基本都是围绕这几个问题展开的。
六、BlackDex32 脱壳:拿到真正能看的原始 dex
1. 为什么要先脱壳
如果你直接对 APK 做静态解包,很多时候会遇到这几种情况:
- 入口类很薄
- 关键业务类缺失
- 全是壳层逻辑
- 字符串搜索结果不完整
所以这次我先通过 BlackDex32 把运行时的 dex 提取出来,再把提取结果交给 JADX。
2. 我实际拿到的 dex 结构
本地整理后的目录就是现在仓库里的:
1/sources
2/sources
3/sources
4/sources
5/sources
10/sources
其中真正带大量 com/ets100 业务代码的主要是:
| dex 目录 |
com/ets100 文件量级 |
作用判断 |
3/sources |
1360 |
大量外围业务、网络、通用模块 |
4/sources |
3132 |
本次逆向主战场,secondary 核心实现集中区 |
10/sources |
23 |
对 secondary 的补充拆分 |
这一步的重要性在于,它让我迅速确定:
classes4.dex 是最值得先啃的
classes3.dex 适合做外围关联
classes10.dex 可以做交叉验证
3. 为什么 classes4.dex 是主战场
因为 4/sources/com/ets100/secondary 下面几乎把这次关心的主干都囊括了:
request
utils
model
xml
ui
这些目录基本对应了:
- 请求怎么构造
- 签名在哪做
- 数据对象长什么样
- XML 分数怎么映射
- 页面操作怎么触发业务请求
七、先看包,再看类:secondary 主干结构怎么拆
1. secondary 顶层目录
4/sources/com/ets100/secondary 顶层目录主要包括:
model
receiver
request
statistic
tutorial
ui
utils
widget
xml
如果从逆向效率出发,我建议优先顺序就是:
request
utils
model
xml
ui
2. request 子包全景
secondary/request 下面有 21 个子包,这里把真正值得关心的先列出来:
| 子包 |
文件数 |
主要职责 |
本次价值 |
baserequest |
3 |
请求共同父类、统一封装、响应验签 |
最高 |
loginregister |
18 |
登录、设备、用户信息 |
最高 |
homework |
70 |
作业列表、详情、结构、分数、提交 |
最高 |
ppr |
13 |
评测资源映射、标注文本 |
最高 |
point |
15 |
单题评分同步、分数查询 |
最高 |
sync |
6 |
批量成绩同步、明细同步 |
高 |
uploadfile |
7 |
预上传、文件上传辅助 |
高 |
resource |
25 |
资源相关接口 |
高 |
level |
27 |
分级阅读评分链路 |
中 |
composition |
8 |
作文业务 |
中 |
dub |
15 |
配音业务 |
中 |
readwrite |
6 |
读写专项 |
中 |
word |
12 |
单词业务 |
中 |
如果你问我“第一遍应该看哪些”,答案就是:
baserequest
loginregister
homework
ppr
point
uploadfile
八、核心基类:BaseRequest 把整个协议钉死了
1. 这个类为什么必须先看
我在 JADX 里搜:
getSign
sendPostRequest
user/login
很快就定位到了:
com.ets100.secondary.utils.JniUtils
com.ets100.secondary.request.baserequest.BaseRequest
这两个类其实就把整个统一协议框架定下来了。
2. BaseRequest 实际做了什么
BaseRequest 干的事情非常关键:
- 调用各子类的
setParams(),收集业务参数
- 自动补齐通用字段
- 组装成固定模板:
[{"r": "route", "params": {...}}]
- 对原始字符串做 Android 风格 Base64
- 调用
JniUtils.getSign(...)
- 拼出最终请求:
- 请求发出时自动拼接
?sn=...
- 响应回来后读取响应头
Sign 再调用 JniUtils.getResponseSign(...) 做验签
3. 静态分析给出的关键结论
从 BaseRequest 里我确认了几个特别重要的细节:
- 业务体不是普通 JSON 对象,而是数组包一层
r 这个路由名被写进了 body
params 是最终参与签名的业务参数集合
body 是 Base64 编码结果,不是 AES 结果
- 响应也有签名校验
换句话说,只要读懂了 BaseRequest,就读懂了这个 APP 里相当大一部分统一协议。
九、JNI 入口:JniUtils 很短,但价值极高
1. JniUtils 结构
这个类本身非常短,只暴露了几个 native 方法:
getSign(String str)
getResponseSign(String str, String str2)
getShareAppIds(boolean z)
并且加载了:
System.loadLibrary("md5")
2. 这说明了什么
这说明:
- 请求签名确实进了 so
- 响应验签也走 native
- 但 native 并不是不可跨越的障碍
因为只要能 Hook 到 getSign() 的入参和返回值,native 里面到底怎么转几圈,对协议复现来说已经不重要了。
十、登录链路:UserLoginRequest 和 DeviceListRequest
1. UserLoginRequest
这个类直接告诉我登录业务参数有哪些:
phone
password
device_code
version
device_name
ets_ticket
再加上 BaseRequest 自动注入的:
use_https
system
global_client_version
sn
sign_response
token
ets_app
就组成了完整登录请求的参数层。
2. DeviceListRequest
这个类对应:
用于设备校验与设备列表获取。
这里能看到的关键字段是:
ticket
login
user_id
ets_ticket
这也是为什么我后面在本地验证层做登录复刻时,会把设备重试作为标准流程而不是异常分支。
3. 登录链路的对象流
flowchart LR
A[UserLoginRequest] --> B[BaseRequest.getBody]
B --> C[固定模板 raw JSON]
C --> D[Android Base64 body]
D --> E[JniUtils.getSign]
E --> F[RequestBody + Head]
F --> G[sendPostRequest]
G --> H[UserLoginRespone]
这个对象流里要抓住两个层次:
十一、Frida 动态验证:把“怀疑”变成“证据”
静态分析虽然已经很接近答案,但真正让我把链路彻底钉死的,是动态 Hook。
我这次主要 Hook 了这些点:
JniUtils.getSign
JniUtils.getResponseSign
UserLoginRequest
DeviceListRequest
BaseRequest.sendPostRequest
这里用的是一份自写的 Frida Hook 脚本,专门负责打印 JNI 入参、请求原文和最终签名。
1. Hook 目标
我真正想看的是下面这些内容:
- 路由名
- 原始
params
- Base64 前的 JSON
- Base64 后的
body
time
- 最终
sign
- 响应头
Sign
- 本地计算的响应验签值
2. Hook 伪代码
hook(JniUtils.getSign, (value) => {
print("sign_input =", value)
})
hook(JniUtils.getResponseSign, (bodyBase64, sn) => {
print("response_sign_input =", bodyBase64)
print("response_sn =", sn)
})
hook(BaseRequest.sendPostRequest, (requestBody, body, sn) => {
print("sn =", sn)
print("body_before_base64 =", body)
print("request_json =", requestBody)
})
3. 动态验证后确认的关键点
Hook 结束以后,基本就能确认:
body 不是加密,是编码
- 编码方式是 Android 风格 Base64
- 请求签名不是只对业务参数做摘要,而是对时间和
body 组合后再做摘要
- 响应签名独立存在
十二、签名链路到底是什么
这里为了避免帖子变成直接可投产的线上接口说明书,我不放真实敏感常量,而统一写成:
签名过程可以抽象成:
def build_payload(route, params):
raw_body = f'[{{"r": "{route}", "params":{json_compact(params)}}}]'
body = android_base64_default(raw_body.encode("utf-8"))
time_value = current_millis()
sign = md5(APP_PREFIX + str(time_value) + body + APP_SUFFIX)
return {
"body": body,
"head": {
"pid": APP_PREFIX,
"time": str(time_value),
"version": "1.0",
},
"sign": sign,
}
1. 真正容易翻车的不是哈希,而是格式
逆向到这里,最容易出错的地方其实不是算法本身,而是这些看似细小的格式问题:
- 最外层是数组不是对象
"r": " 的空格位置
"params": 的拼接格式
- Base64 是否保留 Android 的换行语义
- 时间精度是不是毫秒
sn 是否参与业务参数层
很多“明明公式对了但就是不通”的情况,都是因为这些细节没有完全对齐客户端。
十三、作业链路:HomeworkListV2Request
登录通了以后,下一步我切到作业列表。
关键类:
这个类暴露出的关键字段有:
parent_account_id
status
limit
show_offline
show_old_homework
get_to_do_count
get_to_overtime_count
check_pass
only_parent
version
get_audio_task
这说明列表链路不是单纯“给 token 拉一下”,而是明确支持:
- 多状态查询
- 过期项显示
- 音频任务过滤
- 家长视角数据
也正因为这样,我后面在本地验证层做了并发拉取多个状态再聚合卡片,而不是只请求一个状态。
十四、资源链路:拿到列表以后,不是结束,而是刚开始
列表拿到以后,真正的业务价值在资源。
本地验证层里,这部分主要被拆成了两个能力:
1. 资源包结构
从抓包和项目实现看,一套练习通常包括:
也就是类似:
template
content 1
content 2
content 3
2. ZIP 密码不是固定值
这里一个很关键的点是:
资源包密码并不是一个通用固定密码,也不是明文硬编码;而是要先通过对 libmd5.so 的解包与分析定位出“加密常量”,再结合 ZIP 尾部附加信息动态推导出来。
我当时这段是分两步闭环的:
- 先在
libmd5.so 里顺着 ZIP 解压相关调用链往回找,确认有一段常量不是直接拿来当密码,而是作为“加密值/中间材料”参与后续计算。
- 再回到 ZIP 文件尾部,结合固定标记和中间片段,把 native 里定位到的常量和包尾材料一起还原成最终密码。
伪代码大致如下:
secret = recover_encrypted_constant_from_native("libmd5.so")
tail = read_zip_tail(zip_file, length=336)
assert contains_magic(tail, ["MSTCHINA", "EPLAT"])
segment = extract_middle_segment(tail)
material = combine(secret, segment)
x = md5_upper(material)
password = upper(x + md5(x))
3. 资源解析不是“解出来就完事”
真正难的不是下载资源包,而是把资源包里的题目结构转成统一数据结构。
项目里已经针对几类常见题型做了解析,例如:
collector.read
collector.3q5a
collector.picture
这一步意味着项目已经从“协议复现”走到了“业务语义复现”。
也就是说,不只是能把文件下回来,而是能继续知道:
- 题目是什么类型
- 标准答案在哪里
- 文本内容如何提取
- 后续该用什么文本去做 TTS
十五、评测资源映射:SetPprResRequest
很多人做到这里会忽略一件事:评测不是直接拿题目文本就能发。
APP 里真正的评测前置资源映射在:
request/ppr/SetPprResRequest
它做的事情是:
- 输入一批
entity_id + order
- 或者输入
material_id
- 拿回后续评测要使用的
res_id
这一步非常关键,因为如果资源映射错了,后面:
- 评测引擎参数
- 同步时的
resource_id
- 关联到的题目
都会错位。
十六、评测链路:utils/point 才是真正的大头
1. utils/point 为什么要重点啃
secondary/utils/point 下面有 63 个文件,是这次逆向中另一个核心区。
这里主要包括:
- WebSocket / gRPC 评测调用
- 重试管理
- 结果处理
- XML 解析
- 文件上传
- 分数换算
其中最关键的类是:
EvalResultProcessor
EtsEvalWebSocket
EDUAiWebSocket
EtsPointManager
2. EvalResultProcessor 是评测后半程的总中枢
这个类负责:
- 接收评测返回的
sid/xmlStr/resultKey/meepoKey
- 把 XML 落盘
- 解析 XML 成
EtsXmlScoreBean
- 写回
pointScore/realScore/reject
- 生成
mark_trace_id
- 组织 XML、音频、附属文件上传
可以说,前半段是协议问题,后半段基本都收敛到了 EvalResultProcessor。
十七、XML 语义层:EtsXmlScoreBean
很多人拿到 XML 字符串就满足了,但如果真想把同步链路做出来,还必须继续看:
secondary/xml/EtsXmlScoreBean
它持有的核心分数字段包括:
currentScore
accuracyScore
fluencyScore
integrityScore
standardScore
stressScore
还有:
这说明 XML 不只是用来出一个总分,它里面还承载了:
- 多维度评分
- 词级别结果
- 拒识信息
- 后续展示与同步需要的明细
十八、HAR 还原完整音频评测上传链路
这部分是我后面重新回到 HAR 去确认的。
这部分我当时是结合 HAR 记录和本地分析笔记一起对照出来的。
通过 HAR 和源码交叉验证,链路大致如下:
flowchart LR
A[g/set/ppr-res] --> B[g/set/mark-txt]
B --> C[StreamingRecognize / WebSocket]
C --> D[得到 sid 和 xmlStr]
D --> E[ossEvaluateUploadStsToken]
E --> F[上传 mp3/xml/spx]
F --> G[m/audio/sync-v2]
1. 这里真正被确认下来的关键字段
不是“上传了音频”这么简单,而是这些关键字段最终都被确认了来源和作用:
sid
xmlStr
score
score_detail
mark_trace_id
upload_file_url
detail_file_url
spx_file_url
2. mark_trace_id 的真实来源
这一步非常关键。
结合 EvalResultProcessor 和 HAR,可以确认:
mark_trace_id 不是随便填一个 UUID,而是和评测返回的 sid/traceId 强相关,结构可以抽象成 {"engine":"<sid>"}。
这也是为什么很多“伪上传能成功但结果不对”的脚本,本质上只是把同步接口打通了,但没有把评测结果字段真正补齐。
十九、AudioSyncRequest 和 AudioSyncBean
1. AudioSyncRequest
这个类直接把:
需要的业务字段摊开了。
重点字段包括:
homework_id
resource_id
set_id
entity_id
order
score
correct
upload_file_url
detail_file_url
score_detail
spx_file_url
mark_trace_id
2. AudioSyncBean
这个类不是网络请求,而是同步前的总装 DTO。
它负责:
- 保存单题同步所需字段
- 生成业务要求的
score_detail JSON
这个点特别重要,因为很多人会以为 score_detail 是随便拼的,其实不是。
APP 侧已经把它抽成了稳定结构,里面固定包含:
total_score
fluency_score
accuracy_score
integrity_score
standard_score
category
repeat_mode
version
rate_scale
二十、上传链路:FilePreUploadRequest 和 UploadFileHelper
这部分很多帖子也容易一笔带过,但实际上它是同步链路里非常关键的一环。
1. FilePreUploadRequest
这个请求对应:
它的作用是:
2. UploadFileHelper
这个类负责:
- 请求上传配置
- 组装 OSS 表单参数
- 上传文件
- 把返回结构转成业务对象
这能说明几个现实问题:
- 文件不是直接随便 PUT
- 上传前通常要先拿配置或预上传 ID
- 音频/XML/其他文件可能走不同文件类型和目录策略
二十一、关键对象在包之间怎么流动
1. 登录链路对象流
flowchart LR
A[UserLoginRequest] --> B[BaseRequest]
B --> C[JniUtils.getSign]
C --> D[RequestBody]
D --> E[sendPostRequest]
E --> F[UserLoginRespone]
2. 评测上传链路对象流
flowchart LR
A[HomeworkListV2Request] --> B[作业卡片]
B --> C[SetPprResRequest]
C --> D[SetPprResBean]
D --> E[PaperPointBean]
E --> F[StreamingRecognize]
F --> G[EvalResultProcessor]
G --> H[EtsXmlScoreBean]
H --> I[AudioSyncBean]
I --> J[AudioSyncRequest]
J --> K[m/audio/sync-v2]
3. PaperPointBean
这个类可以看成单题运行时总状态对象,挂了:
- 题目标识
- 评测状态
- 分数字段
- 上传路径
markTraceId
xmlScoreBean
如果做动态日志,我认为比起散着打字段,直接围绕 PaperPointBean 生命周期打点更有价值。
二十二、本地验证层是怎么把安卓结论复刻出来的
这部分是整个项目最有工程味的地方。
它并不是“分析完安卓代码,嘴上说我懂了”,而是实打实地把关键链路做成了本地可验证的闭环。
1. 安卓侧逻辑到本地验证动作的映射
| 安卓侧类/包 |
本地验证动作 |
伪代码思路 |
BaseRequest |
复刻统一请求包装 |
组装 raw JSON -> Base64 body -> head -> sign |
JniUtils |
复刻签名与响应验签 |
sign = digest(prefix + time + body + suffix) |
UserLoginRequest |
复刻登录与设备重试 |
login -> device -> retry login |
HomeworkListV2Request |
并发拉取作业列表 |
for status in statuses: fetch(); merge() |
SetPprResRequest |
上传前资源映射 |
entity_id + order -> res_id |
EvalResultProcessor |
XML 分数提取与业务映射 |
xml -> scores -> score_detail |
| 评测调用层 |
复刻 StreamingRecognize |
build frames -> send -> decode sid/xml |
AudioSyncBean |
评分明细总装 |
score + urls + detail + trace_id |
AudioSyncRequest |
单题同步回传 |
sync(score_detail, mark_trace_id, upload_urls) |
FilePreUploadRequest / UploadFileHelper |
OSS 上传 |
get token/config -> upload objects -> get urls |
| 资源包结构 |
资源下载与题型解析 |
download -> unzip -> parse collectors |
| ZIP 密码推导 |
还原解压密码 |
native 常量 + zip 尾部材料 -> md5 chain -> password |
2. 这说明了什么
说明这套本地验证层做的事情不是“随便写几个请求”,而是:
- 安卓侧先确认协议存在
- Frida 确认真实输入输出
- 本地验证层再把关键逻辑逐步工程化复刻
所以它既是逆向成果,也是工程化成果。
3. 关键伪代码:最小闭环应该怎么写
登录与设备校验
payload = build_signed_payload("user/login", login_params)
resp = post(payload)
if need_device_retry(resp):
dev = post(build_signed_payload("user/device", device_params))
device_code = extract_device_code(dev)
resp = post(build_signed_payload("user/login", with_device_code(login_params, device_code)))
作业列表聚合
cards = {}
for status in [0, 1, 2, 3, 4]:
page = fetch_homework_list(status)
for item in page:
cards[item.id] = merge_card(cards.get(item.id), item)
资源包下载与解析
for job in build_resource_jobs(card):
archive = download(job.url)
secret = recover_native_secret()
password = derive_password(archive, secret)
extract(archive, password)
bundle = parse_bundle(extracted_dir)
评测与同步
res_id = map_ppr_resource(entity_id, order)
eval_result = evaluate_audio(res_id, pcm_audio)
scores = parse_xml(eval_result.xml)
upload_urls = upload_objects(mp3, eval_result.xml, spx_like_file)
sync_result(
homework_id=homework_id,
resource_id=res_id,
score_detail=build_score_detail(scores),
mark_trace_id={"engine": eval_result.sid},
upload_urls=upload_urls,
)
二十三、我认为这次逆向里最容易踩的坑
1. 签名不对,不一定是算法不对
更常见的情况是:
- JSON 模板格式不对
- 外层数组没包
- 空格位置不对
- Base64 用成了普通 PC 风格
- 时间精度不一致
2. 不要一开始就死盯 so
如果抓包已经能告诉你请求结构,静态分析已经能告诉你谁在调用 JNI,那就应该优先解决“输入是什么”,而不是直接陷进 so 细节里。
3. HAR 在后半段特别关键
登录阶段只靠请求包就能看懂很多东西,但到了:
这段,HAR 的时间顺序和上下游关联就非常重要。
4. XML 不要只当字符串
真正有业务价值的是 XML 里映射出来的:
二十四、结语
这次做完以后,我最大的感受就是:
真正高效的安卓逆向,不是上来就啃 so,也不是抓几个包就开始瞎猜,而是让抓包、脱壳、静态分析、动态验证和本地复刻这几步互相闭环。
E 听说这个项目里,最开始看起来最难的是签名;但往后走会发现,签名只是入口。真正拉开差距的是:
- 资源语义解析
- 评测结果还原
- XML 分数字段映射
- OSS 上传
sync-v2 闭环同步
所以如果你也在做类似项目,我建议按这个顺序推进:
- 先抓包,把协议层次分清
- 再脱壳,把真实 dex 拿出来
- 先啃请求基类和 JNI 桥
- 用 Hook 验证,少靠猜
- 先做最小可复现 PoC
- 最后再工程化、Web 化、队列化