吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 949|回复: 18
上一主题 下一主题
收起左侧

[Android 原创] E听说安卓 APP 全逆向过程完整复盘

  [复制链接]
跳转到指定楼层
楼主
James1105 发表于 2026-3-22 14:28 回帖奖励

[原创] E 听说安卓 APP 全逆向过程完整复盘

免责声明
本文仅用于合法授权范围内的客户端研究、协议分析与逆向学习。为避免对线上业务造成影响,文中对固定密钥、敏感常量、部分可直接复现生产请求的细节做了脱敏处理,重点分享分析思路、定位方法、验证过程和工程化复刻路径。

一、前言

这篇帖子想分享的不是“某个 sign 怎么算出来”,而是我完整逆向 E 听说安卓 APP 的全过程,作为一名高二学生,对于逆向的理解还是非常的浅,本文仅分享逆向的过程供大家学习,如果有不对或者有更好的方式欢迎大家指出!

很多逆向帖子最后只停留在:

  • 找到一个加签函数
  • 写个登录脚本
  • 然后结束

但这次项目我实际走得更深一些,最后落出来的东西已经不是单个 PoC,而是一套围绕 E 听说业务协议构建的自动化控制台,覆盖了:

  • 登录与设备校验
  • 作业列表获取
  • 资源包下载与解密
  • 题目与答案解析
  • 评测请求还原
  • XML 分数提取
  • OSS 上传
  • m/audio/sync-v2 成绩同步
  • 声音复刻与 TTS

也就是说,这次逆向真正想解决的问题不是“能不能发一个包”,而是:

能不能把 APP 里的核心业务链路,从登录到结果回传,完整复刻出来。

二、项目整体总结

先给整个仓库做一个一句话总结:

AUTO-ETS 本质上是一套围绕 E 听说业务协议搭出来的自动化中台,把安卓端的认证、作业、资源、评测、上传、同步几条链路全部串起来了。

从能力拆分上,它大致可以分成 5 层:

  • 最小协议验证层
    负责验证签名、登录和设备校验是否已经跑通
  • 资源处理层
    负责资源包下载、共享缓存、解压、题型解析
  • 评测复刻层
    负责鉴权、StreamingRecognize 编码、返回解析、XML 分数提取
  • 语音生成层
    负责声音复刻与 TTS 合成
  • 编排控制层
    负责把登录、作业、资源、评测、上传和任务队列串成统一流程

所以这篇文章我会按下面这个顺序讲:

  1. 先用 Reqable 抓包,看协议长什么样
  2. 再用 BlackDex32 脱壳提取原始 dex
  3. 用 JADX 分析请求基类、JNI 入口和关键业务类
  4. 再用 Frida 动态验证静态分析结果
  5. 最后对照本地验证工程,看这些结论是怎么真正复刻落地的

三、分析环境与工具

这次逆向我主要用了下面这些工具:

  • Reqable
    抓包、改包、比对多次请求差异
  • BlackDex32
    对目标 APP 运行时脱壳,提取真实业务 dex
  • JADX
    反编译脱壳后的原始 dex,搜索类名、方法名、字符串
  • apktool
    辅助看清单、资源和原包结构
  • Frida
    Hook JniUtils、请求构造、请求发送、评测相关函数
  • HAR 记录
    用于还原音频评测上传的完整调用顺序
  • 本地验证工程
    对抓到和反到的结论做协议复现与工程化验证

这里特别强调一下 BlackDex32

本文后面提到的 1/sources10/sources 这些目录,并不是直接对 APK 静态解包得到的,而是:

  1. 先让 APP 在设备上真实运行
  2. 通过 BlackDex32 提取脱壳后的原始 dex
  3. 再把这些 dex 丢进 JADX 进行反编译

这一点非常关键,因为如果目标 APP 带壳,直接解包通常只能看到壳层、壳入口或者不完整类;真正的业务实现往往要靠运行时提取的 dex 才能看全。

四、整体逆向路线


