跳转到内容
123xiao | 无名键客

《从 Frida 到 Xposed:中级开发者实战 Android App 登录校验与签名验证逆向分析》

字数: 0 阅读时长: 1 分钟

从 Frida 到 Xposed:中级开发者实战 Android App 登录校验与签名验证逆向分析

很多开发者学 Android 逆向时,第一步往往是“能 hook 就行”,第二步才发现:能 hook 和能稳定分析业务校验链路,是两回事

这篇文章我会用一个比较实战的角度,带你走一遍:

  • 如何定位 App 的登录校验逻辑
  • 如何分析常见的签名验证/重打包检测
  • 什么时候用 Frida 更高效
  • 什么时候该切到 Xposed
  • 如何写出能运行、能验证、能排错的脚本

说明:本文用于授权测试、安全研究与自有应用加固验证。请勿用于未授权目标。


背景与问题

在 Android App 中,登录失败、接口拒绝、闪退、自检退出,很多时候不是单纯的“账号密码不对”,而是下列校验链路之一在起作用:

  1. 前端登录参数校验
    • 账号格式、密码长度、验证码校验
  2. 本地签名生成
    • 时间戳、nonce、token、设备指纹拼接后做 MD5/SHA/HMAC
  3. 应用完整性校验
    • 校验 APK 签名、证书摘要、包名、安装来源
  4. 环境风险检测
    • Root、模拟器、Frida、Xposed、调试状态检测
  5. 服务端二次验签
    • 本地只负责组包,关键校验在服务端

对中级开发者来说,真正难的不是“hook 某个函数”,而是:

  • 不知道从哪里下手
  • 知道函数名,但不清楚调用时机
  • 能看到参数,却不知道哪一个是关键值
  • Frida 动态改值后有效,但重启就失效
  • 切到 Xposed 后又遇到类加载器、混淆、多进程问题

所以本文的核心目标不是堆工具,而是建立一套分析顺序


前置知识与环境准备

建议你先确认以下环境:

  • Android 真机或模拟器一台
  • 已安装:
    • adb
    • jadx
    • apktool
    • frida-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. 登录链路
  2. 完整性校验链路

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 有两个特征:

  1. 登录请求中包含签名字段 sign
  2. 启动时会做 APK 签名校验,校验失败直接退出

我们要完成三件事:

  • 找到登录请求参数最终生成点
  • 还原或观察 sign 的生成逻辑
  • 绕过本地签名校验,验证是否影响登录流程

第一步:静态定位关键类

先用 jadx 打开 APK,优先搜索以下关键词:

  • login
  • sign
  • md5
  • sha1
  • token
  • timestamp
  • getPackageInfo
  • signatures
  • debuggable
  • root
  • frida

常见的静态入口

  1. 登录按钮点击事件
  2. Retrofit 接口定义
  3. 请求拦截器 Interceptor
  4. 工具类:
    • SignUtil
    • SecurityUtil
    • EncryptUtils
  5. Application 启动阶段的安全检查类

如果混淆严重,不要死磕类名,直接顺着:

  • OkHttpClient.Builder.addInterceptor
  • Request.Builder.addHeader
  • FormBody.Builder.add
  • JSONObject.put

这些 API 往上找调用者,通常比搜索业务词更稳。


第二步:用 Frida 抓登录参数与签名生成

先从最保守、最稳定的点开始:网络请求发出前

如果目标使用 OkHttp,可以 hook Request.BuilderInterceptor

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);
    };
});

这一步的价值很大:你能快速看到登录时到底传了哪些字段,例如:

  • username
  • password
  • timestamp
  • nonce
  • sign
  • deviceId

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.equals
  • String.equalsIgnoreCase

会影响大量业务逻辑。

更好的做法:

  • hook 业务校验方法
  • 或 hook 摘要生成方法
  • 或精确判断调用栈/调用对象后再改结果

4. Android 9+ 取签名方式变了

旧代码常见:

getPackageInfo(pkg, PackageManager.GET_SIGNATURES)

新系统可能用:

  • PackageManager.GET_SIGNING_CERTIFICATES
  • PackageInfo.signingInfo

如果你只盯着 signatures,很可能漏掉真实路径。

5. Native 层签名计算没看到

如果 Java 层只负责传参,真正算法在 so 里,你会发现:

  • Java hook 到输入
  • 却找不到输出生成逻辑

这时要进一步看:

  • System.loadLibrary
  • JNI 方法名
  • RegisterNatives
  • Frida 的 Interceptor.attach

不过本文重点还是 Java 层与框架层,so 层这里只点到为止。


安全/性能最佳实践

逆向分析脚本不只是“能跑”,还要尽量安全、可控、低副作用

1. 优先最小化 hook 范围

不要上来就 hook 全局:

  • String
  • Map
  • JSONObject
  • MessageDigest.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 检测
  • 需要长时间保持生效

我的实际建议

最省时间的组合一般是:

  1. JADX 静态定位
  2. Frida 动态探路
  3. Xposed 固化稳定点
  4. 必要时再进入 native 层

这样不会一开始就陷入“纯静态看不懂、纯动态乱打印”的状态。


总结

把 Android App 的登录校验与签名验证逆向分析做好,关键不在于工具多,而在于顺序正确

  1. 先定位登录请求最终参数
  2. 再确认本地签名函数输入输出
  3. 接着分析 APK 签名/完整性校验
  4. Frida 用来快速试错,Xposed 用来稳定落地
  5. 所有结论都要回到抓包与实际返回结果

如果你是中级开发者,我建议你把这篇文章里的流程记成一句话:

先业务、后安全;先观察、后篡改;先 Frida、后 Xposed。

最后再强调边界条件:

  • 如果关键算法在服务端,本地 hook 只能帮助你理解链路,不能替代服务端验签
  • 如果关键逻辑在 native 层,Java hook 只是入口,不是终点
  • 如果目标存在复杂反调试,优先先做“检测面收敛”,不要盲目加 hook

只要你按本文这套路径走,面对大多数登录校验与签名校验场景,基本都能较快建立可验证的分析闭环。


分享到:

上一篇
《分布式架构中基于消息队列与幂等设计实现高并发订单系统的实战指南》
下一篇
《从提示工程到 RAG:中级开发者构建企业级 AI 问答系统的实战路径》