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

《安卓逆向实战:基于 Frida 与 JADX 定位并绕过常见签名校验逻辑》

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

安卓逆向实战:基于 Frida 与 JADX 定位并绕过常见签名校验逻辑

很多 Android App 会做“签名校验”:确认自己是不是由官方证书签名、有没有被二次打包、是不是运行在被篡改的环境里。对开发者来说,这是保护手段;对逆向分析来说,它往往是第一道门槛。

这篇文章我不打算只讲概念,而是按一个能落地复现的实战路径来走:先用 JADX 找到签名校验代码,再用 Frida 动态 hook,把校验结果改掉,最终验证绕过是否生效。

说明一下:本文内容用于安全研究、加固评估与自测,请勿用于未授权目标。


背景与问题

在 Android 逆向里,签名校验常见于这些场景:

  • App 启动时检查自身签名
  • 登录、支付、会员功能前做二次校验
  • Native 层配合 Java 层双重校验
  • 检测是否被重打包、注入、替换壳

逆向时常见症状也很典型:

  • App 一启动就闪退
  • 某页面打不开,提示“环境异常”
  • 某个按钮点击后无响应
  • 改包名、重签名之后直接触发保护逻辑

如果你只是“知道有签名校验”,但不知道它在哪、怎么执行、怎么改结果,那就很难推进。实际工作里,我一般会把问题拆成三步:

  1. 静态定位:用 JADX 找到疑似签名校验入口
  2. 动态确认:用 Frida 验证是否真的走到了这里
  3. 最小绕过:尽量少改逻辑,只改关键返回值

前置知识与环境准备

建议你至少具备这些基础:

  • 会用 adb
  • 知道 APK 安装、启动、查看日志
  • 对 Java/Android API 有基本认识
  • 用过 Frida 的基础 hook 语法

环境准备

  • Android 真机或模拟器
  • adb
  • jadx-gui
  • frida-tools
  • 与设备架构匹配的 frida-server

安装 Frida 工具:

pip install frida-tools

查看设备架构:

adb shell getprop ro.product.cpu.abi

推送并启动 frida-server

adb push frida-server /data/local/tmp/
adb shell "chmod +x /data/local/tmp/frida-server"
adb shell "/data/local/tmp/frida-server &"

验证设备连接:

frida-ps -U

核心原理

Android 签名校验常见实现方式,核心无非就几类:

  1. 直接读取自身签名并比对
  2. 取签名摘要(如 SHA-1 / SHA-256 / MD5)后比对
  3. 通过 PackageManager 查询包信息
  4. Native 层通过 JNI 调 Java API 或直接校验
  5. 服务端参与校验,本地只做前置判断

Java 层常见签名校验路径

老版本常见写法:

PackageInfo packageInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
Signature[] signatures = packageInfo.signatures;

新版本更常见:

PackageInfo packageInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNING_CERTIFICATES);
SigningInfo signingInfo = packageInfo.signingInfo;

之后通常会:

  • toByteArray()
  • 喂给 MessageDigest
  • 转 hex 字符串
  • 与硬编码值比对

我们绕过的基本思路

绕过不是只有一种姿势,常见有三类:

  • 改入口参数:让它查不到真实状态
  • 改中间过程:比如改摘要、改签名数组
  • 改最终结果:直接把校验函数返回 true

实战里我更推荐先从最终结果下手,因为:

  • 改动最少
  • 风险小
  • 更容易验证
  • 不容易引发连锁副作用

用 JADX 做静态定位

拿到 APK 后,先用 JADX 打开。不要急着全文乱搜,我一般按下面的顺序查。

1. 先搜高频关键词

优先搜索这些词:

  • getPackageInfo
  • GET_SIGNATURES
  • GET_SIGNING_CERTIFICATES
  • signatures
  • signingInfo
  • MessageDigest
  • SHA1
  • SHA-1
  • SHA256
  • md5
  • Signature
  • certificate
  • debuggable
  • tamper
  • 安全
  • 签名

2. 关注这些可疑代码形态

例如:

public static boolean checkSignature(Context context) {
    try {
        PackageManager pm = context.getPackageManager();
        PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 64);
        Signature[] signatures = pi.signatures;
        MessageDigest md = MessageDigest.getInstance("SHA1");
        md.update(signatures[0].toByteArray());
        String hex = bytesToHex(md.digest());
        return "12AB34CD56EF7890".equalsIgnoreCase(hex);
    } catch (Exception e) {
        return false;
    }
}

这个就很典型:

  • getPackageInfo(..., 64)64 就是旧版 GET_SIGNATURES
  • 取签名字节
  • 做摘要
  • 与常量比对
  • 返回 boolean

