防止重打包:检测 Application ID 与签名证书的一致性

防止重打包:检测 Application ID 与签名证书的一致性

各位听众,大家好。今天我们来探讨一个重要的安全议题:如何防止Android应用被重打包,并重点关注检测Application ID与签名证书的一致性。重打包是指攻击者篡改原始APK文件,例如插入恶意代码、修改界面、植入广告等,然后重新签名并发布。这不仅损害了开发者的利益,也威胁了用户的安全。

重打包的危害

重打包带来的危害是多方面的:

  • 恶意软件传播: 攻击者可以在应用中植入病毒、木马等恶意代码,窃取用户隐私、盗取账号密码,甚至控制用户设备。
  • 广告欺诈: 通过修改应用,植入恶意广告,强制展示广告,或劫持正常广告流量,从而进行广告欺诈。
  • 数据窃取: 篡改应用,收集用户敏感信息,例如地理位置、联系人、短信内容等,用于非法目的。
  • 信誉损害: 被重打包的应用如果包含恶意行为,会损害原始应用的声誉,导致用户流失。
  • 经济损失: 开发者因应用被篡改而遭受经济损失,例如广告收入减少、用户付费意愿降低。

检测Application ID与签名证书一致性的必要性

检测Application ID与签名证书的一致性是防止重打包的重要手段之一。原理如下:

  • Application ID (Package Name): 每个Android应用都有一个唯一的Application ID,也称为Package Name,在AndroidManifest.xml文件中定义。它是应用的身份标识,用于在应用商店中区分不同的应用。
  • 签名证书: Android应用必须使用数字证书进行签名才能安装到设备上。签名证书包含了开发者的身份信息,以及用于验证应用完整性的公钥。

当应用被重打包时,攻击者通常会修改Application ID,以便将其伪装成不同的应用,或者与目标应用共存。同时,他们会使用自己的签名证书对应用进行签名。因此,通过检测Application ID与签名证书的对应关系,我们可以判断应用是否被篡改。如果Application ID与签名证书不匹配,则说明应用很可能已被重打包。

检测方法

以下介绍几种检测Application ID与签名证书一致性的方法:

1. 运行时检测 (Runtime Detection)

在应用运行时,通过代码动态获取Application ID和签名证书信息,并进行比对。这是最常用的方法,可以有效防止静态分析绕过。

代码示例 (Java):

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

public class SignatureChecker {

    private static final String EXPECTED_PACKAGE_NAME = "com.example.originalapp"; // 原始应用的Application ID
    private static final String EXPECTED_SIGNATURE_HASH = "YOUR_EXPECTED_SIGNATURE_HASH"; // 原始应用的签名证书哈希值

    public static boolean isAppTampered(Context context) {
        try {
            PackageManager packageManager = context.getPackageManager();
            PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);

            // 检查Application ID
            if (!packageInfo.packageName.equals(EXPECTED_PACKAGE_NAME)) {
                return true; // Application ID不匹配,说明已被篡改
            }

            // 检查签名证书
            Signature[] signatures = packageInfo.signatures;
            if (signatures == null || signatures.length == 0) {
                return true; // 没有签名,说明已被篡改
            }

            String signatureHash = getSignatureHash(signatures[0]);
            if (!signatureHash.equals(EXPECTED_SIGNATURE_HASH)) {
                return true; // 签名证书不匹配,说明已被篡改
            }

            return false; // Application ID和签名证书都匹配,说明应用未被篡改

        } catch (PackageManager.NameNotFoundException e) {
            // 应用未找到,通常不会发生
            return true;
        } catch (NoSuchAlgorithmException e) {
            // 不支持的哈希算法
            return true;
        }
    }

    // 计算签名证书的哈希值
    private static String getSignatureHash(Signature signature) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("SHA-256"); // 可以选择其他哈希算法,例如SHA-1
        md.update(signature.toByteArray());
        byte[] digest = md.digest();
        return bytesToHex(digest);
    }

    // 将字节数组转换为十六进制字符串
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexStringBuilder = new StringBuilder();
        for (byte b : bytes) {
            hexStringBuilder.append(String.format("%02x", b));
        }
        return hexStringBuilder.toString();
    }

    // 在应用的Activity或Service中调用
    public static void checkSignature(Context context) {
        if (isAppTampered(context)) {
            // 应用已被篡改,执行相应的处理,例如退出应用、显示警告信息等
            // 可以使用Timber、Logcat等工具记录日志
            // 可以将检测结果上报到服务器
            android.util.Log.e("SignatureChecker", "Application has been tampered!");
            System.exit(0); // 退出应用
        } else {
            android.util.Log.i("SignatureChecker", "Application is valid.");
        }
    }
}