这条路线的核心思想很简单:

  • 抓包解决“请求外形”问题
  • 脱壳解决“业务代码可读”问题
  • 静态分析解决“调用关系”问题
  • 动态 Hook 解决“最后一锤定音”问题
  • 本地验证层解决“结论是否可用”问题

五、先从抓包入手:Reqable 第一轮观察

1. 为什么先抓登录

我一般做这类项目,第一刀都先切登录,因为登录具备几个优点:

  • 参数少
  • 返回明确
  • 成败路径清晰
  • 最容易判断有没有加签、设备校验、风控字段

这次抓登录很快就能看出,请求并不是简单表单,而是一个类似:

  • body
  • head
  • sign

的统一结构。

同时能观察到:

  • password 仍然出现在业务参数层
  • time 每次请求会变化
  • sign 会跟着变化
  • body 看起来更像 Base64,而不是 AES 这类密文

这一步直接把问题方向从“是不是密码加密”修正成了“是不是整包签名”。

2. 继续抓设备校验与作业列表

接着我又抓了:

  • user/device
  • m/ecard/list
  • g/homework/list

这几类接口。

这里出现了两个非常重要的观察结论:

  1. 不同业务接口复用了相同的请求包装方式
  2. 登录失败后的“设备绑定”不是单纯弹框逻辑,而是完整的服务端链路

也就是说,登录流程实际更像这样:

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. 抓包阶段最重要的是提出正确问题

抓到这些包以后,我并没有急着去猜算法,而是先把问题列出来:

  1. sign 的输入是什么
  2. body 是密文还是编码后的明文
  3. head 自己参与不参与签名
  4. 路由名 r 是放 URL,还是也写进 body
  5. 响应头里的 Sign 是怎么验的
  6. 设备校验阶段哪些字段参与重试
  7. 作业列表和资源链路是不是同构包装

后面所有分析基本都是围绕这几个问题展开的。

六、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

如果从逆向效率出发,我建议优先顺序就是:

  1. request
  2. utils
  3. model
  4. xml
  5. 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 干的事情非常关键:

  1. 调用各子类的 setParams(),收集业务参数
  2. 自动补齐通用字段
  3. 组装成固定模板:
[{"r": "route", "params": {...}}]
  1. 对原始字符串做 Android 风格 Base64
  2. 调用 JniUtils.getSign(...)
  3. 拼出最终请求:
  • body
  • head
  • sign
  1. 请求发出时自动拼接 ?sn=...
  2. 响应回来后读取响应头 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 里面到底怎么转几圈,对协议复现来说已经不重要了。

十、登录链路:UserLoginRequestDeviceListRequest

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

这个类对应:

  • user/device

用于设备校验与设备列表获取。

这里能看到的关键字段是:

  • 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 组合后再做摘要
  • 响应签名独立存在

十二、签名链路到底是什么

这里为了避免帖子变成直接可投产的线上接口说明书,我不放真实敏感常量,而统一写成:

  • APP_PREFIX
  • APP_SUFFIX

签名过程可以抽象成:

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

登录通了以后,下一步我切到作业列表。

关键类:

  • 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 拉一下”,而是明确支持:

  • 多状态查询
  • 过期项显示
  • 音频任务过滤
  • 家长视角数据

也正因为这样,我后面在本地验证层做了并发拉取多个状态再聚合卡片,而不是只请求一个状态。

十四、资源链路:拿到列表以后,不是结束,而是刚开始

列表拿到以后,真正的业务价值在资源。

本地验证层里,这部分主要被拆成了两个能力:

  • 资源包下载与缓存
  • ZIP 密码推导与解压

1. 资源包结构

从抓包和项目实现看,一套练习通常包括:

  • 一个模板包
  • 多个内容包

也就是类似:

  • template
  • content 1
  • content 2
  • content 3

2. ZIP 密码不是固定值

这里一个很关键的点是:

资源包密码并不是一个通用固定密码,也不是明文硬编码;而是要先通过对 libmd5.so 的解包与分析定位出“加密常量”,再结合 ZIP 尾部附加信息动态推导出来。

