最近业余时间写了一款应用《摇啊摇》,安智、安卓、360等几个应用商店已经陆续审核通过并上线。从有想法到最终将产品做出来并发布,断断续续花了近二个半月的业余时间,总体来讲还算顺利,虽然期间也遇到几个小技术难点,最后解决的还算满意。今天说下其中一个小技术难点,现在想想这个小技术难点也很平常,但还是分享出来,希望对有相同疑惑的同学有帮助。
因为java语言自身特性的原因,导致android程序很容易被反编译,虽然可以采用代码混淆的方式,但是如果用了第三方库,混淆脚步编写不好,代码混淆后又会出现程序运行不稳定问题。而没有混淆的程序一旦被反编译后,源码中大量的敏感信息将会暴露无遗。比如与服务器交互的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将是一致的。