26、应用完整性校验绕过:签名校验、Hash校验、Frida Hook绕过与重打包
各位同学,今天我们来聊一个实战中绕不开的话题——完整性校验。说白了,就是应用怎么判断自己有没有被篡改过。你改了他的代码,重新打包,结果一运行就闪退,或者弹个窗说「应用已损坏」——这就是校验在起作用。
我早期做逆向的时候,第一次遇到签名校验,愣是折腾了两天。后来摸清了门道,才发现这东西其实就那几招。今天我把这些套路全盘托出,你学会了,以后遇到类似问题就能快速搞定。
核心思路:完整性校验的本质是「应用自检」。它检查签名、检查文件Hash、检查资源完整性。我们要做的,就是让这个自检过程「睁一只眼闭一只眼」。
26.1 签名校验的原理与绕过
Android应用的签名机制,说白了就是开发者用私钥给APK打了个标记。系统安装时验证这个标记,确保APK没被篡改。但很多应用会在运行时自己再校验一遍——这就是我们要对付的。
签名校验的常见实现方式有几种:
- PackageManager.getPackageInfo():获取当前应用的签名信息,与内置的签名Hash对比
- 读取APK文件:从/data/app/目录读取APK,自己解析签名块
- JNI层校验:在Native层通过C/C++代码校验签名,增加逆向难度
我记得有个项目,应用在Java层和Native层做了双重签名校验。Java层过了,Native层又弹窗。当时我用了两种方法组合才搞定。
绕过方法一:Hook关键API
最直接的方式,就是用Frida Hook掉签名校验相关的API。比如:
// Hook PackageManager.getPackageInfo
Java.perform(function() {
var PackageManager = Java.use('android.content.pm.PackageManager');
PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(packageName, flags) {
console.log('[+] getPackageInfo called: ' + packageName);
var result = this.getPackageInfo(packageName, flags);
// 修改签名信息
if (result != null && result.signatures != null) {
result.signatures[0] = originalSignature; // 替换为原始签名
}
return result;
};
});
小技巧:有时候应用会校验签名数组的长度。你不仅要替换签名值,还要确保数组长度一致。我遇到过因为数组长度不对导致崩溃的情况。
绕过方法二:修改smali代码
如果Hook不管用,那就直接改代码。反编译APK,找到签名校验的逻辑,把校验结果改成始终返回true。比如:
// 原始代码
if (checkSignature() == false) {
showErrorDialog();
finish();
}
// 修改后
if (checkSignature() == false) {
// showErrorDialog(); // 注释掉
// finish(); // 注释掉
}
嗯,这里要注意。有些应用会把校验逻辑写在Application的onCreate里,一启动就校验。你得找到那个入口点,把校验跳过去。
26.2 Hash校验的原理与绕过
Hash校验是另一种常见手段。应用在安装后,计算自身某些文件的Hash值,跟内置的Hash对比。如果不一样,就认为被篡改了。
常见的Hash校验目标包括:
| 校验目标 | 说明 | 绕过难度 |
|---|---|---|
| classes.dex | 校验主DEX文件的Hash | 中等 |
| AndroidManifest.xml | 校验清单文件的完整性 | 低 |
| assets目录下的文件 | 校验资源文件的Hash | 中等 |
| lib目录下的so文件 | 校验Native库的Hash | 高 |
我建议你遇到Hash校验时,先定位校验点。用Frida的Stalker或者简单的日志Hook,看应用在什么时候、对什么文件做了Hash计算。
绕过方法:Hook Hash函数
应用计算Hash时,通常会调用MessageDigest类。我们可以Hook这个类:
Java.perform(function() {
var MessageDigest = Java.use('java.security.MessageDigest');
MessageDigest.digest.overload().implementation = function() {
var result = this.digest();
console.log('[+] digest called, result length: ' + result.length);
// 返回原始文件的Hash值
return originalHashBytes;
};
});
注意:有些应用会自己实现Hash算法,不依赖Java标准库。这种情况下,你得分析它的算法实现,然后修改计算结果。我曾经遇到一个应用,用Native层自己写的SHA-1,调试了好久才找到关键跳转。
26.3 Frida Hook绕过实战
Frida是我们绕过校验的利器。但有些应用会检测Frida的存在,比如检查端口、检查进程名、检查Dex加载等。这时候就需要「反检测」了。
我常用的Frida绕过技巧:
- 使用Frida的spawn模式:在应用启动前注入,避免被检测
- 修改Frida默认端口:用-f参数指定端口,避开默认的27042
- 隐藏Frida特征:使用Frida的Gadget模式,或者用定制版Frida
举个例子,如果应用检测了/proc/self/maps中是否有frida-agent,我们可以这样绕过:
// Hook文件读取操作
Java.perform(function() {
var FileInputStream = Java.use('java.io.FileInputStream');
FileInputStream.read.overload('[B').implementation = function(buffer) {
var result = this.read(buffer);
// 检查读取的内容是否包含frida特征
var content = String.fromCharCode.apply(null, buffer);
if (content.indexOf('frida') !== -1) {
console.log('[+] Detected frida scan, bypassing...');
// 返回空数据或伪造数据
return -1;
}
return result;
};
});
避坑指南:我曾经在某个加固应用上折腾了一整天,Frida怎么都连不上。后来发现是应用在Native层用ptrace反调试了。解决办法是用Frida的spawn模式,在应用启动前就注入,绕过ptrace检测。
26.4 重打包与签名修复
绕过校验之后,最后一步就是重打包。但重打包后签名会变,所以我们需要修复签名。
重打包的标准流程:
- 反编译APK(apktool d target.apk)
- 修改代码或资源
- 回编译APK(apktool b target -o modified.apk)
- 生成签名密钥(keytool -genkey)
- 签名APK(jarsigner或apksigner)
- 对齐(zipalign)
但这里有个关键问题:如果应用做了签名校验,你用新签名打包后,校验会失败。所以必须在重打包前,先把签名校验绕过。
我个人的习惯是:先分析校验逻辑,用Frida Hook验证绕过方案是否有效,然后再进行重打包。这样能避免反复打包测试的麻烦。
核心要点:重打包不是终点,绕过校验才是。你改了代码,但校验没过,应用照样闪退。所以顺序很重要——先绕过,再修改,最后打包。
26.5 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张流程图:
这张图把三种校验方式以及对应的绕过方法都串起来了。你从左边开始,根据应用实际使用的校验方式,选择对应的绕过路径。最后都汇聚到重打包和签名修复这一步。
好了,关于完整性校验绕过,核心内容就这些。你想想看,其实不管应用怎么校验,本质都是在某个点做了「判断」。我们只要找到这个判断点,让它永远返回「通过」就行了。说起来简单,做起来需要耐心和细心。
我建议你找个实际的应用练练手。先从简单的签名校验开始,逐步挑战更复杂的Hash校验和反Frida检测。踩过的坑越多,经验就越丰富。