完整性校验:签名校验、Hash校验、资源完整性校验、绕过方案

大家好,我是你们的老朋友。今天我们来聊聊完整性校验这个话题。说实话,这是移动安全里最基础但也最容易被忽视的一环。我见过太多App,功能做得花里胡哨,结果连最基本的签名校验都没做,被人一把梭就脱了壳改了包。

完整性校验,说白了就是App在运行时检查自己有没有被篡改。你想想看,如果App自己都不检查自己是不是原装的,那逆向工程师改起来岂不是跟玩似的?

签名校验

签名校验是最常见的一种完整性校验方式。Android系统本身就有签名机制,每个App都必须用开发者证书签名才能安装。系统在安装时会校验签名,但运行时呢?系统可不管了。

所以很多App会在代码里自己再做一次签名校验。大致流程是这样的:

// 伪代码示例
public boolean checkSignature() {
    PackageManager pm = getPackageManager();
    PackageInfo pi = pm.getPackageInfo(
        getPackageName(), 
        PackageManager.GET_SIGNATURES
    );
    Signature[] sigs = pi.signatures;
    // 与预埋的签名哈希对比
    return compareWithStoredHash(sigs[0].toCharsString());
}

嗯,这里要注意。这种校验方式有个致命弱点——校验逻辑本身也在代码里。逆向工程师只要找到这个函数,直接nop掉或者hook掉返回值就行了。

核心要点:签名校验的强度不在于算法多复杂,而在于校验点藏得有多深、有多少个。

我在项目中遇到过一家金融App,他们把签名校验分散在十几个地方,有的在native层,有的在Java层,甚至还有个校验藏在so文件的.init_array段里。说实话,当时我也花了整整两天才全部清掉。

Hash校验

Hash校验比签名校验更灵活。它不校验签名,而是直接校验文件内容的哈希值。常见的做法是对Dex文件、So文件或者资源文件计算哈希,然后跟预埋的值对比。

举个例子:

// 对classes.dex做哈希校验
public boolean checkDexHash() {
    File dexFile = new File(
        getApplicationInfo().sourceDir
    );
    byte[] dexBytes = readFile(dexFile);
    String hash = sha256(dexBytes);
    return hash.equals(EXPECTED_HASH);
}

Hash校验有个好处——它不依赖系统API,不容易被全局hook。但坏处也很明显:预埋的哈希值就写在代码里,找到就能改。

我的建议:不要把哈希值明文写在代码里。可以拆分成多段,运行时拼接;或者用XOR加密一下。虽然不能完全防住,但至少能挡住80%的脚本小子。

我曾经见过一个App,把哈希值藏在so文件的某个偏移量里,还做了自修改校验。每次启动时先计算自身哈希,跟预埋值对比,如果不对就直接crash。这种方案虽然麻烦,但确实有效。

资源完整性校验

资源完整性校验,主要是检查图片、布局文件、配置文件这些有没有被改过。为什么要检查资源?因为很多逆向工具会通过修改资源文件来注入恶意代码或者去广告。

常见的资源校验方式:

  • 文件大小校验:检查资源文件的大小是否跟原始一致
  • CRC校验:对资源文件计算CRC32值
  • 内容校验:检查关键资源文件的内容是否被篡改

我记得有个游戏App,他们把游戏资源打包成加密的zip文件,启动时先解密再校验。如果校验失败,就弹个框说「资源损坏,请重新安装」。这种方案对普通用户来说确实够用了。

注意:资源校验不能只检查一两个文件。逆向工程师通常会先改小文件试水,如果没被发现,就会逐步扩大修改范围。建议对关键资源做全覆盖校验。

绕过方案

说了这么多校验方式,那怎么绕过呢?我整理了几种常见的绕过思路:

绕过方式 原理 适用场景
Hook返回值 用Xposed/Frida hook校验函数,直接返回true Java层校验
Patch二进制 直接修改smali代码,把校验逻辑跳过去 无加固App
内存修改 在运行时修改内存中的校验结果 native层校验
重打包 修改后重新签名,同时清理掉校验代码 所有场景

说白了,绕过完整性校验的核心思路就两个:要么让校验永远返回成功,要么让校验代码根本不执行。

我曾经遇到过一个App,他们把校验逻辑放在Application的attachBaseContext里,而且用了反射调用。我当时第一反应是直接hook Application的onCreate,在它执行校验之前就把结果改了。嗯,这招确实管用。

知识体系结构

下面这张图是我整理的完整性校验知识体系,大家可以对照着看:

完整性校验 签名校验 Hash校验 资源完整性校验 Java层校验 Native层校验 Dex文件校验 So文件校验 图片/布局校验 配置文件校验 Hook返回值 Patch二进制 内存修改 重打包 资源替换 解密后修改 校验与绕过,是一场永无止境的攻防博弈

从这张图可以看出来,完整性校验其实是一个体系化的东西。签名校验、Hash校验、资源校验各有侧重,也各有弱点。作为逆向工程师,我们需要理解每种校验的原理,才能找到对应的绕过方案。

避坑指南:我曾经犯过一个错误——只关注了Java层的校验,忽略了native层。结果改完包一运行,直接crash。后来才发现so文件里还有个校验点。所以我的建议是:先全局搜索所有跟校验相关的关键词,再逐个击破。

好了,关于完整性校验就聊这么多。记住一句话:没有绝对安全的校验,只有相对难绕过的方案。作为逆向工程师,我们的目标不是让App绝对安全,而是让破解成本高于破解收益。

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