代码解释:

  • EXPECTED_PACKAGE_NAME: 存储原始应用的Application ID。
  • EXPECTED_SIGNATURE_HASH: 存储原始应用的签名证书哈希值。需要提前获取。
  • isAppTampered(Context context): 核心方法,用于检测应用是否被篡改。
  • getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES): 获取应用的PackageInfo,包括签名信息。
  • getSignatureHash(Signature signature): 计算签名证书的哈希值。
  • bytesToHex(byte[] bytes): 将字节数组转换为十六进制字符串。
  • checkSignature(Context context): 在应用的Activity或Service中调用,执行检测逻辑。

如何获取签名证书的哈希值:

可以使用keytool命令从APK文件中提取签名证书,并计算其哈希值。

keytool -printcert -jarfile your_app.apk | openssl dgst -sha256 -binary | openssl base64

your_app.apk替换为你的APK文件路径。该命令会输出签名证书的SHA-256哈希值的Base64编码。 或者使用如下命令直接获取十六进制的哈希值:

keytool -printcert -jarfile your_app.apk | openssl dgst -sha256 | sed 's/^.*= //g'

注意事项:

  • EXPECTED_PACKAGE_NAMEEXPECTED_SIGNATURE_HASH替换为你的实际值。
  • 建议使用SHA-256或更强的哈希算法。
  • 将签名证书哈希值硬编码在代码中存在一定的风险,攻击者可以通过反编译代码获取。可以使用代码混淆、加密等技术来增加安全性。
  • 可以在多个Activity或Service中进行检测,增加检测的覆盖范围。

2. 使用PackageManager API 获取签名信息

Android的PackageManager API提供了更便捷的方式来获取签名信息。

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class SignatureValidator {

    private static final String EXPECTED_PACKAGE_NAME = "com.example.originalapp";
    private static final List<String> EXPECTED_SIGNATURES = Collections.singletonList("YOUR_BASE64_ENCODED_SIGNATURE"); // 原始应用的签名证书的Base64编码

    public static boolean isAppSignatureValid(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            PackageInfo packageInfo = pm.getPackageInfo(EXPECTED_PACKAGE_NAME, PackageManager.GET_SIGNATURES);

            if (!packageInfo.packageName.equals(context.getPackageName())) {
                // Package name doesn't match. This likely indicates a repackaged app.
                return false;
            }

            Signature[] signatures = packageInfo.signatures;
            if (signatures == null || signatures.length == 0) {
                // No signatures found. This is suspicious.
                return false;
            }

            // Convert signatures to Base64 encoded string for easy comparison.
            List<String> signatureList = Arrays.asList(getSignatures(signatures));

            // Check if at least one of the signatures matches the expected signature.
            for (String signature : signatureList) {
                if (EXPECTED_SIGNATURES.contains(signature)) {
                    return true;  // Signature matches, app is likely valid.
                }
            }

            // None of the signatures matched the expected signature.
            return false;

        } catch (PackageManager.NameNotFoundException e) {
            // Package not found. App might not be installed.
            return false;
        } catch (NoSuchAlgorithmException e) {
            // Algorithm not found. Should not happen.
            return false;
        }
    }

    private static String[] getSignatures(Signature[] signatures) throws NoSuchAlgorithmException {
        String[] signatureList = new String[signatures.length];

        for (int i = 0; i < signatures.length; i++) {
            Signature signature = signatures[i];
            byte[] signatureBytes = signature.toByteArray();

            MessageDigest md = MessageDigest.getInstance("SHA");
            md.update(signatureBytes);
            byte[] digest = md.digest();

            // Convert the byte array to a Base64 encoded string.
            signatureList[i] = Base64.encodeToString(digest, Base64.DEFAULT);
        }

        return signatureList;
    }

    public static void checkSignature(Context context) {
        if (!isAppSignatureValid(context)) {
            // Application has been tampered with!
            android.util.Log.e("SignatureValidator", "Application signature is invalid!");
            // Take appropriate action, such as exiting the app or displaying a warning.
            System.exit(0);
        } else {
            android.util.Log.i("SignatureValidator", "Application signature is valid.");
        }
    }
}

