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 重打包与签名修复

绕过校验之后,最后一步就是重打包。但重打包后签名会变,所以我们需要修复签名。

重打包的标准流程:

  1. 反编译APK(apktool d target.apk)
  2. 修改代码或资源
  3. 回编译APK(apktool b target -o modified.apk)
  4. 生成签名密钥(keytool -genkey)
  5. 签名APK(jarsigner或apksigner)
  6. 对齐(zipalign)

但这里有个关键问题:如果应用做了签名校验,你用新签名打包后,校验会失败。所以必须在重打包前,先把签名校验绕过。

我个人的习惯是:先分析校验逻辑,用Frida Hook验证绕过方案是否有效,然后再进行重打包。这样能避免反复打包测试的麻烦。

核心要点:重打包不是终点,绕过校验才是。你改了代码,但校验没过,应用照样闪退。所以顺序很重要——先绕过,再修改,最后打包。

26.5 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张流程图:

应用完整性校验绕过知识体系 签名校验 Hash校验 Frida Hook绕过 Hook PackageManager 修改smali代码 JNI层Hook Hook MessageDigest 修改文件Hash值 Native层算法分析 spawn模式注入 修改默认端口 隐藏Frida特征 重打包 → 签名修复 → 验证通过

这张图把三种校验方式以及对应的绕过方法都串起来了。你从左边开始,根据应用实际使用的校验方式,选择对应的绕过路径。最后都汇聚到重打包和签名修复这一步。

好了,关于完整性校验绕过,核心内容就这些。你想想看,其实不管应用怎么校验,本质都是在某个点做了「判断」。我们只要找到这个判断点,让它永远返回「通过」就行了。说起来简单,做起来需要耐心和细心。

我建议你找个实际的应用练练手。先从简单的签名校验开始,逐步挑战更复杂的Hash校验和反Frida检测。踩过的坑越多,经验就越丰富。