news 2026/10/3 4:29:10

App签名参数逆向实战:从抓包到Frida Hook再到RPC封装

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
App签名参数逆向实战:从抓包到Frida Hook再到RPC封装

做爬虫或者接口自动化的兄弟,多多少少都会撞上签名参数。以小红书为例,你翻接口请求列表时会看到x-s、x-t、x-s-common这几个常客,而在部分端上还会冒出一个x-mini-signature。这玩意儿每次请求都在变,你要是直接忽略,后端大概率回你一个k=456之类的错误码。本文不打算把整加密库从头到脚扒一遍,只把针对这类请求签名的完整分析链路走一遍:从抓包定位参数,到 jadx 静态找线索,再到 Frida 动态把函数拖出来看个清楚,最后用 RPC 的方式把签名函数变成外部可调用的接口,让业务脚本和 App 内部的加密逻辑彻底打通。

这套流程适用于大多数“请求参数里带动态签名”的 App 逆向场景,不局限于某一家。文章里所有代码都是实操过的写法,你可以直接抄作业,但建议还是先理解每一步在干什么,毕竟真实环境里函数名、包名都会变,方法才是最有价值的。

1. 从一次抓包发现 x-mini-signature 开始

1.1 环境配置:Android + Charles + Frida 一套到底

先把基本环境准备好,我的惯用组合是:

  • 一台 Android 9 或 10 的真机(模拟器也能用,但有些 App 有模拟器检测,真机省事)
  • Charles 4.x 配合手机代理,专门看 HTTPS 明文流量
  • Frida 14/15/16 都行,关键在于frida-server要和电脑端的frida版本完全一致
  • Python 3.8+,装好frida-tools和frida模块

这里重点说几个容易踩的坑。

第一,手机必须能正常连代理,并且 Charles 上要装好 CA 证书。现在的 App 普遍做了证书校验,你光装证书可能不够,很可能需要配合一个类似“JustTrustMe”之类的 Xposed 模块,或者用 Frida 主动绕过校验证书。不过证书这块不是本文重点,我们就假设已经能看到明文请求了。

第二,frida-server的架构必须对齐。常见 Android 设备的 CPU 一般是 arm64-v8a,少部分老设备是 armeabi-v7a。你可以用adb shell getprop ro.product.cpu.abi确认一下,再下载对应架构的frida-server推到手机里:

adb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server &

电脑端执行frida-ps -U,如果能看到设备上跑的进程列表,说明连接畅通。万一提示unable to connect,先检查frida-server和电脑端frida版本是否一致,这是最常见的问题。

1.2 观察请求:签名参数和明文参数的对应关系

Charles 里过滤掉无关域名,找到某个业务接口。比如搜索接口/api/sns/v1/search/notes,查看请求 body,通常里面带着业务参数:keyword、page、page_size之外,还有一个x-mini-signature。

先别急着找算法,我习惯先做几个小实验:

  1. 不管什么请求,x-mini-signature都会出现吗?
  2. 相同参数连续请求两次,签名值一样吗?(验证是否有随机因子)
  3. 改动某一个业务参数(比如page从 1 改成 2),签名变不变?
  4. 把签名参数删掉再发一次请求,后端返回什么?

这些实验能快速告诉我们:签名和哪些请求数据耦合,以及签名是否依赖时间戳或随机数。比如你换个参数它就变,说明签名大概率的计算范围包含请求参数;如果同样的请求参数发两次签名还不同,那大概率混入了时间戳或者一次性 nonce。

把这些点记录下来,后面分析时会省很多力气。我见过不少人一上来就反编译找函数,结果搞了半天不知道在看哪个方法,因为根本没确认这个签名对应的具体请求上下文。

2. 静态分析定位签名入口

2.1 jadx 打开安装包,顺着字符串找线索

把 APK 拖进 jadx-gui,等反编译完成之后,直接全局搜索x-mini-signature。

这一步基本能定位到代码里的使用点。你可能会看到类似这样的 Java 代码:

public final class ApiRequest { public static void addSign(Map<String, String> params) { params.put("x-mini-signature", SignProvider.generateSign(params)); } }

有时候签名参数名会被拆开拼接,比如"x-mini-" + "signature",所以搜索时可以换个关键词,比如搜x-mini、signature、甚至直接搜generateSign。

如果一个 App 做了代码混淆,这类字符串多数还在,因为保险起见服务端要按固定 key 取参数,key 名基本不会混淆。找不到的话,再考虑通过抓包得到的签名值关联搜索值字符串(例如在 APK 里搜你抓到的某个签名片段,但那通常没用,因为签名是动态生成的)。

接着要看SignProvider.generateSign到底做了什么。用 jadx 点击进入这个类,可能它是一个纯 Java 实现,也可能它最终调用了 native 方法。常见写法是:

public class SignProvider { static { System.loadLibrary("secsign"); } public static native String generateSign(Map params); }

如果看到native关键字,恭喜,加密核心逻辑就在 so 库里。这种情况 Java 层只是传参和取结果。

2.2 从可疑类到 hook 点的确定

静态分析最核心的产出,其实是回答一个问题:算签名的函数入口到底在哪?

不管能不能在 Java 层直接看完整个算法,我们都应该先确定一个“可 hook 的 Java 方法”。上面例子里SignProvider.generateSign(Map params)就很适合作为突破口。

但如果代码是混淆过的,方法名可能变成a.b.c.d()这种东西。这时怎么确认它就是算签名的函数?

我的做法是:在 jadx 里选中候选方法,右键Find Usage,看谁调用它、把它的返回值放到哪个参数里。如果调用者把结果put到一个 Map,并且 key 是x-mini-signature,那就基本断定了。

另外还有一个技巧:搜索与设备信息相关的关键词,比如device_id、oaid、timestamp,看看哪个函数的入参包含这些字段,同时返回String。签名函数通常长这样:

public static String a(String path, Map<String, String> header, Map<String, String> params, String deviceId) { ... }

返回的字符串通常是一段十六进制或 base64 乱码。遇到这种,就直接把它作为 Hook 目标。

3. Frida 动态 Hook:把加密函数当场扒干净

3.1 让目标函数停下来:Hook Java 方法的正确姿势

静态分析拿到类名和方法名,接下来就是 Frida 上场。

写一个最基础的 Frida JS 脚本,对目标方法进行 hook,打印入参和返回值即可:

Java.perform(function () { var SignProvider = Java.use("com.example.xhs.SignProvider"); SignProvider.generateSign.implementation = function (params) { var result = this.generateSign(params); console.log("params = " + params.toString()); console.log("result = " + result); return result; }; });

然后用命令行启动:

frida -U -f com.example.xhs -l hook.js --no-pause

-f是冷启动目标 App,--no-pause表示不要停在入口,直接跑起来。这是为了不错过启动早期可能发生的签名调用。

为什么用-f而不用attach?因为签名可能会在 App 刚启动时就被调用,比如初始化上报接口。如果你先打开 App 半天再 attach 上去,那个调用早就过去了,自然 hook 不到。所以做启动阶段的分析,一定要从spawn模式开始。

跑起来之后去 App 里手动触发一次搜索请求,你会在控制台看到类似下面的输出:

params = {keyword=美食, page=1, page_size=20} result = 5f6a2c8d91d23e3f9b8c7a1d4e5f6a70

到这里,至少能确认这个函数确实被调用了,并且返回值就是请求里的x-mini-signature。

3.2 从 Java 到 Native:so 层函数追踪

现实里,SignProvider.generateSign经常是native方法。你直接按上面的方式 hook,也能拿到 Java 层的入参和返回值。但如果想知道 native 内部算了什么,就得看 so 库。

先通过 Java hook 打印出 Java 层调用的native方法所在 lib,可以这样:

