一、Android 完整性校验:应用签名校验原理

说到 Android 应用的完整性校验,签名校验是绕不开的第一道坎。我个人习惯把签名校验理解为应用的「身份证验证系统」——系统在安装或运行应用时,会检查这个 APK 是不是由开发者本人签发的,有没有被篡改过。

1.1 签名校验的底层逻辑

Android 的签名机制其实不复杂。开发者用私钥对 APK 内容签名,系统用公钥验证。具体流程是这样的:

  • 签名阶段:开发者对 APK 中的每个文件计算哈希,生成签名文件(META-INF 目录下的 .RSA、.SF、.MF 文件)
  • 验证阶段:系统安装时读取签名,用公钥解密,对比哈希值是否一致
  • 运行时校验:部分应用会在代码中主动调用 PackageManager.getPackageInfo() 获取签名信息,与硬编码的签名做比对

核心要点:签名校验的本质是「信任链」——系统信任的是原始开发者的私钥。一旦 APK 被重新打包,签名就会改变,系统或应用就会拒绝执行。

1.2 常见的签名校验实现方式

我在项目中遇到过好几种签名校验的实现,这里列一下最常见的:

校验方式 实现原理 绕过难度
系统级校验 Android 系统安装时自动验证 低(重签名即可)
代码中获取签名对比 调用 getPackageInfo().signatures 比对 中(需要 Hook)
JNI 层签名校验 在 native 层获取签名并校验 高(需要逆向 so 文件)
服务端签名校验 将签名发送到服务器验证 极高(需要抓包修改)

我的经验:大部分应用用的是第二种方式——代码中获取签名对比。为什么?因为实现简单,而且对性能影响小。但说实话,这种方式也是最容易被 Hook 绕过的。

二、完整性校验:Hash 校验与文件校验

除了签名校验,很多应用还会做额外的完整性校验。说白了就是「看看我的代码有没有被人动过手脚」。

2.1 Hash 校验

Hash 校验的原理很简单:应用在启动时,对自身的关键文件(比如 classes.dex、AndroidManifest.xml)计算 MD5 或 SHA1,然后跟预置的哈希值做对比。

// 典型的 Hash 校验代码
private boolean checkIntegrity() {
    try {
        String apkPath = getApplicationContext().getPackageCodePath();
        File apkFile = new File(apkPath);
        String md5 = getFileMD5(apkFile);
        // 预置的哈希值
        String expectedMd5 = "a1b2c3d4e5f6...";
        return md5.equals(expectedMd5);
    } catch (Exception e) {
        return false;
    }
}

你想想看,这种校验有个致命问题——预置的哈希值就写在代码里。我只要找到这个字符串,把它改成新 APK 的哈希值,校验就过了。

2.2 文件校验

文件校验比 Hash 校验稍微复杂一点。应用会检查关键文件的修改时间、文件大小、CRC 校验值等属性。我记得有个金融类应用,它会检查 classes.dex 的文件大小,如果跟原始大小不一致就直接闪退。

避坑指南:我曾经遇到过一个应用,它在多个地方做了文件校验——Java 层、Native 层、甚至通过反射调用了系统 API。这种「多重校验」才是最头疼的,你只绕过一处根本没用。

三、Hook 绕过签名校验

好,现在进入实战环节。怎么绕过签名校验?我个人最常用的方法是 Hook。

3.1 使用 Frida Hook 签名校验

Frida 是我最顺手的工具。它的核心思路是:在运行时拦截目标函数的调用,修改返回值。

// Frida 脚本:绕过签名校验
Java.perform(function() {
    var PackageManager = Java.use('android.content.pm.PackageManager');
    var PackageInfo = Java.use('android.content.pm.PackageInfo');
    var Signature = Java.use('android.content.pm.Signature');

    PackageManager.getPackageInfo.implementation = function(packageName, flags) {
        var result = this.getPackageInfo(packageName, flags);
        // 替换为原始应用的签名
        var originalSignature = "3082..."; // 原始签名的 hex 值
        result.signatures[0] = Signature.$new(originalSignature);
        return result;
    };
});

这段代码做了什么?它 Hook 了 getPackageInfo 方法,当应用获取自身签名时,我们返回的是原始签名,而不是重打包后的新签名。应用拿到「正确」的签名,自然就放行了。

3.2 Xposed 方案

如果你用的是 Xposed,思路是一样的,只是写法不同:

XposedHelpers.findAndHookMethod(
    "android.content.pm.PackageManager",
    lpparam.classLoader,
    "getPackageInfo",
    String.class, int.class,
    new XC_MethodHook() {
        @Override
        protected void afterHookedMethod(MethodHookParam param) {
            PackageInfo info = (PackageInfo) param.getResult();
            info.signatures[0] = originalSignature;
        }
    }
);

关键点:Hook 签名校验的核心是「欺骗」——让应用以为它还是原来的那个它。不管是 Frida 还是 Xposed,原理都一样。

