一、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 重打包流程
重打包的流程其实很固定,我一般这么操作:
- 反编译:用 apktool 或 jadx 把 APK 解包
- 修改代码:修改 smali 代码或替换资源文件
- 重新打包:用 apktool 重新打包成 APK
- 重新签名:用 jarsigner 或 apksigner 签名
- 对齐优化:用 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 检查签名是否完整。我吃过这个亏——签名没弄好,安装时直接报「解析包时出现问题」。
六、知识体系总览
下面这张图总结了本章的核心知识结构,你可以把它当作一个快速参考:
这张图把本章的内容串起来了。从左到右看:先理解校验机制(签名 + 完整性),再掌握绕过方法(Hook + 重打包)。说白了就是「知己知彼」——知道应用怎么检查你,你才知道怎么骗过它。
最后说一句:完整性校验与绕过是一场持续的攻防博弈。应用开发者会不断加固,逆向工程师会不断寻找新的绕过方式。但万变不离其宗——只要应用在本地做校验,就一定有办法绕过。区别只在于「需要花多少功夫」。
公众号:蓝海资料掘金营,微信deep3321