Java.perform(function () { var SignProvider = Java.use("com.example.xhs.SignProvider"); SignProvider.generateSign.implementation = function (params) { var result = this.generateSign(params); var libs = Process.enumerateModules(); libs.forEach(function (lib) { // 通常把带security/sign/crypto关键字的模块打出来 if (lib.name.toLowerCase().indexOf("sign") !== -1 || lib.name.toLowerCase().indexOf("sec") !== -1) { console.log("module: " + lib.name + " base=" + lib.base); } }); return result; }; });

拿到模块名之后,可以用Module.findExportByName直接找导出函数。但很多 so 会把敏感函数隐藏起来,不一定有导出符号。这时候更通用的做法是 HookRegisterNatives,把 Java native 方法和底层函数地址映射关系打印出来:

var RegisterNatives = Module.findExportByName(null, "RegisterNatives"); Interceptor.attach(RegisterNatives, { onEnter: function (args) { var env = args[0]; var clazz = args[1]; var methods = args[3]; // 这里需要解析 JNINativeMethod 数组,根据 methodCount 打印每个成员 }, onLeave: function (retval) {} });

这段代码解释起来篇幅不小,而且不同 Android 版本的 JNIEnv 结构差异不大,你可以拿标准的JNINativeMethod结构体去解析。一旦拿到 native 函数地址,就可以继续Interceptor.attach到那个地址,去读入参和返回值。

不过,真正实战中不一定非要扒到 so 内部不可。因为你想调通签名接口,Java 层 hook 到返回值就够了,RPC 层也是调 Java 方法。分析 so 内部的目的是为了彻底复刻算法,但如果我们接受“动态调用原函数”这种方案,是不需要完全还原算法细节的。这也是后面走 RPC 路线的核心逻辑。

3.3 用 hook 结果反推签名规则

虽然不用完整逆向 so,但为了让心里有数,我一般会从现有输出反推一下签名构成。比如:

  • 签名结果是 32 位十六进制,像是 MD5;
  • 入参里包含业务字段和时间戳;
  • 再次改变时间戳,签名立刻变化;

那基本能猜出签名算法大致是MD5(bizParams + timestamp + secret)之类的结构。至于 secret 藏在哪、拼接顺序什么样,可以再 Hook 常见哈希函数做交叉验证。比如 hooklibc.so里常见的MD5_Update、EVP_DigestUpdate等函数,看它处理的原始字符串是什么。

不过我一般不会在这步过度纠结。因为签名算法一旦更新,你之前还原的规则可能全废。与其费劲逆向,不如直接把调用原函数做成 RPC 服务,这样就算算法内部换了,只要方法入口不变,上层业务就完全不受影响。

4. RPC 调用落地:让 Python 直接调 App 里的签名函数

4.1 frida-rpc 基础框架

RPC 是 Frida 自带的能力,核心就是在 JS 脚本里暴露rpc.exports,让外部通过 Python 或 Nodejs 调用 JS 中定义的函数。

先写一个rpc_sign.js:

rpc.exports = { appsign: function (jsonStr) { return Java.performNow(function () { var SignProvider = Java.use("com.example.xhs.SignProvider"); var params = Java.use("java.util.HashMap").$new(); var jsonObject = JSON.parse(jsonStr); for (var key in jsonObject) { params.put(key, String(jsonObject[key])); } return SignProvider.generateSign(params); }); } };

这里用Java.performNow而不是Java.perform,原因是performNow可以在当前调用结束时同步返回结果,Python 端用exports_sync调用时更方便。

注意,rpc.exports必须写在脚本的顶层,不能包在Java.perform里面。否则 Frida 加载后无法正确注册 RPC 入口。

4.2 写一个简单的签名服务客户端

Python 端代码很简单:

import frida import sys import json device = frida.get_usb_device() session = device.attach("com.example.xhs") script = session.create_script(open("rpc_sign.js", encoding="utf-8").read()) script.load() params = { "keyword": "美食", "page": "1", "page_size": "20", "timestamp": "1730000000" } result = script.exports_sync.appsign(json.dumps(params)) print(result)

如果你想把这套能力做成一个独立服务,可以再用 Flask 或 FastAPI 包一层 HTTP 接口:

from flask import Flask, request, jsonify import frida, json app = Flask(__name__) session = None script = None def init_frida(): global session, script device = frida.get_usb_device() session = device.attach("com.example.xhs") script = session.create_script(open("rpc_sign.js", encoding="utf-8").read()) script.load() @app.route("/sign", methods=["POST"]) def sign(): data = request.get_json() if data is None: return jsonify({"code": 400, "message": "empty body"}) sig = script.exports_sync.appsign(json.dumps(data)) return jsonify({"code": 0, "data": sig}) if __name__ == "__main__": init_frida() app.run(host="127.0.0.1", port=5000)

这里一定要绑定127.0.0.1,不要图省事监听0.0.0.0。因为签名服务等于把 App 内部的算法能力裸奔到网络里,一旦端口暴露在局域网,就会被别人滥用。

4.3 稳定性和并发问题实战处理

实际跑签名服务,最大的问题不是签名逻辑,而是稳定性。

第一个坑:App 进程崩溃或被系统回收。此时 Python 端的 session 会失效,再调用时直接异常。解决办法是加一个重连机制,每次调用前检查 session 是否还活着,断开就重新attach。

def get_script(): global session, script try: script.exports_sync.ping() # 在 rpc 里加一个 ping 函数 except Exception: try: session.detach() except Exception: pass init_frida() return script

第二个坑:并发调用。Frida 的 RPC 不是并行的,同一个 session 即使你同时发多个请求,它也是串行执行。如果业务端高并发调你的 HTTP 签名服务,会有一部分请求排队,甚至超时。我建议在 HTTP 层做一个单进程队列,或者直接限制并发数,避免同时涌进大量调用把手机端拖死。

第三个坑:参数格式。签名函数往往需要特定类型的入参,比如 Long、Integer、List。JSON 转出来默认都是字符串,某些 App 的签名函数会校验类型导致结果崩溃。这种时候可以在 JS 侧根据方法签名做类型转换,或者在 Python 端精确构造类型后再 JSON 序列化。经验之谈:能传字符串就传字符串,大多数签名函数内部会自行处理。

第四个坑:Android 系统省电策略。手机息屏一段时间后,CPU 会降频,甚至网络断开,Frida 连接依然在但响应极慢。可以考虑用adb shell svc power stayon true保持屏幕常亮,或者用充电器持续供电。

5. 这个过程中最容易被坑的几个点

5.1 签名参数动态变化?可能是设备指纹参与

有时候你会发现,同一个请求参数,在不同手机上的签名值完全不一样。这说明签名算法里混入了设备相关因子,比如deviceId、oaid、installId。这类因子通常在 App 启动时从服务端拉取,或者本地生成后存储。

处理这种情况,一个技巧是同时 Hook 签名函数入参里没出现的那些静态信息。你可以打印出整个进程里和device相关的字符串,或者通过Java.enumerateLoadedClasses搜索Device/Oaid/IdProvider这样的类,找到最终参与签名的 ID,把它的取值也暴露到 RPC 返回结果里。

另一种情况:签名值每次都不同但算法固定,那是时间戳参与。你只需要保证调用签名时传入的timestamp和最终发包时用的timestamp一致即可,否则服务端校验时间窗口就会导致请求失败。我在 RPC 层会同时返回签名和时间戳,避免业务端自行拼时间导致不一致。

5.2 hook 不到函数怎么办:排查 frida attach 时机

这是新手最容易卡住的地方。脚本明明写了,日志一点输出都没有,原因有很大概率是:

  • 目标类还没加载。这时可以试试 new 一个对象触发类加载,或者在Java.perform后加一个延迟轮询,等类出现再 hook。
  • App 启动早期调用发生在注入之前。需要用frida -f冷启动,并且不要带--pause,或者带--pause后在脚本里完成 hook 再resume。
  • 方法被内联或隐藏。极个别情况下,签名方法直接写在 native 层且没有经过 Java 层调用,这时 Java hook 当然看不见。那就得从 so 入手,或者用frida-trace -U -f com.example.xhs -i "sign"跟踪所有包含sign的导出函数。

我在实战中更常用一种“漏斗法”:先 hook 所有可能含sign字符串的 Java 方法,再慢慢收窄。也可以先不加过滤,把Java.enumerateLoadedClasses里所有类名打印出来,搜sign、security、crypto这些关键词,然后逐个试。

5.3 合规红线与稳定性取舍

逆向分析这种技术本身是中性的。做安全研究、接口测试、个人自动化,都是正当用途。但我要说句实实在在的话:不要拿着签名接口去批量抓取平台用户数据,更不要做撞库、刷接口这类事。一来是法律风险,二来平台风控也不是摆设,高并发签名请求很快会被识别,导致账号封禁、IP 封禁。

另外,文章里的所有代码都是方法论演示,真实 App 的签名函数名、参数结构、Native 库名肯定会不一样。你要做的是掌握这套“定位- Hook- 抽象- RPC”的流程,而不是把我这个示例套到所有 App 上。

我在实际项目里最深的体会是:**与其追求还原每一个加密算法,不如把 RPC 服务层做稳。**因为算法会变,函数入口却相对稳定。只要你把“调用原函数”的能力沉淀成了服务,未来某天 App 更新了签名算法,你只需要重新分析一遍入口,改一行类名,业务代码完全不用动。这大概就是动态分析和 RPC 结合的最大价值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 4:28:22

openPangu-2.0开源全流程:预训练、SFT与后训练RL落地指南

1. 官宣信息量拆解&#xff1a;预训练、SFT、后训练 RL 分别对应大模型项目里的哪一步看到 openPangu-2.0 这个开源消息&#xff0c;很多人的第一反应是“又一个模型权重放出来了”。但如果仔细把标题读一遍&#xff0c;你会发现这次的信息量其实比“发布权重”大得多。预训练、…

作者头像 李华
网站建设 2026/10/3 4:28:00

Zynq接口适配实战:MII转GMII与EMIO UART0配置全攻略

1. 为什么Zynq设计里会同时出现MII转GMII和EMIO UART0我到现在还记得第一次在Zynq上调试MII转GMII时的状态&#xff1a;板子回来&#xff0c;原厂Demo用的PHY芯片是RGMII接口&#xff0c;但我手上这颗PHY只支持MII&#xff0c;我想当然地觉得“MII不就是把GMII的数据位减半吗&a…

作者头像 李华
网站建设 2026/10/3 4:27:43

数据仓库、数据湖、湖仓一体、数据网格四路径实战决策指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 4:27:31

数据库自治运维实战:DAS Agent 如何构建感知-决策-行动闭环

最近几年数据库运维圈子里最热的一个词&#xff0c;大概就是“自治”。从云厂商到自研数据库&#xff0c;几乎都在喊“自动驾驶”“免运维”&#xff0c;但真落到实际生产环境&#xff0c;敢把核心库交给一个 AI 去折腾的&#xff0c;还是少数。这里想聊的这套智能数据库运维大…

作者头像 李华
网站建设 2026/10/3 4:26:55

Agent自动优化CUDA Kernel:冲上NVIDIA榜单第15名的实战拆解

我刚拿到这个题目的时候&#xff0c;第一反应是“这又是哪个榜单&#xff1f;”——毕竟叫 NVIDIA kernel 榜单一口气能冲到第 15 的事&#xff0c;圈子里很少有人公开拆过程。后来搞清楚了&#xff0c;是个公开的 GPU Kernel 性能评测排行&#xff0c;上面是各种算子的 CUDA 实…

作者头像 李华
网站建设 2026/10/3 4:26:19

AI漫剧创作为何需要32GB显存?英特尔锐炫Pro B70全流程实战解析

最近后台几乎被同一类问题刷爆&#xff1a;做AI漫剧&#xff0c;到底要多大显存才够用&#xff1f;我的回答一直很直接——如果能一步到位&#xff0c;直接上32GB。这不是玄学&#xff0c;是我拿英特尔锐炫Pro B70这张专业卡跑了一整条AI漫剧流水线之后最真切的感受。AI漫剧不是…

作者头像 李华