Android | 签名安全

发布于:2025-06-21 ⋅ 阅读:(20) ⋅ 点赞:(0)

检验和签名

校验开发者在数据传送时采用的一种校正数据的一种方式, 常见的校验有:签名校验(最常见)、dexcrc校验、apk完整性校验、路径文件校验等。

通过对 Apk 进行签名,开发者可以证明对 Apk 的所有权和控制权,可用于安装和更新其应用。而在 Android 设备上的安装 Apk ,如果是一个没有被签名的 Apk,则会被拒绝安装。在安装 Apk 的时候,软件包管理器也会验证 Apk 是否已经被正确签名,并且通过签名证书和数据摘要验证是否合法没有被篡改。只有确认安全无篡改的情况下,才允许安装在设备上。

简单来说,APK 的签名主要作用有两个:

  1. 证明 APK 的所有者。
  2. 允许 Android 市场和设备校验 APK 的正确性。

Android 目前支持以下四种应用签名方案:
v1 方案:基于 JAR 签名。
v2 方案:APK 签名方案 v2(在 Android 7.0 中引入)
v3 方案:APK 签名方案 v3(在 Android 9 中引入)
v4 方案:APK 签名方案 v4(在 Android 11 中引入)

签名校验-防君子不防小人

就是验证APK是否被重新签名过,这种校验是在代码层面的校验。

校验的处理通常是:

kill/killProcess-----kill/KillProcess()可以杀死当前应用活动的进程,这一操作将会把所有该进程内的资源(包括线程全部清理掉).当然,由于ActivityManager时刻监听着进程,一旦发现进程被非正常Kill,它将会试图去重启这个进程。这就是为什么,有时候当我们试图这样去结束掉应用时,发现它又自动重新启动的原因.

system.exit-----杀死了整个进程,这时候活动所占的资源也会被释放。

finish----------仅仅针对Activity,当调用finish()时,只是将活动推向后台,并没有立即释放内存,活动的资源并没有被清理

校验的方法

private boolean SignCheck() {
    String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
    String nowSignMD5 = "";
    try {
        // 得到签名的MD5
        PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),PackageManager.GET_SIGNATURES);
        Signature[] signs = packageInfo.signatures;
        String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
        nowSignMD5 = MD5Utils.MD5(signBase64);
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return trueSignMD5.equals(nowSignMD5);
}

这种校验的方式是在代码层面,对于有心者来说破解毫无难度。

可以适当的增加校验的难度:

package com.ctuav.common.utils

import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Process
import android.util.Log
import java.security.MessageDigest

/**
 * author  : ls
 * time    : 2025/6/19 08:51
 * desc    : 防君子不防小人
 */
object SecurityUtils {
    // 用于混淆的密钥
    private  val SIGNATURE_KEY = "d8q1_Kp9#mN3vX7"
    // 这里请替换为你实际的签名SHA1(大写、无冒号)
    private const val CORRECT_SIGNATURE = "-----------"

    /**
     * 多重签名校验
     */
    @JvmStatic
    fun verifyAppSignature(context: Context): Boolean {
        try {
            // 1. 基础签名校验
            val primary = checkPrimarySignature(context)
            if (!primary) {
                System.exit(0)
                return false
            }
            // 2. 二次加密校验
            val secondary = checkSecondarySignature(context)
            if (!secondary) {
                System.exit(0)
                return false
            }
            // 3. 反调试措施
            if (!antiDebugCheck(context)) {
                System.exit(0)
                return false
            }
            return true
        } catch (e: Exception) {
            System.exit(0)
            return false
        }
    }

    // 基础签名校验(SHA1)
    private fun checkPrimarySignature(context: Context): Boolean {
        return try {
            val packageInfo = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
                context.packageManager.getPackageInfo(
                    context.packageName,
                    PackageManager.GET_SIGNING_CERTIFICATES
                )
            } else {
                context.packageManager.getPackageInfo(
                    context.packageName,
                    PackageManager.GET_SIGNATURES
                )
            }
            val signatures = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
                packageInfo.signingInfo.apkContentsSigners
            } else {
                packageInfo.signatures
            }
            if (signatures.isEmpty()) return false
            val cert = signatures[0].toByteArray()
            val md = MessageDigest.getInstance("SHA1")
            // 生成无冒号、全大写的 SHA1 字符串
            val sha1 = md.digest(cert).joinToString("") { "%02X".format(it) }
            Log.d("---------", "sha1: $sha1 md: $md")
            // 混淆校验
            obfuscateCheck(sha1)
        } catch (e: Exception) {
            false
        }
    }

    // 二次加密校验
    private fun checkSecondarySignature(context: Context): Boolean {
        return try {
            val pid = Process.myPid()
            val uid = Process.myUid()
            val combined = "$pid:$uid:${context.packageName}"
            val encrypted = encryptData(combined)
            validateEncryption(encrypted)
        } catch (e: Exception) {
            false
        }
    }

    // 反调试措施(安装时间校验+随机延时)
    private fun antiDebugCheck(context: Context): Boolean {
        val now = System.currentTimeMillis()
        val installTime = context.packageManager.getPackageInfo(context.packageName, 0).firstInstallTime
        if (now - installTime < 0) return false
        try {
            Thread.sleep((1..5).random().toLong())
        } catch (_: Exception) {}
        return true
    }

    // 签名混淆校验
    private fun obfuscateCheck(signature: String): Boolean {
        fun obfuscate(str: String): Int {
            return str.toByteArray().map { it.toInt() xor SIGNATURE_KEY.hashCode() }.sum()
        }
        return obfuscate(signature) == obfuscate(CORRECT_SIGNATURE)
    }

    // 简单加密算法
    private fun encryptData(data: String): String {
        return data.toByteArray().map { (it.toInt() xor SIGNATURE_KEY.hashCode()) + 1 }.joinToString("")
    }

    // 加密校验
    private fun validateEncryption(encrypted: String): Boolean {
        return try {
            val checkSum = encrypted.toCharArray().map { it.code }.reduce { acc, i -> acc xor i }
            checkSum != 0
        } catch (e: Exception) {
            false
        }
    }
}