这类函数几乎就是理想 hook 点。

3. 反查调用链

找到校验函数后,继续看:

  • 谁调用了它?
  • 是在 Application.attach() 里调用?
  • 还是 MainActivity.onCreate()
  • 有没有在按钮点击、网络请求前调用?

如果函数名混淆严重,不要怕,重点看参数类型调用链位置


典型校验流程图

flowchart TD
    A[App 启动/进入功能页] --> B[调用签名校验函数]
    B --> C[PackageManager 获取签名信息]
    C --> D[MessageDigest 计算摘要]
    D --> E[与内置摘要比对]
    E -->|匹配| F[继续执行]
    E -->|不匹配| G[弹窗/闪退/禁用功能]

动态分析思路:先确认,再绕过

静态看到了可疑函数,不代表它一定执行。下一步要做的是:用 Frida 动态确认

一条经验

我踩过一个很常见的坑:JADX 里找到一个非常像的校验函数,结果 hook 半天没反应。后来发现那是旧版本遗留代码,真正执行的是另一个混淆类里的实现。

所以别上来就“改返回值”,先打印日志确认函数是否被调用。


实战代码(可运行)

下面给一个完整示例。假设我们在 JADX 里定位到:

  • 类名:com.demo.security.SignCheck
  • 方法:public static boolean checkSignature(android.content.Context context)

示例一:直接 hook 最终返回值

Java.perform(function () {
    var SignCheck = Java.use('com.demo.security.SignCheck');

    SignCheck.checkSignature.overload('android.content.Context').implementation = function (context) {
        console.log('[*] checkSignature called');
        var original = this.checkSignature(context);
        console.log('[*] original result = ' + original);
        console.log('[*] force return true');
        return true;
    };
});

启动方式:

frida -U -f com.demo.app -l hook_sign.js

如果不需要 spawn,也可以 attach:

frida -U com.demo.app -l hook_sign.js

示例二:hook PackageManager.getPackageInfo

有些 App 会在多个地方做签名校验,逐个改业务函数太慢。这时候可以直接盯底层 API。

Java.perform(function () {
    var ApplicationPackageManager = Java.use('android.app.ApplicationPackageManager');

    ApplicationPackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function (pkg, flags) {
        console.log('[*] getPackageInfo called');
        console.log('    pkg   = ' + pkg);
        console.log('    flags = ' + flags);
        return this.getPackageInfo(pkg, flags);
    };
});

这个脚本先用于观察:

  • 哪个包名被查询
  • 使用了什么 flags
  • 是否在启动早期触发

如果你已经确认某个路径稳定,再进一步 hook 上层校验逻辑会更安全。


示例三:hook 摘要比对函数

有的应用会把签名摘要封装进工具类,例如:

public static boolean equalsExpected(String digest)

那么可以直接改这里:

Java.perform(function () {
    var Utils = Java.use('com.demo.security.SecurityUtils');

    Utils.equalsExpected.overload('java.lang.String').implementation = function (digest) {
        console.log('[*] equalsExpected called, digest=' + digest);
        return true;
    };
});

这种方式的优点是:

  • 不碰系统 API
  • 对业务影响范围小
  • 成功率通常不错

示例四:通用打印签名摘要,辅助确认目标值

如果你还没找到比对常量,可以先把真实签名摘要打印出来。

Java.perform(function () {
    var MessageDigest = Java.use('java.security.MessageDigest');
    var Integer = Java.use('java.lang.Integer');
    var StringCls = Java.use('java.lang.String');

    function toHex(bytes) {
        var result = '';
        for (var i = 0; i < bytes.length; i++) {
            var v = bytes[i];
            if (v < 0) v += 256;
            var hv = v.toString(16);
            if (hv.length < 2) hv = '0' + hv;
            result += hv;
        }
        return result;
    }

    MessageDigest.digest.overload().implementation = function () {
        var ret = this.digest();
        try {
            var algo = this.getAlgorithm();
            console.log('[*] MessageDigest.digest algo=' + algo + ', hex=' + toHex(ret));
        } catch (e) {
            console.log('[!] print digest error: ' + e);
        }
        return ret;
    };
});

这个脚本不是专门绕过,而是用来帮助你定位摘要值和算法类型


更完整的定位与绕过流程

sequenceDiagram
    participant R as 逆向分析者
    participant J as JADX
    participant F as Frida
    participant A as App

    R->>J: 搜索签名相关 API/常量
    J-->>R: 返回可疑校验函数与调用链
    R->>F: 编写日志型 hook
    F->>A: 注入并观察方法调用
    A-->>F: 输出命中日志与参数
    R->>F: 修改为返回值绕过
    F->>A: 强制校验通过
    A-->>R: 功能恢复/不再闪退