四、重打包与重新签名

绕过校验只是第一步。真正的目标是:修改应用逻辑后,让它还能正常运行。

4.1 重打包流程

重打包的流程其实很固定,我一般这么操作:

  1. 反编译:用 apktool 或 jadx 把 APK 解包
  2. 修改代码:修改 smali 代码或替换资源文件
  3. 重新打包:用 apktool 重新打包成 APK
  4. 重新签名:用 jarsigner 或 apksigner 签名
  5. 对齐优化:用 zipalign 对齐(Android 4.0+ 可选)
# 重打包命令示例
# 1. 反编译
apktool d target.apk -o output_dir

# 2. 修改代码(略)

# 3. 重新打包
apktool b output_dir -o modified.apk

# 4. 生成签名密钥(如果没有)
keytool -genkey -alias mykey -keyalg RSA -keystore my.keystore

# 5. 重新签名
jarsigner -verbose -keystore my.keystore modified.apk mykey

# 6. 对齐(可选)
zipalign -v 4 modified.apk final.apk

4.2 签名方案的演进

这里有个坑要注意。Android 的签名方案从 v1 发展到了 v4:

签名方案 引入版本 特点
v1 (JAR 签名) Android 1.0 基于 META-INF 文件,不保护 APK 内容
v2 (APK 签名方案) Android 7.0 保护整个 APK 内容,更安全
v3 (APK 签名方案 v3) Android 9.0 支持密钥轮换
v4 (APK 签名方案 v4) Android 11.0 增量更新支持

注意:如果你用 apktool 重打包,默认只保留 v1 签名。对于 Android 7.0+ 的应用,必须用 apksigner 同时添加 v1 和 v2 签名,否则安装时会报错。

五、实战:修改应用逻辑后成功重打包运行

光说不练假把式。我们拿一个实际案例来走一遍完整流程。

5.1 场景描述

假设有个应用,它有个「VIP 功能」的开关。我们想把这个开关打开,让普通用户也能用 VIP 功能。

5.2 定位关键代码

用 jadx 打开 APK,搜索关键词 "VIP" 或 "vip",找到关键判断逻辑:

// 原始代码
public boolean isVip() {
    return getSharedPreferences("user", 0)
        .getBoolean("is_vip", false);
}

这个方法的返回值决定了用户是否是 VIP。我们只需要让它永远返回 true。

5.3 修改 smali 代码

反编译后找到对应的 smali 文件,修改如下:

# 原始 smali
.method public isVip()Z
    .locals 2
    const/4 v0, 0x0
    return v0
.end method

# 修改后
.method public isVip()Z
    .locals 1
    const/4 v0, 0x1
    return v0
.end method

看到了吗?我把 const/4 v0, 0x0 改成了 const/4 v0, 0x1,返回值从 false 变成了 true。

5.4 绕过签名校验

这个应用有签名校验。我们用 Frida 脚本绕过:

// bypass_signature.js
Java.perform(function() {
    var PackageManager = Java.use('android.content.pm.PackageManager');
    PackageManager.getPackageInfo.implementation = function(name, flags) {
        var result = this.getPackageInfo(name, flags);
        // 这里填入原始应用的签名哈希值
        result.signatures[0].hashCode = function() { return 123456; };
        return result;
    };
});

5.5 重打包与安装

最后一步,重打包、签名、安装:

# 重打包
apktool b modified_app -o vip_modified.apk

# 签名(同时支持 v1 和 v2)
apksigner sign --ks my.keystore --v1-signing-enabled true --v2-signing-enabled true vip_modified.apk

# 安装
adb install vip_modified.apk

我的建议:安装前先用 apksigner verify 检查签名是否完整。我吃过这个亏——签名没弄好,安装时直接报「解析包时出现问题」。

六、知识体系总览

下面这张图总结了本章的核心知识结构,你可以把它当作一个快速参考:

Android 完整性校验与绕过知识体系 完整性校验 签名校验 系统级校验 代码级校验 重签名即可绕过 需要 Hook 绕过 完整性校验 Hash 校验 文件校验 修改预置哈希值 修改校验逻辑 绕过方法 Frida / Xposed Hook 重打包 + 重新签名 核心思路:欺骗应用,让它以为自己是原始版本

这张图把本章的内容串起来了。从左到右看:先理解校验机制(签名 + 完整性),再掌握绕过方法(Hook + 重打包)。说白了就是「知己知彼」——知道应用怎么检查你,你才知道怎么骗过它。

最后说一句:完整性校验与绕过是一场持续的攻防博弈。应用开发者会不断加固,逆向工程师会不断寻找新的绕过方式。但万变不离其宗——只要应用在本地做校验,就一定有办法绕过。区别只在于「需要花多少功夫」。


公众号:蓝海资料掘金营,微信deep3321