Android | 签名安全
检验和签名
校验开发者在数据传送时采用的一种校正数据的一种方式, 常见的校验有:签名校验(最常见)、dexcrc校验、apk完整性校验、路径文件校验等。通过对 Apk 进行签名,开发者可以证明对 Apk 的所有权和控制权,可用于安装和更新其应用。而在 Android 设备上的安装 Apk ,如果是一个没有被签名的 Apk,则会被拒绝安装。在安装 Apk 的时候,软件包管理器也会验证 Apk 是否已经被正确签名,并且通过签名证书和数据摘要验证是否合法没有被篡改。只有确认安全无篡改的情况下,才允许安装在设备上。
简单来说,APK 的签名主要作用有两个:
- 证明 APK 的所有者。
- 允许 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 {// 得到签名的MD5PackageInfo 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.utilsimport 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 = "-----------"/*** 多重签名校验*/@JvmStaticfun 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 falseval 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).firstInstallTimeif (now - installTime < 0) return falsetry {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里也是最好的。- Java 层通过 JNI 调用 native 方法
- Native 层获取包名、签名信息
- Native 层对签名做校验(如 SHA1、MD5、Base64 等)
- 校验结果返回 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_) {// 获取传入的SHA1const 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层的校验交叉,破解的难度又上去了。