一个可复现的教学示例

下面给一个简化版 Android Java 校验函数,便于你理解 hook 点。

目标代码示例

package com.demo.security;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;

import java.security.MessageDigest;

public class SignCheck {
    public static boolean checkSignature(Context context) {
        try {
            PackageInfo pi = context.getPackageManager().getPackageInfo(
                    context.getPackageName(),
                    PackageManager.GET_SIGNATURES
            );
            Signature signature = pi.signatures[0];
            byte[] cert = signature.toByteArray();
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            byte[] digest = md.digest(cert);

            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02X", b));
            }

            return "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".equals(sb.toString());
        } catch (Exception e) {
            return false;
        }
    }
}

对应 Frida 绕过脚本

Java.perform(function () {
    var SignCheck = Java.use('com.demo.security.SignCheck');

    SignCheck.checkSignature.overload('android.content.Context').implementation = function (context) {
        console.log('[*] bypass SignCheck.checkSignature');
        return true;
    };
});

运行:

frida -U -f com.demo.app -l bypass.js --no-pause

逐步验证清单

为了避免“看起来 hook 了,实际上没生效”,建议按这个清单走。

第一步:确认进程注入成功

frida-ps -U | grep demo

或者直接看命令行输出里有没有你写的日志。

第二步:确认类名和方法签名无误

如果报错:

  • ClassNotFoundException
  • TypeError: cannot find overload

优先检查:

  • 混淆后类名是否真实
  • 方法是否静态/实例
  • 参数类型是否匹配
  • 是否存在多个重载

第三步:先打印原始返回值

不要一开始就全改,先看:

  • 方法是不是被调用了
  • 原始返回值是什么
  • 调用频率是否异常

第四步:改返回值后验证业务结果

验证不应该只看“没报错”,而要看:

  • 页面是否正常打开
  • 功能按钮是否恢复
  • 是否还有后续二次校验
  • 是否存在延迟检测

常见坑与排查

这一节很重要,因为很多问题不是“不会写 hook”,而是“hook 得太晚、hook 错层、被混淆误导”。

1. hook 时机太晚

如果签名校验发生在 Application.attach() 甚至更早,attach 方式可能来不及。

解决办法:

frida -U -f com.demo.app -l hook_sign.js --no-pause

spawn 模式,让脚本在应用主逻辑执行前注入。


2. 方法重载选错

例如:

checkSignature(Context)
checkSignature(Context, String)

这时候必须明确 overload(...)

错误示例:

SignCheck.checkSignature.implementation = function (context) {
    return true;
};

更稳妥的写法:

SignCheck.checkSignature.overload('android.content.Context').implementation = function (context) {
    return true;
};

3. 混淆后函数名毫无语义

遇到 a.a.a.a() 这类代码很正常。不要依赖函数名,改看:

  • 参数类型
  • 返回值类型
  • 调用位置
  • 内部是否调用 PackageManager
  • 是否出现摘要算法字符串

4. 校验在 Native 层

Java 层怎么改都不生效,可能原因之一是:

  • Java 只是做表面检查
  • 真正判定在 so 里
  • Java 和 Native 双重校验

这时可以先定位 JNI 方法,例如在 JADX 中搜索:

  • System.loadLibrary
  • native
  • JNI
  • registerNatives

再决定是否继续用 Frida hook native 导出符号或 JNI 桥接函数。


5. 不是签名校验,是完整性联动检测

有些异常现象看起来像签名校验,实际是:

  • root 检测
  • 调试检测
  • 模拟器检测
  • Xposed/Frida 检测
  • dex/so 完整性校验

如果你绕过了一个函数,但 App 还是崩,说明可能还有联动检测链路。


6. Android 版本差异

不同 Android 版本签名 API 不完全一样:

  • 旧版:GET_SIGNATURES
  • 新版:GET_SIGNING_CERTIFICATES

如果只盯一个 API,容易漏掉目标实现。


校验逻辑的常见实现分类

classDiagram
    class SignatureCheck {
        +checkByPackageManager()
        +checkByDigest()
        +checkByNative()
        +checkByServer()
    }

    class PackageManagerPath {
        +getPackageInfo()
        +read signatures/signingInfo
    }

    class DigestPath {
        +MessageDigest SHA1/SHA256/MD5
        +bytesToHex()
        +equals()
    }

    class NativePath {
        +JNI bridge
        +libxxx.so verify
    }

    class ServerPath {
        +upload local fingerprint
        +server decision
    }

    SignatureCheck --> PackageManagerPath
    SignatureCheck --> DigestPath
    SignatureCheck --> NativePath
    SignatureCheck --> ServerPath

