从 Frida 到 Xposed:中级开发者实战 Android App 登录校验与签名验证逆向分析
很多开发者学 Android 逆向时,第一步往往是“能 hook 就行”,第二步才发现:能 hook 和能稳定分析业务校验链路,是两回事。
这篇文章我会用一个比较实战的角度,带你走一遍:
- 如何定位 App 的登录校验逻辑
- 如何分析常见的签名验证/重打包检测
- 什么时候用 Frida 更高效
- 什么时候该切到 Xposed
- 如何写出能运行、能验证、能排错的脚本
说明:本文用于授权测试、安全研究与自有应用加固验证。请勿用于未授权目标。
背景与问题
在 Android App 中,登录失败、接口拒绝、闪退、自检退出,很多时候不是单纯的“账号密码不对”,而是下列校验链路之一在起作用:
- 前端登录参数校验
- 账号格式、密码长度、验证码校验
- 本地签名生成
- 时间戳、nonce、token、设备指纹拼接后做 MD5/SHA/HMAC
- 应用完整性校验
- 校验 APK 签名、证书摘要、包名、安装来源
- 环境风险检测
- Root、模拟器、Frida、Xposed、调试状态检测
- 服务端二次验签
- 本地只负责组包,关键校验在服务端
对中级开发者来说,真正难的不是“hook 某个函数”,而是:
- 不知道从哪里下手
- 知道函数名,但不清楚调用时机
- 能看到参数,却不知道哪一个是关键值
- Frida 动态改值后有效,但重启就失效
- 切到 Xposed 后又遇到类加载器、混淆、多进程问题
所以本文的核心目标不是堆工具,而是建立一套分析顺序。
前置知识与环境准备
建议你先确认以下环境:
- Android 真机或模拟器一台
- 已安装:
adbjadxapktoolfrida-tools- 对应版本
frida-server
- 如要使用 Xposed:
- LSPosed / EdXposed 环境
- 可编译的 Android Studio 工程
- 基础知识:
- Java 层方法 hook
- Android 类加载流程
- OkHttp / Retrofit 基本请求链路
- APK 签名基础
典型工具分工
| 工具 | 适合场景 | 优点 | 局限 |
|---|---|---|---|
| JADX | 静态浏览 Java/Kotlin 代码 | 快速定位调用关系 | Native/反射/混淆深时吃力 |
| Frida | 动态分析、快速试错 | 灵活、改值快、无需重打包 | 容易被检测 |
| Xposed | 稳定持久 hook | 开机后长期生效,适合复杂流程 | 环境要求高 |
| apktool | 资源与 smali 分析 | 看 Manifest、组件、资源配置 | 修改成本较高 |
核心原理
这一类问题,本质上是在分析两个链路:
- 登录链路
- 完整性校验链路
1. 登录校验的常见路径
flowchart TD
A[点击登录按钮] --> B[UI层校验]
B --> C[ViewModel/Presenter 组装参数]
C --> D[本地签名生成]
D --> E[网络请求发出]
E --> F[服务端返回]
F --> G[本地解析并落库/跳转]
你在逆向时真正应该盯住的,不是“按钮点击”本身,而是这几个关键点:
- 请求发送前的最终参数
- 签名函数的输入与输出
- 返回包解析处的登录成功判定
2. 签名校验的常见路径
sequenceDiagram
participant App
participant PM as PackageManager
participant Cert as Signature/Certificate
participant Guard as 校验逻辑
App->>PM: 获取本包 PackageInfo
PM-->>App: 返回 signatures / signingInfo
App->>Cert: 提取证书字节/摘要
Cert-->>App: SHA1/MD5/自定义摘要
App->>Guard: 与内置值比对
Guard-->>App: 通过/失败
常见实现包括:
PackageManager.getPackageInfo(...)PackageInfo.signatures- Android 9+ 的
signingInfo - 对证书做:
- MD5
- SHA1
- SHA256
- Base64
- 或者再经过一次自定义编码
3. 为什么 Frida 和 Xposed 要配合用
我自己的经验是:
- Frida 适合“探路”
- 看参数、堆栈、返回值
- 快速验证改值是否影响结果
- Xposed 适合“固化”
- 找到稳定切点后,把逻辑做成长期 hook
- 适合登录页、Application、统一网关这类高频入口
可以把它理解成:
- Frida:侦察兵
- Xposed:工程兵
分析思路:先登录,后验签,再反检测
一个实用顺序如下:
flowchart LR
A[静态分析入口 Activity/Fragment] --> B[定位网络库与请求参数]
B --> C[Frida hook 登录参数/签名函数]
C --> D[确认关键字段和返回结果]
D --> E[分析签名校验与完整性检测]
E --> F[必要时切 Xposed 做稳定 hook]
为什么按这个顺序?
因为很多人一上来就死磕签名校验,结果后来发现真正导致失败的是:
- 登录请求体中某个隐藏字段没有构造对
- 时间戳在 native 层二次处理
- 某个 token 来自前一次接口
先把业务链路打通,你才能分清哪里是核心障碍。
实战场景设定
假设目标 App 有两个特征:
- 登录请求中包含签名字段
sign - 启动时会做 APK 签名校验,校验失败直接退出
我们要完成三件事:
- 找到登录请求参数最终生成点
- 还原或观察
sign的生成逻辑 - 绕过本地签名校验,验证是否影响登录流程
第一步:静态定位关键类
先用 jadx 打开 APK,优先搜索以下关键词:
loginsignmd5sha1tokentimestampgetPackageInfosignaturesdebuggablerootfrida
常见的静态入口
- 登录按钮点击事件
- Retrofit 接口定义
- 请求拦截器
Interceptor - 工具类:
SignUtilSecurityUtilEncryptUtils
- Application 启动阶段的安全检查类
如果混淆严重,不要死磕类名,直接顺着:
OkHttpClient.Builder.addInterceptorRequest.Builder.addHeaderFormBody.Builder.addJSONObject.put
这些 API 往上找调用者,通常比搜索业务词更稳。
第二步:用 Frida 抓登录参数与签名生成
先从最保守、最稳定的点开始:网络请求发出前。
如果目标使用 OkHttp,可以 hook Request.Builder 或 Interceptor。
Frida 脚本:打印 OkHttp 请求信息
Java.perform(function () {
var RequestBuilder = Java.use('okhttp3.Request$Builder');
RequestBuilder.build.implementation = function () {
var req = this.build();
try {
console.log('=== OkHttp Request ===');
console.log('URL: ' + req.url().toString());
console.log('Method: ' + req.method());
var headers = req.headers();
console.log('Headers:\n' + headers.toString());
} catch (e) {
console.log('build hook error: ' + e);
}
return req;
};
});
运行方式:
frida -U -f com.example.target -l hook_okhttp.js --no-pause
如果你已经知道是表单参数或 JSON 参数,还可以进一步 hook 参数构造点。
Frida 脚本:打印 JSONObject.put
Java.perform(function () {
var JSONObject = Java.use('org.json.JSONObject');
JSONObject.put.overload('java.lang.String', 'java.lang.Object').implementation = function (k, v) {
console.log('[JSONObject.put] ' + k + ' = ' + v);
return this.put(k, v);
};
});
这一步的价值很大:你能快速看到登录时到底传了哪些字段,例如:
usernamepasswordtimestampnoncesigndeviceId
Frida 脚本:hook 常见摘要函数
很多 App 的 sign 最后会落到标准算法上,因此可以直接 hook:
Java.perform(function () {
var MessageDigest = Java.use('java.security.MessageDigest');
MessageDigest.getInstance.overload('java.lang.String').implementation = function (alg) {
console.log('[MessageDigest.getInstance] ' + alg);
return this.getInstance(alg);
};
});
如果你已经定位到具体业务类,比如 com.demo.app.security.SignUtil,直接 hook 它更高效。
Frida 脚本:hook 业务签名函数
Java.perform(function () {
var SignUtil = Java.use('com.demo.app.security.SignUtil');
SignUtil.genSign.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function (u, p, ts) {
console.log('[genSign] username=' + u + ', password=' + p + ', ts=' + ts);
var ret = this.genSign(u, p, ts);
console.log('[genSign] ret=' + ret);
return ret;
};
});
我当时做这类分析时,最常踩的坑就是:明明 hook 到了摘要算法,却看不到原始输入。
所以更推荐优先 hook 业务封装函数,而不是一开始就钻到MessageDigest.digest()。
第三步:定位登录成功判定点
抓到请求参数还不够,你还需要知道:App 认为什么叫登录成功。
常见判定方式:
- 响应码
code == 0 data.token != null- 本地写入
SharedPreferences - 保存用户信息到数据库
- 跳转首页 Activity
Frida 脚本:hook SharedPreferences 写入 token
Java.perform(function () {
var SPImpl = Java.use('android.app.SharedPreferencesImpl$EditorImpl');
SPImpl.putString.overload('java.lang.String', 'java.lang.String').implementation = function (k, v) {
if (k.toLowerCase().indexOf('token') >= 0) {
console.log('[SP token] ' + k + ' = ' + v);
}
return this.putString(k, v);
};
});
这一步能帮你判断:
- 登录失败是服务端拒绝
- 还是本地校验没过导致不落库
- 又或者是返回成功但后续完整性检查触发退出
第四步:分析并绕过 APK 签名校验
登录逻辑跑通后,再看完整性校验。
常见 Java 层签名检测代码
public static boolean checkSignature(Context context) {
try {
PackageManager pm = context.getPackageManager();
PackageInfo info = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signs = info.signatures;
String sha1 = sha1(signs[0].toByteArray());
return "12AB34CD56EF...".equalsIgnoreCase(sha1);
} catch (Exception e) {
return false;
}
}
Frida 绕过方式 1:直接改返回值
如果已经找到目标方法,最直接:
Java.perform(function () {
var Guard = Java.use('com.demo.app.security.Guard');
Guard.checkSignature.implementation = function () {
console.log('[Bypass] checkSignature -> true');
return true;
};
});
Frida 绕过方式 2:改 getPackageInfo 结果前的比对逻辑
如果没有直接方法名,可以 hook 比较点,例如 String.equalsIgnoreCase,但这个范围太大,容易误伤。更推荐 hook 证书摘要计算函数。
Frida 绕过方式 3:hook PackageManager.getPackageInfo
Java.perform(function () {
var AppPkgMgr = Java.use('android.app.ApplicationPackageManager');
AppPkgMgr.getPackageInfo.overload('java.lang.String', 'int').implementation = function (pkg, flag) {
var ret = this.getPackageInfo(pkg, flag);
if (pkg.indexOf('com.example.target') >= 0) {
console.log('[getPackageInfo] pkg=' + pkg + ', flag=' + flag);
}
return ret;
};
});
这一招的作用主要是观察调用频率和时机,帮助你判断校验发生在:
- Application 启动时
- 登录前
- 接口请求前
- 某个 Activity
onResume中
第五步:当 Frida 不稳定时,切到 Xposed
有些 App 对 Frida 检测很重,或者你需要在非常早的时机 hook,比如:
Application.attach- 多 Dex 动态加载后再 hook
- 某些校验只在冷启动最早阶段触发
这时候 Xposed/LSPosed 往往更稳。
Xposed 模块示例:hook 登录签名函数
下面给一个可运行的基础示例。
build.gradle 关键依赖
dependencies {
compileOnly 'de.robv.android.xposed:api:82'
}
assets/xposed_init
com.example.xposed.LoginHook
Xposed 代码:hook 指定包名与签名函数
package com.example.xposed;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class LoginHook implements IXposedHookLoadPackage {
private static final String TARGET_PKG = "com.example.target";
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (!TARGET_PKG.equals(lpparam.packageName)) {
return;
}
XposedBridge.log("[Xposed] Loaded: " + lpparam.packageName);
try {
Class<?> signUtilClass = XposedHelpers.findClass(
"com.demo.app.security.SignUtil",
lpparam.classLoader
);
XposedHelpers.findAndHookMethod(signUtilClass, "genSign",
String.class, String.class, String.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
XposedBridge.log("[genSign] u=" + param.args[0]
+ ", p=" + param.args[1]
+ ", ts=" + param.args[2]);
}
@Override
protected void afterHookedMethod(MethodHookParam param) {
XposedBridge.log("[genSign] ret=" + param.getResult());
}
});
} catch (Throwable t) {
XposedBridge.log("[Xposed] hook genSign failed: " + t);
}
}
}
Xposed 代码:绕过签名校验
package com.example.xposed;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class SignatureBypass implements IXposedHookLoadPackage {
private static final String TARGET_PKG = "com.example.target";
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (!TARGET_PKG.equals(lpparam.packageName)) {
return;
}
try {
XposedHelpers.findAndHookMethod(
"com.demo.app.security.Guard",
lpparam.classLoader,
"checkSignature",
XC_MethodReplacement.returnConstant(true)
);
XposedBridge.log("[Xposed] checkSignature bypassed");
} catch (Throwable t) {
XposedBridge.log("[Xposed] bypass failed: " + t);
}
}
}
什么时候优先用 Xposed
如果你遇到以下情况,建议直接上 Xposed:
- Frida 附加后 App 立刻闪退
- 目标检测
frida-server/ 27042 端口 - 方法在启动早期执行,Frida 来不及 hook
- 需要长期保留调试逻辑,不想每次手动注入
逐步验证清单
这个清单非常重要,能避免“我写了好多 hook,但不知道现在到底卡在哪”。
验证 1:登录请求是否真正发出
- 看 OkHttp hook 是否打印 URL
- 抓包确认请求已到服务端
- 确认不是本地 UI 校验直接拦截
验证 2:sign 是否变化可控
- 固定用户名密码,只改时间戳
- 观察
sign是否随时间变化 - 固定时间戳,只改密码
- 判断签名输入字段集合
验证 3:签名校验绕过后,App 是否还退出
- 若仍退出,说明还有:
- Root 检测
- 调试检测
- Frida 检测
- so 层校验
验证 4:服务端是否仍拒绝
- 本地绕过仅影响客户端
- 若服务端做二次验签,本地改值不一定生效
- 需要区分“本地门禁”与“服务端门禁”
常见坑与排查
这部分我尽量写得接地气一点,因为真正花时间的常常不是 hook 本身,而是这些小坑。
1. Hook 了方法,但就是不生效
常见原因:
- 类名错了
- 方法重载没选对
- 方法在别的 ClassLoader 中
- 目标走的是 Kotlin companion/object 或内联逻辑
- App 代码被壳或动态加载
排查建议:
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch: function (name) {
if (name.indexOf('SignUtil') >= 0) {
console.log(name);
}
},
onComplete: function () {}
});
});
如果是动态加载,先 hook Application.attach 拿到 classloader。
Java.perform(function () {
var Application = Java.use('android.app.Application');
Application.attach.overload('android.content.Context').implementation = function (ctx) {
console.log('[attach] classloader=' + ctx.getClassLoader());
this.attach(ctx);
};
});
2. App 一注入 Frida 就闪退
大概率是反调试/反 Frida 检测。
常见检测点:
/proc/self/maps- 端口扫描
- 进程名特征
- 栈信息异常
TracerPid
处理思路:
- 延迟注入改为 spawn 模式
- 精简脚本,减少 noisy hook
- 优先 hook 检测函数返回值
- 必要时改用 Xposed
3. Hook equals 这类系统方法后全局异常
这是典型“图省事导致误伤”。
比如为了绕过签名摘要比对,直接 hook:
String.equalsString.equalsIgnoreCase
会影响大量业务逻辑。
更好的做法:
- hook 业务校验方法
- 或 hook 摘要生成方法
- 或精确判断调用栈/调用对象后再改结果
4. Android 9+ 取签名方式变了
旧代码常见:
getPackageInfo(pkg, PackageManager.GET_SIGNATURES)
新系统可能用:
PackageManager.GET_SIGNING_CERTIFICATESPackageInfo.signingInfo
如果你只盯着 signatures,很可能漏掉真实路径。
5. Native 层签名计算没看到
如果 Java 层只负责传参,真正算法在 so 里,你会发现:
- Java hook 到输入
- 却找不到输出生成逻辑
这时要进一步看:
System.loadLibrary- JNI 方法名
RegisterNatives- Frida 的
Interceptor.attach
不过本文重点还是 Java 层与框架层,so 层这里只点到为止。
安全/性能最佳实践
逆向分析脚本不只是“能跑”,还要尽量安全、可控、低副作用。
1. 优先最小化 hook 范围
不要上来就 hook 全局:
StringMapJSONObjectMessageDigest.digest
除非你只是做一次性侦察。
更推荐:
- 先静态定位候选类
- 再 hook 业务类
- 最后才扩大范围补漏
2. 给日志加过滤条件
比如只打印登录相关 URL:
Java.perform(function () {
var RequestBuilder = Java.use('okhttp3.Request$Builder');
RequestBuilder.build.implementation = function () {
var req = this.build();
var url = req.url().toString();
if (url.indexOf('/login') >= 0) {
console.log('[Login Request] ' + url);
}
return req;
};
});
否则日志过大时,性能会明显下降,甚至拖垮目标进程。
3. 区分“观察”和“篡改”
我一般建议分两阶段:
- 第一阶段:只打印,不改值
- 第二阶段:局部改值验证假设
这样你能避免因为改值太早,导致分析链路失真。
4. Xposed 模块要考虑多进程
有些 App 会把安全检测放在独立进程:
:guard:push:remote
因此要同时打印:
XposedBridge.log("pkg=" + lpparam.packageName + ", process=" + lpparam.processName);
否则你以为 hook 失效,其实只是进错进程了。
5. 不要忽视服务端边界
这是安全分析里特别重要的一点:
- 本地绕过 ≠ 服务端绕过
- 本地成功显示登录 ≠ 服务端真正授权
- 修改签名字段若无法通过服务端验签,最终只是 UI 假象
所以所有结论都应该回到:
- 抓包结果
- 返回码
- 服务端状态变化
一个更完整的 Frida 实战脚本
下面给一个稍微完整一点的示例,串起登录参数、签名函数、签名校验绕过三个点。
Java.perform(function () {
console.log('[*] Frida start');
// 1. hook 登录请求构造
try {
var JSONObject = Java.use('org.json.JSONObject');
JSONObject.put.overload('java.lang.String', 'java.lang.Object').implementation = function (k, v) {
if (['username', 'password', 'timestamp', 'sign', 'token'].indexOf(String(k)) >= 0) {
console.log('[JSONObject.put] ' + k + ' = ' + v);
}
return this.put(k, v);
};
console.log('[+] JSONObject hook ok');
} catch (e) {
console.log('[-] JSONObject hook failed: ' + e);
}
// 2. hook 业务签名函数
try {
var SignUtil = Java.use('com.demo.app.security.SignUtil');
SignUtil.genSign.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function (u, p, ts) {
console.log('[genSign] input => ' + u + ', ' + p + ', ' + ts);
var ret = this.genSign(u, p, ts);
console.log('[genSign] output => ' + ret);
return ret;
};
console.log('[+] SignUtil hook ok');
} catch (e) {
console.log('[-] SignUtil hook failed: ' + e);
}
// 3. hook 签名校验
try {
var Guard = Java.use('com.demo.app.security.Guard');
Guard.checkSignature.implementation = function () {
console.log('[Bypass] checkSignature => true');
return true;
};
console.log('[+] Guard hook ok');
} catch (e) {
console.log('[-] Guard hook failed: ' + e);
}
// 4. 观察 token 落地
try {
var SPImpl = Java.use('android.app.SharedPreferencesImpl$EditorImpl');
SPImpl.putString.overload('java.lang.String', 'java.lang.String').implementation = function (k, v) {
if (String(k).toLowerCase().indexOf('token') >= 0) {
console.log('[SP] ' + k + ' = ' + v);
}
return this.putString(k, v);
};
console.log('[+] SharedPreferences hook ok');
} catch (e) {
console.log('[-] SharedPreferences hook failed: ' + e);
}
});
运行后,你应该重点观察:
- 点击登录时是否打印用户名、时间戳、sign
genSign是否被调用checkSignature是否在启动时触发- 登录成功后 token 是否保存
如果这四个点都能打通,说明你已经基本掌握了目标 App 的关键认证链路。
Frida 与 Xposed 的取舍建议
适合 Frida 的情况
- 你还在探索阶段
- 不确定类名和方法名
- 需要快速验证多个猜想
- 不想重启设备反复刷模块
适合 Xposed 的情况
- 你已经找到稳定 hook 点
- 需要早期时机介入
- 目标有明显 Frida 检测
- 需要长时间保持生效
我的实际建议
最省时间的组合一般是:
- JADX 静态定位
- Frida 动态探路
- Xposed 固化稳定点
- 必要时再进入 native 层
这样不会一开始就陷入“纯静态看不懂、纯动态乱打印”的状态。
总结
把 Android App 的登录校验与签名验证逆向分析做好,关键不在于工具多,而在于顺序正确:
- 先定位登录请求最终参数
- 再确认本地签名函数输入输出
- 接着分析 APK 签名/完整性校验
- Frida 用来快速试错,Xposed 用来稳定落地
- 所有结论都要回到抓包与实际返回结果
如果你是中级开发者,我建议你把这篇文章里的流程记成一句话:
先业务、后安全;先观察、后篡改;先 Frida、后 Xposed。
最后再强调边界条件:
- 如果关键算法在服务端,本地 hook 只能帮助你理解链路,不能替代服务端验签
- 如果关键逻辑在 native 层,Java hook 只是入口,不是终点
- 如果目标存在复杂反调试,优先先做“检测面收敛”,不要盲目加 hook
只要你按本文这套路径走,面对大多数登录校验与签名校验场景,基本都能较快建立可验证的分析闭环。