这里对签名进行了加密二次校验和混淆校验,此处的签名还是保留在客户端的,最好的做法是校验的工作放在服务端处理。

签名校验是如何被破解的?

反编译、二次打包
+ 通过反编译工具(如 jadx、apktool)获取应用源码 + 定位签名校验的代码位置 + 修改校验逻辑或替换正确的签名值 + 重新打包签名
Hook
+ 使用 Xposed、Frida 等 Hook 框架 + Hook 签名校验相关方法 + 直接返回 true 或修改返回值 + 无需重新打包,运行时动态修改
修改smali
定位、找到地方,直接替换或者删除判断逻辑,这也是去除广告、VIP的奇技淫巧。

该怎么办

大多数的基础措施都无法拦住有心者,只是增加难度和成本。
增加class.dex的校验
重新打包通常都会修改源文件,需要重新打包编译,所以生成的dex的 Hash值是有变化的,可以对其增加校验,这个工作和代理检测、签名校验一样是加在业务端的。
    public static long getApkCRC(Context context) {
        ZipFile zf;
        try {
            zf = new ZipFile(context.getPackageCodePath());
            // 获取apk安装后的路径
            ZipEntry ze = zf.getEntry("classes.dex");
            return ze.getCrc();
        }catch (Exception e){
            return 0;
        }
    }

判断逻辑

    String srcStr = MD5Util.getMD5(String.valueOf(CommentUtils.getApkCRC(getApplicationContext())));
    if(!srcStr.equals(getString(R.string.classes_txt))){
        // 可能被重编译了,需要退出
        android.os.Process.killProcess(android.os.Process.myPid());
    }

比较脆弱 可以进行二次较密增加破解的难度,依然是挡不住有心者。

加上Native 层签名校验
这可能是最靠谱的措施了,C++和SO的安全度较高,逆向的难度大,同样的平时处理一些加密的操作的时候写在cpp里也是最好的。
  1. Java 层通过 JNI 调用 native 方法
  2. Native 层获取包名、签名信息
  3. Native 层对签名做校验(如 SHA1、MD5、Base64 等)
  4. 校验结果返回 Java 层,决定是否继续运行
#include <jni.h>
#include <string>
#include <android/log.h>
#include <time.h>
#include <string.h>

#define LOG_TAG "NativeCheck"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

// 签名SHA1(无冒号、全大写)
const char* CORRECT_SHA1 = "-----";
// 动态密钥生成用的盐值
const char* SALT = "d8q1_Kp9#mN3vX7";

// 生成动态密钥
std::string generateDynamicKey() {
    time_t now = time(nullptr);
    std::string key;
    for(int i = 0; i < strlen(SALT); i++) {
        key += (SALT[i] ^ ((now >> (i % 8)) & 0xFF));
    }
    return key;
}

// 二次加密
std::string encryptSignature(const std::string& signature, const std::string& key) {
    std::string encrypted;
    for(size_t i = 0; i < signature.length(); i++) {
        encrypted += signature[i] ^ key[i % key.length()];
    }
    return encrypted;
}

// 安全比较
bool secureCompare(const std::string& a, const std::string& b) {
    if(a.length() != b.length()) return false;
    int result = 0;
    for(size_t i = 0; i < a.length(); i++) {
        result |= a[i] ^ b[i];
    }
    return result == 0;
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_ctuav_common_utils_SecurityUtils_verifySignatureNative(JNIEnv *env, jobject thiz, jstring sha1_) {
    // 获取传入的SHA1
    const char* actualSha1 = env->GetStringUTFChars(sha1_, 0);
    
    // 生成动态密钥
    std::string dynamicKey = generateDynamicKey();
    
    // 对实际SHA1和正确SHA1都进行二次加密
    std::string encryptedActual = encryptSignature(actualSha1, dynamicKey);
    std::string encryptedCorrect = encryptSignature(CORRECT_SHA1, dynamicKey);
    
    // 安全比较
    bool result = secureCompare(encryptedActual, encryptedCorrect);
    
    // 释放资源
    env->ReleaseStringUTFChars(sha1_, actualSha1);
    
    // 混淆返回结果
    return (result ^ 1) ^ 1 ? JNI_TRUE : JNI_FALSE;
} 

代码层面的校验和native层的校验交叉,破解的难度又上去了。


网站公告

今日签到

点亮在社区的每一天
去签到