安全/性能最佳实践

虽然我们在讲绕过,但做分析脚本时也要讲方法,不然很容易把目标进程搞崩。

1. 优先最小化 hook 范围

我的建议是:

  • 先 hook 单个业务函数
  • 再考虑 hook 通用工具类
  • 最后才去动系统 API

原因很简单:越底层,影响面越大。

例如直接 hook MessageDigest.digest(),虽然好用,但会打印大量日志,甚至影响加密、网络、登录等其他逻辑。


2. 日志要适度

不要一上来在高频函数里疯狂 console.log()。某些函数调用量非常大,日志会:

  • 拖慢 App
  • 增加卡顿
  • 干扰时序
  • 导致你误判“App 自己不稳定”

建议加过滤条件,例如只关注目标包名、目标算法、目标线程。


3. 保留原始返回值用于比对

一个很实用的习惯是:

  • 先打印原始值
  • 再决定强改
  • 必要时加开关控制

例如:

var forceBypass = true;

这样你可以快速切换“观察模式”和“绕过模式”。


4. 对异常做兜底

脚本尽量别因为一个打印失败就中断:

try {
    // print something
} catch (e) {
    console.log(e);
}

特别是 hook 系统类、数组、字节流时,容错很重要。


5. 面对双重校验时,先找最终决策点

有些 App 会同时做:

  • 本地签名校验
  • Native 指纹校验
  • 服务端授权校验

这时最有效的策略通常不是三个都硬怼,而是先找最终阻断功能的决策点,比如:

  • if (!envOk) return;
  • showRiskDialog();
  • finish();
  • System.exit(0);

从结果反推,比逐个层面穷举更高效。


边界条件:什么时候“绕过了本地签名校验”也没用?

这个问题很现实,值得提前讲清楚。

以下场景中,即使你本地绕过成功,也不一定能拿到完整功能:

  1. 服务端校验证书指纹
  2. 关键参数带签名并在服务端验签
  3. Native 层做核心逻辑,Java 层只是壳
  4. 完整性检测结果被上报服务器
  5. 存在 Frida/Xposed 检测并触发风控

所以本文的方法更适合:

  • 定位本地保护逻辑
  • 验证防护强度
  • 做功能可达性分析
  • 安全评估与研究

不意味着可以替代全部链路分析。


一个更稳的 Frida 模板

最后给一个我在实战里比较常用的模板:先枚举类是否存在,再 hook,避免脚本启动即报错。

Java.perform(function () {
    var targetClass = 'com.demo.security.SignCheck';

    try {
        var SignCheck = Java.use(targetClass);
        console.log('[*] found class: ' + targetClass);

        var overload = SignCheck.checkSignature.overload('android.content.Context');
        overload.implementation = function (context) {
            console.log('[*] checkSignature hit');
            var result = overload.call(this, context);
            console.log('[*] original = ' + result);
            return true;
        };
    } catch (e) {
        console.log('[!] hook failed: ' + e);
    }
});

这个模板的好处是:

  • 出错信息更明确
  • 保留原始调用结果
  • 后续便于扩展成条件绕过

总结

JADX + Frida 处理 Android 签名校验,最实用的路线其实很朴素:

  1. JADX 静态搜索高频 API 和摘要比对逻辑
  2. 沿调用链找到真正生效的校验点
  3. Frida 先打印日志确认命中
  4. 优先修改最终返回值,做最小绕过
  5. 如果无效,再考虑 Native 层或联动检测

如果你是中级读者,我建议你把这套方法沉淀成自己的分析套路,而不是记几个孤立的 hook 片段。因为现实里的 App 会混淆、会分层、会多重校验,但定位思路和验证方法其实是共通的。

最后给几条可执行建议:

  • 首选 spawn 注入,避免 hook 太晚
  • 静态定位不要只看函数名,要看 API、常量、调用链
  • 绕过时尽量从 最终决策点 下手
  • 如果 Java 层无效,及时怀疑 JNI/so/服务端联动
  • 每次只改一个点,确保你知道“到底是哪一步生效了”

只要你能把“静态定位 + 动态验证 + 最小改动”这三步跑顺,常见的签名校验逻辑基本都能较高效地拆开看清楚。


分享到:

上一篇
《Spring Boot 中基于 Spring Cache + Redis 实现多级缓存与缓存一致性的实战指南》
下一篇
《Spring Boot 中基于 Spring Cache + Redis 的多级缓存实战:一致性、穿透防护与性能调优》