我当时这段是分两步闭环的:

  1. 先在 libmd5.so 里顺着 ZIP 解压相关调用链往回找,确认有一段常量不是直接拿来当密码,而是作为“加密值/中间材料”参与后续计算。
  2. 再回到 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

还有:

  • wordData

这说明 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>"}

这也是为什么很多“伪上传能成功但结果不对”的脚本,本质上只是把同步接口打通了,但没有把评测结果字段真正补齐。

十九、AudioSyncRequestAudioSyncBean

1. AudioSyncRequest

这个类直接把:

  • m/audio/sync-v2

需要的业务字段摊开了。

重点字段包括:

  • 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

二十、上传链路:FilePreUploadRequestUploadFileHelper

这部分很多帖子也容易一笔带过,但实际上它是同步链路里非常关键的一环。

1. FilePreUploadRequest

这个请求对应:

  • m/file/pre-upload

它的作用是:

  • 先拿预上传 ID

2. UploadFileHelper

这个类负责:

  1. 请求上传配置
  2. 组装 OSS 表单参数
  3. 上传文件
  4. 把返回结构转成业务对象

这能说明几个现实问题:

  • 文件不是直接随便 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 在后半段特别关键

登录阶段只靠请求包就能看懂很多东西,但到了:

  • 评测
  • OSS
  • sync-v2

这段,HAR 的时间顺序和上下游关联就非常重要。

4. XML 不要只当字符串

真正有业务价值的是 XML 里映射出来的:

  • 总分
  • 多维度分
  • 词级结果
  • 拒识信息

二十四、结语

这次做完以后,我最大的感受就是:

真正高效的安卓逆向,不是上来就啃 so,也不是抓几个包就开始瞎猜,而是让抓包、脱壳、静态分析、动态验证和本地复刻这几步互相闭环。

E 听说这个项目里,最开始看起来最难的是签名;但往后走会发现,签名只是入口。真正拉开差距的是:

  • 资源语义解析
  • 评测结果还原
  • XML 分数字段映射
  • OSS 上传
  • sync-v2 闭环同步

所以如果你也在做类似项目,我建议按这个顺序推进:

  1. 先抓包,把协议层次分清
  2. 再脱壳,把真实 dex 拿出来
  3. 先啃请求基类和 JNI 桥
  4. 用 Hook 验证,少靠猜
  5. 先做最小可复现 PoC
  6. 最后再工程化、Web 化、队列化

mermaid-1774159341401.png (39.29 KB, 下载次数: 1)

mermaid-1774159341401.png

mermaid-1774159341401.png (39.29 KB, 下载次数: 3)

mermaid-1774159341401.png

免费评分

参与人数 3吾爱币 +2 热心值 +2 收起 理由
Leaf08 + 1 我很赞同!
tuoaa1 + 1 谢谢@Thanks!
tantanxin147 + 1 + 1 我高二的时候只会逆向行驶

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Leaf08 发表于 2026-3-28 22:26
能透露一下抓包的方法吗,我试过用真机挂vpn+charles的方法抓,但是估计是有证书验证,进去之后白屏,只能抓到很少量的接口
沙发
zyx168zyx 发表于 2026-3-23 18:26
3#
chenney 发表于 2026-3-23 18:48
4#
wangdongjiang 发表于 2026-3-23 19:05
感谢分享
5#
ahao914425 发表于 2026-3-23 19:45
给了思路感谢
6#
qiezi1142 发表于 2026-3-23 20:51
居然连e听说都有人搞 膜拜大佬了
7#
fzlte0 发表于 2026-3-23 22:00
学习了,谢谢。
8#
wangbaobao123 发表于 2026-3-24 07:09
可以,学习了!
9#
AIways 发表于 2026-3-24 09:19
我高中的时候还只会玩个电脑游戏
10#
tempxx 发表于 2026-3-24 09:55
太厉害了,为你点赞!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-12 04:02

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表