代码解释:

  • EXPECTED_PACKAGE_NAME: 存储原始应用的Application ID。
  • EXPECTED_SIGNATURES: 存储原始应用的签名证书的Base64编码。 支持多个签名,方便应用升级签名时兼容。
  • isAppSignatureValid(Context context): 核心方法,用于检测应用签名是否有效。
  • getSignatures(Signature[] signatures): 获取签名证书的Base64编码的列表。

如何获取签名证书的Base64编码:

可以使用keytool命令从APK文件中提取签名证书,并进行Base64编码。

keytool -exportcert -alias <your_alias> -keystore <your_keystore_path> | openssl sha1 -binary | openssl base64

替换 <your_alias> 为你的密钥别名, <your_keystore_path>为你的keystore文件路径。

3. NDK Native 代码检测

将检测逻辑放在Native层,可以提高安全性,防止反编译破解。

代码示例 (C++):

#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/asset_manager_jni.h>
#include <android/asset_manager.h>
#include <fstream>
#include <sstream>
#include <iomanip>

#define LOG_TAG "SignatureChecker"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

// Expected values (replace with your actual values)
static const char* EXPECTED_PACKAGE_NAME = "com.example.originalapp";
static const char* EXPECTED_SIGNATURE_HASH = "YOUR_EXPECTED_SIGNATURE_HASH";

std::string bytesToHex(const unsigned char* hash, size_t length) {
    std::stringstream ss;
    ss << std::hex << std::setfill('0');
    for (size_t i = 0; i < length; i++) {
        ss << std::setw(2) << (int)hash[i];
    }
    return ss.str();
}

extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_originalapp_SignatureChecker_isAppTamperedNative(JNIEnv *env, jobject /* this */, jobject context) {
    jclass contextClass = env->GetObjectClass(context);
    jmethodID getPackageManagerMethod = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject packageManager = env->CallObjectMethod(context, getPackageManagerMethod);

    jclass packageManagerClass = env->GetObjectClass(packageManager);
    jmethodID getPackageNameMethod = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
    jstring packageNameObj = (jstring) env->CallObjectMethod(context, getPackageNameMethod);
    const char* packageName = env->GetStringUTFChars(packageNameObj, nullptr);

    // Check Package Name
    if (strcmp(packageName, EXPECTED_PACKAGE_NAME) != 0) {
        LOGE("Package name mismatch! Expected: %s, Actual: %s", EXPECTED_PACKAGE_NAME, packageName);
        env->ReleaseStringUTFChars(packageNameObj, packageName);
        return JNI_TRUE;
    }
    env->ReleaseStringUTFChars(packageNameObj, packageName);

    // Get PackageInfo
    jmethodID getPackageInfoMethod = env->GetMethodID(packageManagerClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jstring packageNameStr = env->NewStringUTF(EXPECTED_PACKAGE_NAME);
    jobject packageInfo = env->CallObjectMethod(packageManager, getPackageInfoMethod, packageNameStr, 64); //PackageManager.GET_SIGNATURES = 64

    if (packageInfo == nullptr) {
        LOGE("Failed to get PackageInfo.");
        env->DeleteLocalRef(packageNameStr);
        return JNI_TRUE;
    }
    env->DeleteLocalRef(packageNameStr);

    // Get Signatures
    jclass packageInfoClass = env->GetObjectClass(packageInfo);
    jfieldID signaturesField = env->GetFieldID(packageInfoClass, "signatures", "[Landroid/content/pm/Signature;");
    jobjectArray signaturesArray = (jobjectArray) env->GetObjectField(packageInfo, signaturesField);

    if (signaturesArray == nullptr) {
        LOGE("No signatures found.");
        return JNI_TRUE;
    }

    jsize signatureCount = env->GetArrayLength(signaturesArray);
    if (signatureCount == 0) {
        LOGE("Signature array is empty.");
        return JNI_TRUE;
    }

    jobject signatureObj = env->GetObjectArrayElement(signaturesArray, 0); // Get the first signature
    jclass signatureClass = env->GetObjectClass(signatureObj);
    jmethodID toByteArrayMethod = env->GetMethodID(signatureClass, "toByteArray", "()[B");
    jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signatureObj, toByteArrayMethod);

    // Calculate SHA-256 Hash
    jsize signatureByteLength = env->GetArrayLength(signatureByteArray);
    unsigned char* signatureBytes = new unsigned char[signatureByteLength];
    env->GetByteArrayRegion(signatureByteArray, 0, signatureByteLength, reinterpret_cast<jbyte*>(signatureBytes));

    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256(signatureBytes, signatureByteLength, hash);

    std::string signatureHash = bytesToHex(hash, SHA256_DIGEST_LENGTH);
    delete[] signatureBytes;

    // Check Signature Hash
    if (signatureHash != EXPECTED_SIGNATURE_HASH) {
        LOGE("Signature hash mismatch! Expected: %s, Actual: %s", EXPECTED_SIGNATURE_HASH, signatureHash.c_str());
        return JNI_TRUE;
    }

    LOGI("Signature check passed!");
    return JNI_FALSE;
}

对应的Java代码:

package com.example.originalapp;

import android.content.Context;

public class SignatureChecker {

    static {
        System.loadLibrary("native-lib"); // 加载Native库
    }

    public native boolean isAppTamperedNative(Context context);

    public static void checkSignature(Context context) {
        SignatureChecker checker = new SignatureChecker();
        if (checker.isAppTamperedNative(context)) {
            android.util.Log.e("SignatureChecker", "Application has been tampered!");
            System.exit(0);
        } else {
            android.util.Log.i("SignatureChecker", "Application is valid.");
        }
    }
}

代码解释:

  • EXPECTED_PACKAGE_NAME: 存储原始应用的Application ID。
  • EXPECTED_SIGNATURE_HASH: 存储原始应用的签名证书哈希值。
  • isAppTamperedNative(Context context): Native方法,用于检测应用是否被篡改。
  • 使用JNI调用Android API获取PackageInfo和签名信息。
  • 计算签名证书的SHA-256哈希值。
  • 比对Application ID和签名证书哈希值。

CMakeLists.txt 文件配置:

cmake_minimum_required(VERSION 3.4.1)

include_directories(src/main/cpp/include) # Include directory for openssl

add_library(native-lib SHARED src/main/cpp/native-lib.cpp)

find_library(log-lib log)

# 添加OpenSSL库
find_library(
        openssl_lib
        NAMES libcrypto
        HINTS /usr/local/ssl/lib # 修改为你的OpenSSL库的路径
)

target_link_libraries(
        native-lib
        ${log-lib}
        ${openssl_lib} # 链接OpenSSL库
)

build.gradle 配置:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-frtti -fexceptions"
                arguments "-DANDROID_STL=c++_shared"
            }
        }
    }
    ...
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}

重要提示:

  1. OpenSSL依赖: 该代码依赖于OpenSSL库进行SHA-256哈希计算。 需要确保你的项目配置了OpenSSL库。 可以静态编译OpenSSL库到你的APK中,或者动态链接系统提供的OpenSSL库。 上面的CMakeLists.txt 文件配置是动态链接OpenSSL库的示例。
  2. 安全性: 将敏感信息(例如签名证书哈希值)存储在Native代码中,可以增加安全性,但仍然存在被破解的风险。 可以使用更复杂的加密算法、代码混淆等技术来进一步提高安全性。
  3. JNI: 熟悉JNI编程是必要的,需要理解Java和Native代码之间的数据类型转换、对象引用管理等。
  4. 错误处理: 在Native代码中要进行充分的错误处理,例如空指针检查、异常捕获等,避免程序崩溃。

