移动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 
#include 
 
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项目已经采取了加固措施,但仍然无法防止应用被二次打包。

扫描二维码获取更多内容

你可能感兴趣的:(移动app安全测试 - 客户端篇(三)签名校验)