安卓逆向实战:基于 Frida 与 JADX 的登录参数加密流程定位与 Hook 分析
很多人做安卓逆向时,最先卡住的不是“怎么反编译”,而是“参数到底在哪里加密的”。尤其是登录接口:抓包只能看到一串密文,APP 里类名又被混淆,直接搜 encrypt、sign、md5 常常一无所获。
这篇文章我就按一个真实实战思路来带你走一遍:先用 JADX 做静态分析缩小范围,再用 Frida 动态 Hook把登录参数加密流程抓出来,最后验证“明文输入 -> 加密函数 -> 请求发出”的完整链路。
这篇文章默认用于授权测试、学习研究和自有应用安全分析。不要用于未授权目标。
背景与问题
我们面对的典型场景通常是这样的:
- 抓包能看到
/login请求 - 用户名、密码、时间戳、签名等参数中,至少一部分已经被加密或摘要
- 直接改包重放失败
- 想知道:
- 明文参数从哪里进入加密逻辑?
- 参与加密的字段有哪些?
- 算法是标准算法还是自定义拼接?
- 盐值、固定 token、设备指纹是否参与了签名?
如果只靠抓包,很难回答这些问题;如果只靠静态分析,又常被混淆和调用链绕晕。JADX + Frida 的组合,本质上是把“看代码”和“看运行时”拼起来。
前置知识与环境准备
建议你至少准备这些工具:
jadx-gui:静态查看 APK 代码adb:连接手机/模拟器frida-toolsobjection(可选)- 一台已能运行目标 APP 的 Android 设备或模拟器
Python / Frida 安装
pip install frida frida-tools
常用命令确认
查看设备进程:
frida-ps -U
附加到目标应用:
frida -U -f com.example.app -l hook_login.js
如果目标 APP 有反调试或闪退,后面“常见坑与排查”会专门说。
核心原理
这一类问题,核心不是“记住某个 API”,而是理解登录参数加密链路通常长什么样。
常见链路可以抽象为:
flowchart LR
A[登录按钮点击] --> B[收集用户名/密码]
B --> C[业务层组装请求对象]
C --> D[加密/签名函数]
D --> E[网络层发包]
E --> F[服务端验签]
逆向时,我们要回答两个问题:
-
入口在哪里?
- 登录按钮点击
- ViewModel / Presenter 的登录方法
- Retrofit / OkHttp 发包前封装
-
加密点在哪里?
- 业务代码里的工具类
- JNI so 层
- 通用加密 API,如
MessageDigest、Cipher - 网络拦截器中统一签名
用 JADX 缩小范围:先找“登录”,再找“参数加工”
静态分析阶段,我一般不急着搜算法,而是先找业务入口。
1. 搜关键字
优先搜这些词:
loginusernamepassword/loginsigntokentimestampnonce
如果接口路径被常量化,也可以搜:
- Retrofit 注解:
@POST,@FormUrlEncoded,@Body - OkHttp:
newCall,Request.Builder - JSON 构造:
JSONObject,HashMap,TreeMap
2. 从登录按钮倒推调用链
通常会看到类似结构:
LoginActivityLoginFragmentLoginViewModelUserRepositoryApiService
一个典型的调用关系如下:
sequenceDiagram
participant UI as LoginActivity
participant VM as LoginViewModel
participant Repo as UserRepository
participant Util as SignUtil
participant Net as ApiService
UI->>VM: onLoginClick(username, password)
VM->>Repo: login(username, password)
Repo->>Util: buildSign(params)
Util-->>Repo: sign
Repo->>Net: login(data, sign)
Net-->>Repo: response
Repo-->>VM: result
VM-->>UI: success/fail
3. 重点观察三类代码
第一类:Map/JSON 组装点
例如:
HashMap<String, String> map = new HashMap<>();
map.put("username", username);
map.put("password", pwd);
map.put("timestamp", ts);
map.put("sign", SignUtil.sign(map));
这类地方非常关键,因为它告诉你:
- 哪些字段参与签名
- 签名发生在参数组装前还是后
- 密码是否先二次处理
第二类:工具类调用
比如:
SignUtil.sign(...)
EncryptUtils.encrypt(...)
SecurityManager.getToken(...)
NativeBridge.encode(...)
即便类名被混淆,只要某个方法被反复调用、入参又像字符串/Map,就值得怀疑。
第三类:标准加密 API
搜这些类名常常有惊喜:
java.security.MessageDigestjavax.crypto.Cipherjavax.crypto.spec.SecretKeySpecandroid.util.Base64java.net.URLEncoder
很多 APP 所谓“自定义加密”,其实是:
- 字段排序
- 拼接固定盐值
- MD5 / SHA1 / SHA256
- Base64
- AES
- 再 URL encode
定位思路:从“可见输入”向“不可见加密”推进
如果你在 JADX 中已经找到一个疑似登录流程,可以按下面路径逐步验证:
flowchart TD
A[找到登录按钮或登录接口] --> B[确认用户名密码传递方法]
B --> C[查看请求对象构造]
C --> D[定位 sign/encrypt/native 方法]
D --> E[Hook 入参和返回值]
E --> F[对照抓包结果验证]
F --> G[确认最终加密链路]
这个过程有个经验点:不要一上来就 Hook 全世界。先缩小到“登录时一定会经过的几个方法”,否则日志会多到没法看。
实战代码(可运行)
下面给一套可直接改造的 Frida 脚本。思路是分层 Hook:
- Hook 登录入口方法
- Hook 参数组装类
- Hook 标准摘要/加密 API
- Hook 网络层请求
包名、类名需要你根据实际 APK 调整。代码是可运行模板,不是伪代码。
示例 1:Hook 登录业务方法
假设 JADX 中看到了:
com.example.app.ui.LoginViewModel- 方法:
doLogin(java.lang.String, java.lang.String)
Java.perform(function () {
var LoginViewModel = Java.use("com.example.app.ui.LoginViewModel");
LoginViewModel.doLogin.overload("java.lang.String", "java.lang.String").implementation = function (username, password) {
console.log("========== doLogin called ==========");
console.log("[+] username = " + username);
console.log("[+] password = " + password);
var ret = this.doLogin(username, password);
console.log("[+] doLogin return = " + ret);
return ret;
};
});
运行:
frida -U -f com.example.app -l hook_login.js
这个阶段的目的不是立刻拿到密文,而是确认:
- 你 Hook 的方法是不是登录真实入口
- 用户输入有没有被提前处理
示例 2:Hook 参数组装与签名方法
假设你在 JADX 中发现:
com.example.app.security.SignUtil- 方法:
sign(java.util.Map)
Java.perform(function () {
var SignUtil = Java.use("com.example.app.security.SignUtil");
var HashMap = Java.use("java.util.HashMap");
var MapEntry = Java.use("java.util.Map$Entry");
function printMap(mapObj) {
var entrySet = mapObj.entrySet();
var iterator = entrySet.iterator();
while (iterator.hasNext()) {
var entry = Java.cast(iterator.next(), MapEntry);
console.log(" " + entry.getKey() + " = " + entry.getValue());
}
}
SignUtil.sign.overload("java.util.Map").implementation = function (map) {
console.log("========== SignUtil.sign called ==========");
console.log("[+] input map:");
printMap(map);
var ret = this.sign(map);
console.log("[+] sign result = " + ret);
return ret;
};
});
如果签名函数用的是 HashMap 或 TreeMap,这个脚本通常就能把参与签名的字段一把抓出来。
示例 3:Hook MessageDigest,判断是否用了 MD5/SHA
如果你还没找到业务签名类,可以退一步 Hook 标准摘要 API。
Java.perform(function () {
var MessageDigest = Java.use("java.security.MessageDigest");
var StringCls = Java.use("java.lang.String");
MessageDigest.getInstance.overload("java.lang.String").implementation = function (algorithm) {
console.log("[+] MessageDigest.getInstance -> " + algorithm);
return this.getInstance(algorithm);
};
MessageDigest.digest.overload("[B").implementation = function (input) {
var result = this.digest(input);
try {
var plain = StringCls.$new(input);
console.log("========== MessageDigest.digest ==========");
console.log("[+] algorithm = " + this.getAlgorithm());
console.log("[+] input(str) = " + plain);
console.log("[+] input(len) = " + input.length);
} catch (e) {
console.log("[+] digest input decode failed");
}
return result;
};
});
这个脚本特别适合判断:
- 是否是
MD5 - 是否是
SHA-1/SHA-256 - 摘要前是否有固定拼接串
但要注意:
byte[]不一定能直接转字符串,遇到乱码是正常的。
示例 4:Hook Cipher,判断是否用了 AES
Java.perform(function () {
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.getInstance.overload("java.lang.String").implementation = function (transformation) {
console.log("[+] Cipher.getInstance -> " + transformation);
return this.getInstance(transformation);
};
Cipher.doFinal.overload("[B").implementation = function (input) {
console.log("========== Cipher.doFinal called ==========");
console.log("[+] algorithm = " + this.getAlgorithm());
console.log("[+] input length = " + input.length);
var ret = this.doFinal(input);
console.log("[+] output length = " + ret.length);
return ret;
};
});
适合快速判断:
- 是否用了
AES/ECB/PKCS5Padding - 是否存在对称加密而不只是摘要签名
示例 5:Hook OkHttp 请求体,关联最终发包参数
很多时候你已经知道签名函数了,但还需要确认“最终发出去的是不是这份数据”。这时 Hook 网络层最直接。
下面示例 Hook OkHttp 的 Request.Builder:
Java.perform(function () {
var RequestBuilder = Java.use("okhttp3.Request$Builder");
RequestBuilder.url.overload("java.lang.String").implementation = function (url) {
console.log("[+] Request URL = " + url);
return this.url(url);
};
RequestBuilder.method.overload("java.lang.String", "okhttp3.RequestBody").implementation = function (method, body) {
console.log("[+] HTTP Method = " + method);
console.log("[+] RequestBody = " + body);
return this.method(method, body);
};
});
如果你需要进一步打印请求体内容,可以配合 okio.Buffer 去读,但不同版本 OkHttp 实现差异较大。教程里我建议先确认 URL 和方法,避免一开始把网络层 Hook 得太重。
逐步验证清单
做这类分析,我建议按这个顺序一项项打勾,不容易乱:
阶段 1:确认入口
- 登录按钮点击能定位到方法
- 用户名、密码能在业务层方法中打印出来
阶段 2:确认参数构造
- 找到
Map/JSONObject/ 请求体组装位置 - 确认是否有
timestamp/nonce/deviceId
阶段 3:确认加密算法
- 找到
sign/encrypt/ native 方法 - 或通过
MessageDigest/CipherHook 识别算法 - 记录入参与出参
阶段 4:确认最终发包
- 将 Hook 得到的 sign 与抓包中的 sign 对比
- 确认是否有 URL 编码或二次封装
- 确认是否存在拦截器统一补签
一个完整案例思路
假设你最终在 JADX 中看到类似逻辑:
public String login(String user, String pwd) {
String ts = String.valueOf(System.currentTimeMillis());
HashMap<String, String> map = new HashMap<>();
map.put("username", user);
map.put("password", md5(pwd));
map.put("timestamp", ts);
String sign = SignUtil.sign(map);
map.put("sign", sign);
return api.login(map);
}
那么真实链路很可能是:
- 用户输入原始密码
- 先做一次
md5(pwd) - 再把
username + password + timestamp这些字段做排序/拼接 SignUtil.sign(map)生成sign- 发包
此时你就不该只盯着“密码加密”,而要意识到:
password字段和sign字段是两套逻辑- 登录失败时服务端可能校验的是整体签名,而不是单独密码
- 仅复现
md5(pwd)远远不够
常见坑与排查
这一部分很重要,我自己当时踩坑最多的地方基本都在这。
1. Hook 了方法,但没输出
常见原因:
- 方法重载选错了
- 类名是混淆后的,不是你以为的那个
- 目标方法还没被类加载
- APP 走的是另一条登录路径
排查建议:
Java.perform(function () {
var cls = Java.use("com.example.app.security.SignUtil");
console.log(cls.sign.overloads);
});
先把 overloads 打出来,确认参数签名。
2. Frida 附加后闪退
常见原因:
- 有 Frida 检测
- 有 ptrace / 反调试
- 启动时校验环境
- Root 检测
可尝试:
- 用
-f启动而不是 attach - 先 spawn 再 resume
- 用更早时机注入
- 配合反检测脚本
- 在模拟器和真机之间切换验证
3. 找到 MessageDigest 了,但日志太多
因为 APP 全局很多地方都会用摘要,比如:
- 图片缓存 key
- 埋点签名
- token 校验
- 文件校验
建议增加过滤条件:
if (this.getAlgorithm().toUpperCase().indexOf("MD5") >= 0) {
// 只打印 MD5
}
或者只在登录页面点击后打印几秒钟内的调用。
4. Map 打印出来了,但字段顺序和服务端不一致
这是个高频坑。很多签名逻辑会:
- 对 key 排序
- 过滤空值
- 去掉
sign本身 - 在尾部拼固定 salt
也就是说,你看到的原始 HashMap,不代表真正参与摘要时的拼接顺序。
解决办法:
- Hook 真正的
sign(Map)方法 - 再往里跟,找到“拼接成字符串”的那一步
- 如果有
StringBuilder.append()集中出现,也值得重点看
5. 明明拿到了 sign,重放还是失败
别急,这很常见。原因可能有:
timestamp失效nonce一次性使用deviceId/androidId参与签名- 请求头也参与了校验
- TLS 层有证书绑定,不是单纯参数问题
你要补查的点包括:
- Header 是否有动态 token
- Body 外层是否又加了一层统一签名
- Native 层是否参与
- 是否存在服务端风控字段
6. Java 层找不到,可能在 Native 层
如果你看到类似:
public native String encode(String data);
或者签名逻辑一进方法就跳 JNI,那说明关键流程可能在 so 里。这时策略要切换:
- 先 Hook Java 层 native 调用入口,看入参与返回值
- 再决定是否深入
libxxx.so
对于教程场景,先把 Java 层入口和结果拿到,通常已经足够还原协议的大部分行为。
安全/性能最佳实践
虽然这是逆向分析文章,但实际操作时仍然建议守住边界。
安全边界
- 仅分析自有应用或获得明确授权的目标
- 不要在生产用户环境中随意注入脚本
- 对抓到的账号、token、设备标识做脱敏
- 记录实验数据时,避免把密钥和敏感参数直接公开
Hook 性能控制
Frida 很强,但日志打多了会明显拖慢 APP,甚至触发异常。
建议:
- 先 Hook 少量高价值方法,不要全局扫射
- 对高频方法加过滤条件
- 不要长时间打印大对象、完整字节数组
- 遇到加密 API 高频调用时,只在登录动作触发后短时采样
例如可以加个简单的开关:
var enableLog = false;
Java.perform(function () {
var LoginActivity = Java.use("com.example.app.ui.LoginActivity");
LoginActivity.onLoginClick.implementation = function () {
enableLog = true;
console.log("[+] login click, enable hook log");
return this.onLoginClick();
};
});
然后在其他 Hook 点中判断 enableLog 再输出。
分层记录,比一把梭更稳
我比较推荐的节奏是:
- 先确认登录入口
- 再确认参数组装
- 再确认 sign 函数
- 最后补网络层验证
这样每一步都有结果,不容易把自己绕进去。
一个更稳的 Hook 组合建议
如果你只想快速拿结果,我建议从下面这组最小闭环开始:
- Hook 登录业务入口
- Hook
sign(Map)或疑似工具类 - Hook
MessageDigest.getInstance - Hook 请求 URL
这样通常就能回答 80% 的问题:
- 登录时传入了什么明文
- 哪些字段参与签名
- 用了什么算法
- 最终请求去了哪个接口
只有在这些信息还不够时,再去补:
Cipher.doFinal- Native 方法
- OkHttp 请求体细节
- 拦截器统一加签
总结
做“登录参数加密流程定位”,真正有效的方法不是盲猜算法,而是建立一条可验证的调用链:
- 用 JADX 找到登录业务入口和参数组装点
- 用 Frida Hook 关键方法的入参与返回值
- 用标准 API Hook 辅助识别摘要/加密算法
- 用网络层 Hook 对照最终发包结果
一句话概括这套方法:先缩小范围,再动态确认,不要一开始就试图看懂所有代码。
如果你现在正卡在某个 APP 的登录接口,我建议你立刻做这三件事:
- 在 JADX 中先找到登录入口和
/login请求位置 - 优先 Hook 业务层的
login与sign(Map) - 再用
MessageDigest/Cipher判断到底是摘要、对称加密还是混合流程
边界条件也要记住:
- 如果 Java 层看不到核心逻辑,要考虑 JNI
- 如果 sign 对得上但仍失败,要补查 header、timestamp、deviceId
- 如果 Frida 一附加就崩,要先处理反调试和反注入
你不用一次把整套链路全拿下。只要先抓到**“明文长什么样、在哪一步变成密文”**,后面的分析就会轻松很多。