摘要
在移动端逆向中,最让人头疼的情况之一,不是接口加了 sign,也不是参数字段多,而是:
页面明明在正常请求数据,但抓包工具就是抓不到核心包。
某东 App 搜索链路就是一个非常典型的例子。页面能正常搜索、正常翻页,但常规抓包工具始终拿不到核心请求。继续深挖后发现,问题并不是“没有请求”,而是这条链路根本不走普通抓包路径。
本文会完整复盘这条搜索链路的分析过程,包括:
为什么传统抓包抓不到搜索核心包
搜索请求的真实网络栈是什么
sign 链与 body 加密链为什么必须拆开看
为什么
decrypt failed并不意味着 sign 算错最终如何把搜索接口稳定复现出来
一、问题现象:搜索正常,但抓不到核心请求
最初的现象非常典型:
App 内输入关键词可以正常搜索
搜索结果页可以正常翻页
排序、筛选会触发页面刷新
但常规抓包工具里始终抓不到完整的核心搜索请求
这类情况通常意味着两件事:
请求没有走普通系统代理链路
App 在发包前后做了额外封装、压缩、加密或业务处理
换句话说:
抓不到,不等于没有请求。
二、为什么常规抓包工具抓不到?
1. 搜索页不是普通远程 H5
在运行时观察中,可以看到搜索相关资源来自本地路径:
file:///data/user/0/com.jingdong.app.mall/files/taronative/localTNTemp/taroNativeSpace/jdsearch/...这意味着搜索结果页并不是简单的远程网页,而是本地资源容器 + App 内部逻辑驱动的数据渲染页面。
所以如果只盯:
WebView 的
loadUrl页面内 XHR / fetch
浏览器代理链路
很多时候只能看到外围资源,看不到真正的数据主请求。
2. 搜索请求不走普通 okhttp 直发链
继续观察 App 内部对象层,能看到搜索相关请求经过了如下链路:
HttpGroupAdapterJDJsonRequestFactorycom.android.volleyPlatformNetworkStrategyV2CronetUrlStack
这说明:
搜索不是“普通 okhttp 请求”,而是京东自有封装层 + Volley + Cronet 的组合链路。
代码片段 1:Frida 观察到的关键调用栈
com.jingdong.jdsdk.network.toolbox.JDJsonRequestFactory.initJDRequestcom.android.volley.toolbox.BasicNetwork.performRequestcom.android.volley.toolbox.PlatformNetworkStrategyV2.performRequestcom.android.volley.toolbox.CronetUrlStack.performRequest
3. Cronet 对传统抓包天然不友好
很多依赖以下能力的工具,在 Cronet 面前都会明显失效:
系统代理转发
通用 SSL 中间人
简单 okhttp hook
标准 Java 网络调用链观察
所以最终表现就是:
页面在发请求,但抓包工具抓不到核心搜索接口。
三、分析方向:不要只盯网络包,要进入对象层
遇到这类问题,最有效的做法不是继续换抓包工具,而是直接观察 App 内部对象层:
functionId是怎么设置的?body是什么时候构造出来的?发包前是否被加密?
返回值在什么阶段变成明文 JSON?
一旦进入这一层,很多“抓不到包”的问题都会自动消失。
因为这时候真正重要的不是“包”,而是:
App 内部到底是怎么构造和处理请求对象的。
四、搜索相关 functionId 的定位
在广谱 hook 阶段,重点覆盖了这些位置:
HttpGroupAdapter.addHttpSetting.setFunctionIdHttpSetting.putJsonParamHttpSetting.setJsonParamsJDJsonRequestFactory.initJDRequestjava.net.URL(String)
这样做的目的,是先回答一个问题:
搜索页到底触发了哪些 functionId?
最终可以稳定观察到几类目标:
searchsearchBoxWordswitchQuery
其中:
searchBoxWord对应联想词 / 搜索框建议switchQuery更偏配置 / 开关类接口search才是真正的商品搜索主接口
也就是说,分析重点最终收敛到:
functionId=search代码片段 2:Frida 中抓到的 search 请求关键信息
[JD-SEARCH-PRECISE] ADD fid=search[JD-SEARCH-PRECISE] INIT fid=search[JD-SEARCH-PRECISE] INIT netLibType=type_cronet[JD-SEARCH-PRECISE] RESP fid=search status=200 cache=false
截图说明位 3:search 请求明文参数
[截图位置]- keyword- page- pagesize- show_posnum- exposedCount- insertedCount
五、search 请求并不是“拿到 sign 就能打”
这是这次分析里最重要的一点。
很多接口复现失败,大家第一反应都会归结成:
sign 算错了。
但某东搜索链路不是这样。
它至少包含两条独立链路:
sign 链
body 加密链
如果不把这两条分开,后面会一直判断错误。
1. sign 链负责什么?
sign 链负责生成:
stsignsv
native 入口是:
Java_com_jingdong_common_utils_BitmapkitUtils_getSignFromJni继续进到 so 内部,主逻辑会拼接如下明文:
functionId=...&body=...&uuid=...&client=...&clientVersion=...&st=...&sv=...
然后返回:
st=...&sign=...&sv=...这里最关键的一点是:
sign 使用的是明文 body。
代码片段 3:IDA 中 getSignFromJni 的核心拼接逻辑
functionId=body=uuid=client=clientVersion=st=sv=
2. body 加密链负责什么?
真正发包时,POST 里的 body 并不是刚才那份明文 JSON。
Java 路径很明确:
EncryptBodyController.encryptBody-> EncryptTool.encrypt-> zp.e.b(..., MODIFIED_BASE64)-> zp.d.b(byte[])
最终发包使用的是包装后的 JSON:
{"hdid":"...","ts":1234567890,"ridx":-1,"cipher":{"body":"..."},"ciphertype":5,"version":"1.2.1","appname":"com.jingdong.app.mall"}所以这里必须牢牢记住一句话:
sign 用明文 body,真正发包用加密后的 body。
代码片段 4:JADX 中 body 加密链
publicstatic StringencryptBody(String str) {HashMapmap=newHashMap();if (!TextUtils.isEmpty(str)) {map.put("body", str);}return EncryptTool.encrypt(map);}
strB = zp.e.d(JdSdk.getInstance().getApplicationContext(), "", true).b(map, e.b.MODIFIED_BASE64);
jSONObject.put(str2, d.b(map.get(str2).getBytes()));六、为什么一开始会返回 decrypt failed
在最初重放 search 时,即使 sign 能算出来,请求仍然失败,返回:
{"code":"604","echo":"decrypt failed"}这一步非常容易让人误判成:
sign 算错
参数缺失
URL query 不对
但实际上,这个错误已经把问题指向得非常明确:
服务端在尝试解密 body 时失败了。
也就是说,问题根本不在 sign,而在:
请求里带了
bef=1但 POST 的
body还是明文 JSON服务端按“加密 body”规则去解,自然失败
一旦这点想清楚,修复方向就很明确了:
先补 body 加密链,不要继续纠结 sign。
七、body 加密修对之后,接口行为发生了什么变化?
把 modified base64 包装逻辑补上之后,接口行为立刻发生变化:
不再返回
decrypt failedHTTP 状态码变成
200返回内容长度明显增大
说明请求已经不是卡在前置校验,而是进入了业务返回阶段
这一步非常关键,因为它说明:
body 加密链已经基本走通。
截图说明位 4:修正 body 后 HTTP 200
[截图位置]- status_code = 200- signed_query = functionId=search&st=...&sign=...&sv=...
八、响应为什么一开始看起来像乱码?
body 修好之后,新的问题出现了:
HTTP 200
但是本地脚本拿到的响应看起来像乱码
直接
json.loads(...)失败
此时很容易误判为:
sign 仍然有问题
服务端返回了业务加密内容
或者 body 仍未完全匹配
但继续观察后发现:
Content-Encoding: br看起来像是 Brotli 压缩响应。
不过真正验证后发现:
当前拿到的
response.content实际已经是明文 JSON。
也就是说:
requests/urllib3已经自动处理过压缩但 header 里仍保留了
Content-Encoding: br如果再手动解一次 Brotli,就会报解码失败
代码片段 5:响应最终确认状态
status_code = 200content_type = text/plain;charset=utf-8content_encoding = brbody_state = already_plain_jsonresp_preview = '{"HcCid3":"655", ... }'
九、最终结果:搜索接口已经可以稳定复现
到这里,这条搜索链路已经从一个“抓不到包”的黑盒状态,变成了一条完整可控的接口流程:
能定位真实主接口:
functionId=search能构造明文 body
能对 body 做正确的 modified base64 包装
能生成 sign 参数
能拿到有效业务 JSON
能提取分页字段与商品列表字段
最终拿到的核心结果包括:
curPagehasNextPagewareCountwareInfo
这意味着这条接口已经具备:
可分析
可验证
可复现
可自动化扩展
的基础条件。
十、结语
某东这条搜索链路,表面看是一个“抓包工具抓不到搜索接口”的问题,但真正拆开之后,它其实是一个非常标准的移动端逆向案例:
页面层不是普通远程 H5
网络层不是普通 okhttp
下层用了 Cronet
请求有 sign 链
body 还有独立加密链
响应还存在传输层处理差异
这类问题真正难的,从来不是“接口藏得多深”,而是:
你能不能把不同层拆开看。
一旦拆开:
哪一层在构造请求
哪一层在做签名
哪一层在加密 body
哪一层在处理响应
就会越来越清楚。
逆向里最重要的,不是工具数量,而是分层能力。
--
如果后续继续整理,我会把这条链路的:
搜索翻页逻辑
结果列表字段抽取
排序 / 筛选参数变化
sign 分支差异分析
继续单独拆出来写。
夜雨聆风