移動app安全測試 - 客戶端篇(三)簽名校驗

前言:

二次打包作為移動app安全風險的一部分,通常由逆向破解者進行破解,然后插入廣告、植入惡意代碼、修改內購邏輯逃避支付等等。這些惡意行為嚴重危害移動產品和用戶利益,同時也影響企業口碑。

簽名校驗:

防止二次打包最普遍的方式之一,便是進行簽名校驗。校驗又分為很多層次,有針對package信息,有的針對文件hash,有的甚至針對代碼段等等。這里只列舉最簡單的幾種方式供參考。

1、普通校驗

系統將應用的簽名信息封裝在 PackageInfo 中,調用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可獲取指定包名的簽名信息。

  /**
    * 做普通的簽名校驗 - Java層
  */
private byte[] getCertificateSHA1Fingerprint(Context context) {
    PackageManager pm = context.getPackageManager();
    String packageName = context.getPackageName();
 
    try {
        PackageInfo packageInfo = pm.getPackageInfo(packageName, 
            PackageManager.GET_SIGNATURES);
        Signature[] signatures = packageInfo.signatures;
        byte[] cert = signatures[0].toByteArray();
        X509Certificate x509 = X509Certificate.getInstance(cert);
        MessageDigest md = MessageDigest.getInstance("SHA1");
        return md.digest(x509.getEncoded());
    } catch (PackageManager.NameNotFoundException | CertificateException |
            NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return null;
}

當然如果java層不夠安全的話,可以放在native層去做。 native 代碼調用 Java 函數的套路:先找到 jclassjmethodID,再 CallXXXMethod。

#include <jni.h>
#include <stddef.h>
 
extern "C" {
 
JNIEXPORT jbyteArray JNICALL
Java_com_github_piasy_MainActivity_nativeGetSig(
        JNIEnv *env, jclass type, jobject context) {
    // context.getPackageManager()
    jclass context_clazz = env->GetObjectClass(context);
    jmethodID getPackageManager = env->GetMethodID(context_clazz, 
        "getPackageManager", "()Landroid/content/pm/PackageManager;");
    jobject packageManager = env->CallObjectMethod(context, 
        getPackageManager);
 
    // context.getPackageName()
    jmethodID getPackageName = env->GetMethodID(context_clazz, 
        "getPackageName", "()Ljava/lang/String;");
    jstring packageName = (jstring) env->CallObjectMethod(context, 
        getPackageName);
 
    // packageManager->getPackageInfo(packageName, GET_SIGNATURES);
    jclass package_manager_clazz = env->GetObjectClass(packageManager);
    jmethodID getPackageInfo = env->GetMethodID(package_manager_clazz, 
        "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    jint flags = 0x00000040;
    jobject packageInfo = env->CallObjectMethod(packageManager, 
        getPackageInfo, packageName, flags);
 
    jthrowable exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // packageInfo.signatures[0]
    jclass package_info_clazz = env->GetObjectClass(packageInfo);
    jfieldID fid = env->GetFieldID(package_info_clazz, "signatures",
        "[Landroid/content/pm/Signature;");
    jobjectArray signatures = (jobjectArray) env->GetObjectField(
        packageInfo, fid);
    jobject signature = env->GetObjectArrayElement(signatures, 0);
 
    // signature.toByteArray()
    jclass signature_clazz = env->GetObjectClass(signature);
    jmethodID signature_toByteArray = env->GetMethodID(signature_clazz, 
        "toByteArray", "()[B");
    jbyteArray sig_bytes = (jbyteArray) env->CallObjectMethod(
        signature, signature_toByteArray);
 
    // X509Certificate appCertificate = X509Certificate.getInstance(sig_bytes);
    jclass x509_clazz = env->FindClass("javax/security/cert/X509Certificate");
    jmethodID x509_getInstance = env->GetStaticMethodID(x509_clazz, 
        "getInstance", "([B)Ljavax/security/cert/X509Certificate;");
    jobject x509 = (jstring) env->CallStaticObjectMethod(x509_clazz, 
        x509_getInstance, sig_bytes);
 
    exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // x509.getEncoded()
    jmethodID getEncoded = env->GetMethodID(x509_clazz, 
        "getEncoded", "()[B");
    jbyteArray public_key = (jbyteArray) env->CallObjectMethod(x509, getEncoded);
 
    exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // MessageDigest.getInstance("SHA1")
    jclass message_digest_clazz = env->FindClass("java/security/MessageDigest");
    jmethodID message_digest_getInstance = env->GetStaticMethodID(
        message_digest_clazz, "getInstance", 
        "(Ljava/lang/String;)Ljava/security/MessageDigest;");
    jstring sha1_name = env->NewStringUTF("SHA1");
    jobject sha1 = env->CallStaticObjectMethod(message_digest_clazz, 
        message_digest_getInstance, sha1_name);
 
    exception = env->ExceptionOccurred();
    env->ExceptionClear();
    if (exception) {
        return NULL;
    }
 
    // sha1.digest(public_key)
    jmethodID digest = env->GetMethodID(message_digest_clazz, 
        "digest", "([B)[B");
    jbyteArray sha1_bytes = (jbyteArray) env->CallObjectMethod(
        sha1, digest, public_key);
 
    return sha1_bytes;
}
 
}

相應破解方式,動態代理IPackageManager。

2、動態代理檢測

動態代理的原理是系統動態的為我們創建了一個代理類,所以檢測 IPackageManager 的類名即可發現端倪

 /**
    * 檢測 PM 代理
    */
@SuppressLint("PrivateApi")
private boolean checkPMProxy(){
    String truePMName = "android.content.pm.IPackageManager$Stub$Proxy";
    String nowPMName = "";
    try {
        // 被代理的對象是 PackageManager.mPM
        PackageManager packageManager = getPackageManager();
        Field mPMField = packageManager.getClass().getDeclaredField("mPM");
        mPMField.setAccessible(true);
        Object mPM = mPMField.get(packageManager);
        // 取得類名
        nowPMName = mPM.getClass().getName();
    } catch (Exception e) {
        e.printStackTrace();
    }
    // 類名改變說明被代理了
    return truePMName.equals(nowPMName);
}

3、使用新的API

api28以上,可以使用新的api進行檢測。

 /**
    * 使用較新的 API 檢測
    */
@SuppressLint("PackageManagerGetSignatures")
private boolean useNewAPICheck(){
    String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
    String nowSignMD5 = "";
    Signature[] signs = null;
    try {
        // 得到簽名 MD5
        if (VERSION.SDK_INT >= 28) {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(
                    getPackageName(),
                    PackageManager.GET_SIGNING_CERTIFICATES);
            signs = packageInfo.signingInfo.getApkContentsSigners();
        } else {
            PackageInfo packageInfo = getPackageManager().getPackageInfo(
                    getPackageName(),
                    PackageManager.GET_SIGNATURES);
            signs = packageInfo.signatures;
        }
        String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
        nowSignMD5 = MD5Utils.MD5(signBase64);
    } catch (PackageManager.NameNotFoundException e) {
        e.printStackTrace();
    }
    return trueSignMD5.equals(nowSignMD5);
}

總結:

簽名校驗還有一些其他的騷操作,比如提早檢測、校驗application等。雖然使用校驗不能防止應用被破解、二次打包,但是可以極大的提高破解者的破解成本。雖然目前Android項目已經采取了加固措施,但仍然無法防止應用被二次打包。

掃描二維碼獲取更多內容
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。