Android Studio NDK 入门教程--JNI签名验证防止恶意调用

    xiaoxiao2021-03-25  89

    概述

    根据前面的文章来看,JNI其实只实现了关键代码加密,如果别人拿到了你的Java Native方法定义和对应的so,即可完成对你so里方法的调。因为native 方法和类都是不能混淆的,混淆了方法的函数名就变了,调用的时候就找不到方法了,因此如果反编译APK可以非常容易拿到相关文件和代码。 显然我们需要一些手段来在JNI的验证请求接口的是不是我们的程序。

    签名验证的原理

    可以用如下图来表明加了验证之后调用JNI的逻辑,用一个isValid 来表明请求的应用是不是我们自己的应用。isValid 通过init 去初始化。 

    如何判别调用者的有效性

    直接有效的方案就是使用签名进行判定,如果你的keystore没有泄漏,第三方破解概率几乎为零。当然这都是相对的,任何防护都会有破绽。大多数第三方的Android SDK也都通过签名来判断申请的key是否用在了你申请的应用上。因此在大多数SDK申请key的时候会让你填写SHA1,因为在程序运行的时候SDK会获取你签名的SHA1去向服务器验证,你申请的appkey和SHA1是否正确。

    签名验证的实现

    在代码中获取签名的SHA1

    先尝试在Java代码中获取签名的SHA1

    private String getCertSHA1(Context context) { try { //获取包管理器 PackageManager packageManager=context.getPackageManager(); //获取包名 String packageName=context.getPackageName(); //获得包信息 PackageInfo pis = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); //获得签名 Signature[] signs = pis.signatures; //签名 //获得签名数组的第一位 Signature sign=signs[0]; //获得X.509证书工厂 CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); byte[] signBytes=sign.toByteArray(); ByteArrayInputStream byteIn=new ByteArrayInputStream(signBytes); //获取X509证书 X509Certificate cert = (X509Certificate) certFactory.generateCertificate(byteIn); //获取证书发行者SHA1 MessageDigest sha1=MessageDigest.getInstance("SHA1"); byte[] certByte=cert.getEncoded(); byte[] bs=sha1.digest (certByte); return toHex(bs); } catch (CertificateException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return null; } //将Byte转换成HexString 辅助函数 char[] cs=new char[16]; { for (int i=0;i < 10;i++) { cs[i] = (char) ('0' + i); } for (int i=10;i < 16;i++) { cs[i] = (char) ('A' + i - 10); } } String toHex(byte[] bs) { char[] cs=new char[bs.length * 2]; int x; for (int i=0;i < bs.length;i++) { x = bs[i] & 0xff; cs[2 * i] = this.cs[x / 16]; cs[2 * i + 1] = this.cs[x % 16]; } return new String(cs); } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263

    这里使用的是debug.keystore  LogCat输出:    Keytool输出:    可以看到我们获取的SHA1是正确的。 

    注意:这里的signature只与签名时使用的证书有关系,这里生成的SHA1,也可以通过APK包中的CERT.RSA(解压APK之后,META-INF文件夹中)文件得到。 

    在JNI中获取签名SHA1

    我并没有在Android的C库中找到类似于PackageManager之类的东西,所以任然采用JNI的反射机制去调用相关的Java方法,其实就是将上面的Java方法翻译成C++代码。

    //Native 方法声明,显然这里需要传递一个Context过去用于获取包管理器以及包名 public static native void native_init(Context context); 12 12 //C++实现 static bool is_valid= false; const char *app_signature_sha1="40C438E3DAC29E04718E141BD816B3FC1A53E389"; const char HexCode[]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; JNIEXPORT void JNICALL Java_com_wastrel_signtest_NativeFunc_native_1init (JNIEnv *env, jclass clz, jobject context_object){ jclass context_class = env->GetObjectClass(context_object); //context.getPackageManager() jmethodID methodId = env->GetMethodID(context_class, "getPackageManager", "()Landroid/content/pm/PackageManager;"); jobject package_manager_object = env->CallObjectMethod(context_object, methodId); if (package_manager_object == NULL) { LOGE("getPackageManager() Failed!"); return; } //context.getPackageName() methodId = env->GetMethodID(context_class, "getPackageName", "()Ljava/lang/String;"); jstring package_name_string = (jstring)env->CallObjectMethod(context_object, methodId); if (package_name_string == NULL) { LOGE("getPackageName() Failed!"); return ; } env->DeleteLocalRef(context_class); //PackageManager.getPackageInfo(Sting, int) //public static final int GET_SIGNATURES= 0x00000040; jclass pack_manager_class = env->GetObjectClass(package_manager_object); methodId = env->GetMethodID(pack_manager_class, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); env->DeleteLocalRef(pack_manager_class); jobject package_info_object = env->CallObjectMethod(package_manager_object, methodId, package_name_string, 0x40); if (package_info_object == NULL) { LOGE("getPackageInfo() Failed!"); return ; } env->DeleteLocalRef(package_manager_object); //PackageInfo.signatures[0] jclass package_info_class = env->GetObjectClass(package_info_object); jfieldID fieldId = env->GetFieldID(package_info_class, "signatures", "[Landroid/content/pm/Signature;"); env->DeleteLocalRef(package_info_class); jobjectArray signature_object_array = (jobjectArray)env->GetObjectField(package_info_object, fieldId); if (signature_object_array == NULL) { LOGE("PackageInfo.signatures[] is null"); return ; } jobject signature_object = env->GetObjectArrayElement(signature_object_array, 0); env->DeleteLocalRef(package_info_object); //Signature.toByteArray() jclass signature_class = env->GetObjectClass(signature_object); methodId = env->GetMethodID(signature_class, "toByteArray", "()[B"); env->DeleteLocalRef(signature_class); jbyteArray signature_byte = (jbyteArray) env->CallObjectMethod(signature_object, methodId); //new ByteArrayInputStream jclass byte_array_input_class=env->FindClass("java/io/ByteArrayInputStream"); methodId=env->GetMethodID(byte_array_input_class,"<init>","([B)V"); jobject byte_array_input=env->NewObject(byte_array_input_class,methodId,signature_byte); //CertificateFactory.getInstance("X.509") jclass certificate_factory_class=env->FindClass("java/security/cert/CertificateFactory"); methodId=env->GetStaticMethodID(certificate_factory_class,"getInstance","(Ljava/lang/String;)Ljava/security/cert/CertificateFactory;"); jstring x_509_jstring=env->NewStringUTF("X.509"); jobject cert_factory=env->CallStaticObjectMethod(certificate_factory_class,methodId,x_509_jstring); //certFactory.generateCertificate(byteIn); methodId=env->GetMethodID(certificate_factory_class,"generateCertificate",("(Ljava/io/InputStream;)Ljava/security/cert/Certificate;")); jobject x509_cert=env->CallObjectMethod(cert_factory,methodId,byte_array_input); env->DeleteLocalRef(certificate_factory_class); //cert.getEncoded() jclass x509_cert_class=env->GetObjectClass(x509_cert); methodId=env->GetMethodID(x509_cert_class,"getEncoded","()[B"); jbyteArray cert_byte=(jbyteArray)env->CallObjectMethod(x509_cert,methodId); env->DeleteLocalRef(x509_cert_class); //MessageDigest.getInstance("SHA1") jclass message_digest_class=env->FindClass("java/security/MessageDigest"); methodId=env->GetStaticMethodID(message_digest_class,"getInstance","(Ljava/lang/String;)Ljava/security/MessageDigest;"); jstring sha1_jstring=env->NewStringUTF("SHA1"); jobject sha1_digest=env->CallStaticObjectMethod(message_digest_class,methodId,sha1_jstring); //sha1.digest (certByte) methodId=env->GetMethodID(message_digest_class,"digest","([B)[B"); jbyteArray sha1_byte=(jbyteArray)env->CallObjectMethod(sha1_digest,methodId,cert_byte); env->DeleteLocalRef(message_digest_class); //toHexString jsize array_size=env->GetArrayLength(sha1_byte); jbyte* sha1 =env->GetByteArrayElements(sha1_byte,NULL); char *hex_sha=new char[array_size*2+1]; for (int i = 0; i <array_size ; ++i) { hex_sha[2*i]=HexCode[((unsigned char)sha1[i])/16]; hex_sha[2*i+1]=HexCode[((unsigned char)sha1[i])%16]; } hex_sha[array_size*2]='\0'; LOGE(" %s ",hex_sha); //比较签名 if (strcmp(hex_sha,app_signature_sha1)==0) { LOGE("验证通过"); is_valid= true; } else{ ThrowRuntimeExcption(env,"验证失败"); } return ; } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108

    运行结果: 

    显然我们的结果达到了。在JNI中获得了签名的SHA1。验证通过之后isValid=true。别的JNIFunc调用的时候先判断一下isValid的值再操作即可。当应用签名和so里预置签名不一致时将无法获得正确结果。

    其实代码不用这么长

    从上面的Java代码或C++代码都可以看到,从CertificateFactory开始其实签名的元数据已经没有改变。因此其实我们不必大废周章的通过CertificateFactory 和MessageDigest两个类去求签名的SHA1值。其实直接比较元数据即可完成验证。那么我们如何获得签名的元数据呢?

    最简单的办法就是用发布的keystore签名APk,然后在Java代码中打印一次signature.toCharsString,然后从Logcat中拷贝出来。使用keytool工具提取。  使用命令keytool -list -rfc -keystore your.keystore。  拷贝-----BEGIN CERTIFICATE-----到-----END CERTIFICATE-----之间的内容。很显然这是一段Base64,然后使用在线解码工具即可完成解码,注意要勾选结果使用16进制显示。  去掉空格和\x等垃圾数据。

    修改签名常量为获得的字符串:

    const char *app_signature= "308201dd30820146020101300d06092a864886" "f70d010105050030373116301406035504030c0d416e64726f6964204465" "6275673110300e060355040a0c07416e64726f6964310b30090603550406" "13025553301e170d3136303732363134323834315a170d34363037313931" "34323834315a30373116301406035504030c0d416e64726f696420446562" "75673110300e060355040a0c07416e64726f6964310b3009060355040613" "02555330819f300d06092a864886f70d010101050003818d003081890281" "8100879fe1f34241cccb893c9a97fee78b89c74836086cd475145e32a2b37" "dc149a2a141c5924aa877b8571defb43bfc0fa3182f5f888a7a063186af5" "13a0afdd0a874c8d201656f6453bdb11e6fd2a3f59491c79399f3b73e611" "cc0ddba58e30bdce9a12aaf63d298fb9c87675570a339e7fcf896edde5fb" "b5236b6f6eff954e4330203010001300d06092a864886f70d01010505000" "38181002f6cd4d87d190edd21964dcbdf0c5f27225737d85a9501f1601f7" "d20dc00182504288871356383f7d4f01cc031c9a4faf395f210385aad0197" "f98031e259ca69746f47768b077e9f0f965bc008b961fdd0747a2affa147f" "707703123e1d0346e9f2f4fda391217a0fdeae4c1f842ccdfff4d346c74d" "9e92b6f2c617c8cb4958f"; 123456789101112131415 123456789101112131415

    然后修改C++方法中的//Signature.toByteArray()注释以下的内容:

    jclass signature_class = env->GetObjectClass(signature_object); methodId = env->GetMethodID(signature_class, "toCharsString", "()Ljava/lang/String;"); env->DeleteLocalRef(signature_class); jstring signature_jstirng = (jstring) env->CallObjectMethod(signature_object, methodId); const char *sign=env->GetStringUTFChars(signature_jstirng,NULL); if (strcmp(sign,app_signature)==0) { LOGE("验证通过"); is_valid= true; } else{ ThrowRuntimeExcption(env,"验证失败"); } return; 1234567891011121314 1234567891011121314

    总结

    libxxx.so认证,其实只是利用了Android签名的安全性来完成验证。本文大量从C++中调用了Java方法,包括静态方法,非晶态方法等等。

    资源地址:http://download.csdn.net/detail/venusic/9615261

    转载请标明来自于http://blog.csdn.net/venusic

    转载请注明原文地址: https://ju.6miu.com/read-16949.html

    最新回复(0)