4. 应用完整性校验服务 (Remote Attestation)

使用第三方的应用完整性校验服务,例如Google Play Integrity API 或 AppAttest (iOS)。 这些服务可以提供更强的安全保障,例如硬件级别的安全校验,防止Root设备上的攻击。

Google Play Integrity API:

Google Play Integrity API 可以帮助你检测应用是否在正版的Android设备上运行,以及应用是否被篡改。它提供多种级别的完整性校验,可以根据你的需求选择。

AppAttest (iOS):

AppAttest 是苹果提供的安全服务,可以验证应用是否在正版的iOS设备上运行,并且没有被篡改。 它使用硬件级别的密钥对应用进行签名,可以有效防止重打包攻击。

表格对比:

检测方法 优点 缺点 难度 安全性
运行时检测 (Java) 简单易用,开发成本低 容易被反编译和Hook,安全性较低
NDK Native 代码检测 安全性较高,难以被反编译和Hook 开发成本较高,需要熟悉JNI和Native编程,需要处理OpenSSL依赖。
应用完整性校验服务 安全性最高,可以提供硬件级别的安全校验,防止Root设备上的攻击 需要依赖第三方服务,可能会有额外的费用,需要网络连接。
静态分析检测 可以离线检测,不需要运行应用,可以快速检测大量应用 容易被绕过,攻击者可以通过修改APK文件来躲避检测

防御策略

除了检测Application ID与签名证书的一致性之外,还可以采取以下防御策略,形成多层次的安全防护体系:

  1. 代码混淆: 使用ProGuard等工具对代码进行混淆,增加反编译的难度。
  2. 字符串加密: 对敏感字符串(例如签名证书哈希值)进行加密,防止直接暴露在代码中。
  3. 动态加载: 将部分代码放在服务器端,动态加载到应用中,增加攻击者篡改的难度。
  4. 完整性校验: 在应用启动时,对关键文件进行完整性校验,例如校验APK文件的哈希值。
  5. Root检测: 检测设备是否Root,如果Root则限制应用的功能或退出应用。
  6. 反调试: 防止应用被调试,增加攻击者分析和修改应用的难度。
  7. 安全加固: 使用专业的应用加固服务,例如梆梆安全、爱加密等,对应用进行全方位的保护。
  8. 定期更新: 定期更新应用,修复安全漏洞,及时应对新的攻击手段。
  9. 加强密钥管理: 安全地存储和管理签名密钥,防止密钥泄露。使用硬件安全模块 (HSM) 或密钥管理服务 (KMS) 来保护密钥。
  10. 监控和响应: 监控应用在应用商店和非官方渠道的发布情况,及时发现被重打包的应用,并采取相应的措施,例如向应用商店举报、向用户发布警告等。

应用商店审核

应用商店也应该加强对应用的审核,例如:

  • 签名验证: 验证应用签名是否有效,以及是否与开发者账号关联。
  • 代码扫描: 扫描应用代码,检测是否存在恶意代码或安全漏洞。
  • 行为分析: 分析应用的行为,检测是否存在恶意行为,例如窃取用户隐私、恶意广告等。
  • 人工审核: 对可疑应用进行人工审核,确保应用的安全性。

总结来说,检测 Application ID 和签名证书的一致性是保护 Android 应用免受重打包攻击的关键步骤,结合多种安全措施和应用商店的严格审核,可以构建更强大的安全防线。

增强应用安全,应对重打包挑战

通过运行时检测、NDK代码保护和应用完整性校验服务,开发者可以有效增强应用安全性,降低被重打包的风险。

多层防御体系,筑牢应用安全防线

代码混淆、动态加载和安全加固等多种防御策略的结合,能够构建更完善的应用安全体系,有效应对重打包攻击的威胁。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注