近期业余时间写了一款应用《摇啊摇》,安智、安卓、360等几个应用商店已经陆续审核通过并上线。从有想法到终于将产品做出来并公布,断断续续花了近二个半月的业余时间,整体来讲还算顺利,尽管期间也遇到几个小技术难点。最后解决的还算惬意。今天说下当中一个小技术难点,如今想想这个小技术难点也非常寻常,但还是分享出来,希望对有同样疑惑的同学有帮助。
因为java语言自身特性的原因,导致android程序非常easy被反编译。尽管能够採用代码混淆的方式,可是假设用了第三方库。混淆脚步编写不好,代码混淆后又会出现程序执行不稳定问题。
而没有混淆的程序一旦被反编译后。源代码中大量的敏感信息将会暴露无遗。比方与server交互的url地址信息,假设使用了动态链接库,那么native方法也将暴露。况且混淆时是不混淆native方法的。别人看到native方法,就能够自己载入so文件,那么非常多核心的东西,别人就能够间接的使用了,尽管他不一定能用好,但至少能够调用了。
针对上面的林林总总程序被反编译后可能出现的问题,我的解决方法是使用jni技术。在ndk环境下做包签名信息核查,因为是ndk环境,所以这个非常难反编译,也非常难绕过该核查。理论上,签名文件keystore是唯一的,而且仅仅有程序作者才拥有。详细做法为,在所有的native方法内。添加签名信息核查推断,仅仅有签名信息核查通过。程序才干做进一步操作,否则直接返回NULL,这样,即使别人拿到了so文件,摸清楚了native方法參数及使用方法,但因为签名信息不一致。native方法所有返回NULL,so文件瞬间变成砖头。
相同。对于url地址等敏感信息,添加签名信息核查,仅仅有核查通过。程序才会返回正确的字符串,否则直接返回NULL。这样能够非常好的隐藏和保护敏感信息。
说了半天,关键的一步是怎样在ndk环境下,获取包签名信息,以下的代码为相关实现。
jstring loadSignature(JNIEnv* env, jobject obj) { // 获得Context类 jclass cls = (*env)->GetObjectClass(env, obj); // 得到getPackageManager方法的ID jmethodID mid = (*env)->GetMethodID(env, cls, "getPackageManager", "()Landroid/content/pm/PackageManager;"); // 获得应用包的管理器 jobject pm = (*env)->CallObjectMethod(env, obj, mid); // 得到getPackageName方法的ID mid = (*env)->GetMethodID(env, cls, "getPackageName", "()Ljava/lang/String;"); // 获得当前应用包名 jstring packageName = (jstring)(*env)->CallObjectMethod(env, obj, mid); // 获得PackageManager类 cls = (*env)->GetObjectClass(env, pm); // 得到getPackageInfo方法的ID mid = (*env)->GetMethodID(env, cls, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;"); // 获得应用包的信息 jobject packageInfo = (*env)->CallObjectMethod(env, pm, mid, packageName, 0x40); //GET_SIGNATURES = 64; // 获得PackageInfo 类 cls = (*env)->GetObjectClass(env, packageInfo); // 获得签名数组属性的ID jfieldID fid = (*env)->GetFieldID(env, cls, "signatures", "[Landroid/content/pm/Signature;"); // 得到签名数组 jobjectArray signatures = (jobjectArray)(*env)->GetObjectField(env, packageInfo, fid); // 得到签名 jobject sign = (*env)->GetObjectArrayElement(env, signatures, 0); // 获得Signature类 cls = (*env)->GetObjectClass(env, sign); // 得到toCharsString方法的ID mid = (*env)->GetMethodID(env, cls, "toCharsString", "()Ljava/lang/String;"); // 返回当前应用签名信息 return (jstring)(*env)->CallObjectMethod(env, sign, mid); }
上述代码获得的包签名信息实际是一个非常长的字符串。为了更高效的进行签名信息比对,还能够将其进行md5加密,加密成32位字符串形式。另外,我在查阅资料过程中,看到有的资料提到用包签名的hashcode值做比对。这样的方式更简单一点。但我没有採用这样的方式,总认为这样的方式可能不精确,不过个人认为,有兴趣的同学能够查阅很多其它相关资料。这里还要说明一点,上面代码获得的包签名字符串信息,使用md5加密后,得到的加密结果与包签名实际的md5
fingerprint是不一致的。主要是由于将签名信息使用toCharsString()转换成字符串后在进行md5加密所致。假设使用toByteArray()将其转成数组。然后加密,加密结果与包签名实际md5 fingerprint将是一致的。