您的位置:首页 > 移动开发 > Android开发

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

2018-02-11 18:06 453 查看

    一、背景

    一直以来,签名校验都是防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无法正常运行。当然,还有这里的代码只是测试代码,线上代码应该做一些去掉一些敏感的字符串,以及通过算法来生成签名信息,反调试等安全措施。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: