六、Android安全机制之NDK实现防钩子签名校验

    一、背景

    一直以来,签名校验都是防Apk被反编译的重要措施之一,但是随着反编译技术的日渐发展,普通的签名校验方式已经可以被轻易的攻破了。这里对目前常用的签名校验方式及其破解法进行了梳理:

1,Java层通过PackageManager获取签名信息进行对比×(hook掉与PMS交互的IPackageManager即可完美破解)

2,Java层通过解压Apk包获取签名信息进行对比×(写在Java层容易被找到逻辑代码然后被修改或被Xpose等hook工具破解)

3,NDK层通过反射PackageManager获取签名信息进行对比×(同1)

4,NDK层通过解压Apk包获取签名信息进行对比√(不通过PMS获取,并且写在NDK层代码较难被反编译,即使反编译了也难看懂,目前较完美的一种签名校验方式,本文采用此方式进行签名校验)


    可以看到,当前签名校验方式存在两个问题:

①,容易被hook,通过钩子(hook)技术可以破解掉基本上所有的签名校验,使的签名校验这种防护措施如同虚设

②,Java层容易被反编译,即使代码混淆的情况下,也可以找到相应的逻辑代码。

    基于这两个问题,有了本文NDK实现防钩子签名校验


    二、实现

    大体流程如下:

①,获取Apk路径,Apk安装的时候会拷贝一份Apk到/data/app目录下,但是不同的版本路径不一样,有的是 /data/app/包名-1/base.apk,有的则是/data/app/包名-2.apk,所以不能直接写死路径。这里通过context的getPackageCodePath方法获取,这里不使用java层传入context的方式,而是通过反射 ActivityThread的application来取得context,不产生一个jni方法,从而加大破解难度;

②,解压Apk获取CERT.RSA,java层解压很好办,有相应的api,但NDK层解压Apk就得依赖第三方压缩库了,这里采用zip库;

③,把上一步获取CERT.RSA提取出公钥,这里通过openssl提取出公钥即可;

④,把上一步获取的公钥进行MD5后对比已知值,这里同样采用openssl的api,验证不通过的话可采用exit(0)退出应用。

//对公钥MD5后进行比对验证
void MD5_Check(char *src) {
    char buff[3] = {'\0'};
    char hex[33] = {'\0'};
    unsigned char digest[MD5_DIGEST_LENGTH];

    MD5_CTX ctx;
    MD5_Init(&ctx);
    MD5_Update(&ctx, src, strlen(src));
    MD5_Final(digest, &ctx);

    strcpy(hex, "");
    for (int i = 0; i != sizeof(digest); i++) {
        sprintf(buff, "%02x", digest[i]);
        strcat(hex, buff);
    }
    if (strcmp(hex, "c8b5cf87aea796a187828b706504ca4b") == 0) {
        LOGI("SigCheckLog:MD5->验证通过 %s", hex);
    } else
        LOGI("SigCheckLog:MD5->验证失败 %s", hex);
}

//提取签名公钥
int Openssl_Verify(const unsigned char *signature_msg, unsigned int length) {
    //DER编码转换为PKCS7结构体
    PKCS7 *p7 = d2i_PKCS7(NULL, &signature_msg, length);
    if (p7 == NULL) {
        printf("error.\n");
        return 0;
    }

    //获得签名者信息stack
    STACK_OF(PKCS7_SIGNER_INFO) *sk = PKCS7_get_signer_info(p7);
    //获得签名者个数,可以有多个签名者
    int signCount = sk_PKCS7_SIGNER_INFO_num(sk);

    for (int i = 0; i < signCount; i++) {
        //获得签名者信息
        PKCS7_SIGNER_INFO *signInfo = sk_PKCS7_SIGNER_INFO_value(sk, i);
        //获得签名者证书
        X509 *cert = PKCS7_cert_from_signer_info(p7, signInfo);
        char *pubKey = (char *) cert->cert_info->key->public_key->data;
//        LOGI("SigCheckLog:  %s\n",pubKey);
        MD5_Check(pubKey);
        X509_free(cert);
    }
    return 1;
}

//解压apk
void uncompress_apk(const char *mpath, const char *fname) {
    int i = 0;
    struct zip *apkArchive = zip_open(mpath, 0, NULL);

    struct zip_stat fstat;
    zip_stat_init(&fstat);
    struct zip_file *file = zip_fopen(apkArchive, fname, 0);
    if (!file) {
        return;
    }
    zip_stat(apkArchive, fname, 0, &fstat);
    LOGI("File %i:%s Size1: %d Size2: %d", i, fstat.name, fstat.size, fstat.comp_size);
    unsigned char *buffer = (unsigned char *) malloc(fstat.size);
    zip_fread(file, buffer, fstat.size);
    Openssl_Verify(buffer, fstat.size);
    free(buffer);
    zip_fclose(file);
    zip_close(apkArchive);
}

//获取apk路径
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    assert(env != NULL);
    jclass localClass = env->FindClass("android/app/ActivityThread");
    if (localClass != NULL) {
        jmethodID getapplication = env->GetStaticMethodID(localClass, "currentApplication",
                                                          "()Landroid/app/Application;");
        if (getapplication != NULL) {
            jobject application = env->CallStaticObjectMethod(localClass, getapplication);
            jclass context = env->GetObjectClass(application);
            jmethodID methodID_func = env->GetMethodID(context, "getPackageCodePath",
                                                       "()Ljava/lang/String;");
            jstring path = static_cast<jstring>(env->CallObjectMethod(application, methodID_func));
            const char *ch = env->GetStringUTFChars(path, 0);;
            LOGI("%s", ch);
            uncompress_apk(ch, "META-INF/CERT.RSA");//.SF
            env->ReleaseStringUTFChars(path, ch);
        }
    }
    return JNI_VERSION_1_4;
}


    三、验证

在同一签名的情况下,做修改代码,增加或删除资源操作,依然可以通过签名校验;

同一Apk,更换签名后,不能通过签名校验。



    四、结语

    此签名校验的方式最好是放在主程序逻辑代码里,因为如果把校验独立开来,破解者直接去掉loadSo的代码即可破解,所以应当把签名校验放在主程序逻辑代码里,比如请求数据的加密算法等,这样不加载此So,将直接导致Apk无法正常运行。当然,还有这里的代码只是测试代码,线上代码应该做一些去掉一些敏感的字符串,以及通过算法来生成签名信息,反调试等安全措施。

猜你喜欢

转载自blog.csdn.net/u012874222/article/details/79312477