起因
说出来你可能不信,之所以研究JNI是因为爱好学习...
然而,是因为前同事写了一个so库,用来保存客户端敏感信息。结果源码给搞丢了...不得不说,真的是很安全啊...
背景
大家都知道,Android应用大部分是利用java语言开发的,并且我们的APK文件又是可以轻松获取的。那么一些敏感的数据如果放在java层的话,那就很可能被别有用心的人反编译出来,用于一些非法用途,造成无法挽回的损失。
我们一般会采用混淆、加壳等方式对apk进行加固保护,加大反编译的难度。但是,一些常量是不会/不能被混淆的,这种敏感信息就需要我们的额外保护了。
更安全的native代码
相对于java代码容易被反编译,使用NDK开发出来的原生C++代码编译后生成的so库是一个二进制文件,这无疑增加了破解的难度。利用这个特性,可以将客户端敏感信息写在C++代码中,增强应用的安全性。
然而,简单将敏感信息保存在so中就足够安全了吗?
并不是,虽然native方法与java代码存在一个对应关系,如包名、路径、方法名称,但这也是可以轻松实现的。所以,将敏感信息存放在so库中有一个不得不考虑的问题就是,万一别人将你的so库直接copy出来拿去用了呢?因此,我们还需要在native层对应用的包名、签名进行鉴权校验,如果不是自己的应用,不返回相关信息,或者直接退出应用!
创建包含C/C++代码的项目
该项目示例使用Android Studio 3.0.1编译调试。如果需要将生成的so文件用于正式项目中,那么必须保证创建so的项目与你使用so项目的包名、方法路径、方法名相一致,不然将无法正常调用so库中的native代码!
注意,若你的Android Studio不能创建支持C++代码的项目,请先下载NDK、CMake等相关组件。
编写代码
- 在默认生成的cpp目录下添加头文件
native-lib.h
#include
#ifndef JNIDEMO_NATIVE_LIB_H
#define JNIDEMO_NATIVE_LIB_H
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL
Java_com_fedming_jnidemo_JniUtils_getSignature(JNIEnv *env);
#ifdef __cplusplus
}
#endif
#endif //JNIDEMO_NATIVE_LIB_H
- 编写鉴权并返回相关信息的C++方法
native-lib.cpp
#include
#include
#include
#include
#include "native-lib.h"
extern "C"
JNIEXPORT jstring JNICALL
Java_com_fedming_jnidemo_JniUtils_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
extern "C"
const char *AUTH_KEY = "MySecretSignature";
const char *PACKAGE_NAME = "com.fedming.jnidemo";
const char *RELEASE_SIGN_MD5 = "2634EFD64B5CDD13A9CCA6049949D26E";
/**
* getApplication
* @param env
* @return j_object
*/
static jobject getApplication(JNIEnv *env) {
jobject application = NULL;
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
if (activity_thread_clz != NULL) {
jmethodID currentApplication = env->GetStaticMethodID(
activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication != NULL) {
application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
}
env->DeleteLocalRef(activity_thread_clz);
}
return application;
}
/**
* HexToString
* @param source
* @param dest
* @param sourceLen
*/
static void ToHexStr(const char *source, char *dest, int sourceLen) {
short i;
char highByte, lowByte;
for (i = 0; i < sourceLen; i++) {
highByte = source[i] >> 4;
lowByte = (char) (source[i] & 0x0f);
highByte += 0x30;
if (highByte > 0x39) {
dest[i * 2] = (char) (highByte + 0x07);
} else {
dest[i * 2] = highByte;
}
lowByte += 0x30;
if (lowByte > 0x39) {
dest[i * 2 + 1] = (char) (lowByte + 0x07);
} else {
dest[i * 2 + 1] = lowByte;
}
}
}
/**
*
* byteArrayToMd5
* @param env
* @param source
* @return j_string
*/
static jstring ToMd5(JNIEnv *env, jbyteArray source) {
// MessageDigest
jclass classMessageDigest = env->FindClass("java/security/MessageDigest");
// MessageDigest.getInstance()
jmethodID midGetInstance = env->GetStaticMethodID(classMessageDigest, "getInstance",
"(Ljava/lang/String;)Ljava/security/MessageDigest;");
// MessageDigest object
jobject objMessageDigest = env->CallStaticObjectMethod(classMessageDigest, midGetInstance,
env->NewStringUTF("md5"));
jmethodID midUpdate = env->GetMethodID(classMessageDigest, "update", "([B)V");
env->CallVoidMethod(objMessageDigest, midUpdate, source);
// Digest
jmethodID midDigest = env->GetMethodID(classMessageDigest, "digest", "()[B");
jbyteArray objArraySign = (jbyteArray) env->CallObjectMethod(objMessageDigest, midDigest);
jsize intArrayLength = env->GetArrayLength(objArraySign);
jbyte *byte_array_elements = env->GetByteArrayElements(objArraySign, NULL);
size_t length = (size_t) intArrayLength * 2 + 1;
char *char_result = (char *) malloc(length);
memset(char_result, 0, length);
ToHexStr((const char *) byte_array_elements, char_result, intArrayLength);
// 在末尾补\0
*(char_result + intArrayLength * 2) = '\0';
jstring stringResult = env->NewStringUTF(char_result);
// release
env->ReleaseByteArrayElements(objArraySign, byte_array_elements, JNI_ABORT);
// 指针
free(char_result);
return stringResult;
}
JNIEXPORT jstring JNICALL
Java_com_fedming_jnidemo_JniUtils_getSignature(JNIEnv *env) {
jobject context = getApplication(env);
// 获得Context类
jclass cls = env->GetObjectClass(context);
// 得到getPackageManager方法的ID
jmethodID mid = env->GetMethodID(cls, "getPackageManager",
"()Landroid/content/pm/PackageManager;");
// 获得应用包的管理器
jobject pm = env->CallObjectMethod(context, mid);
// 得到getPackageName方法的ID
mid = env->GetMethodID(cls, "getPackageName", "()Ljava/lang/String;");
// 获得当前应用包名
jstring packageName = (jstring) env->CallObjectMethod(context, mid);
const char *c_pack_name = env->GetStringUTFChars(packageName, NULL);
// 比较包名,若不一致,直接return包名
if (strcmp(c_pack_name, PACKAGE_NAME) != 0) {
return (env)->NewStringUTF(c_pack_name);
}
// 获得PackageManager类
cls = env->GetObjectClass(pm);
// 得到getPackageInfo方法的ID
mid = env->GetMethodID(cls, "getPackageInfo",
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
// 获得应用包的信息
jobject packageInfo = env->CallObjectMethod(pm, mid, packageName, 0x40); //GET_SIGNATURES = 64;
// 获得PackageInfo 类
cls = env->GetObjectClass(packageInfo);
// 获得签名数组属性的ID
jfieldID fid = env->GetFieldID(cls, "signatures", "[Landroid/content/pm/Signature;");
// 得到签名数组
jobjectArray signatures = (jobjectArray) env->GetObjectField(packageInfo, fid);
// 得到签名
jobject signature = env->GetObjectArrayElement(signatures, 0);
// 获得Signature类
cls = env->GetObjectClass(signature);
mid = env->GetMethodID(cls, "toByteArray", "()[B");
// 当前应用签名信息
jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid);
//转成jstring
jstring str = ToMd5(env, signatureByteArray);
char *c_msg = (char *) env->GetStringUTFChars(str, 0);
if (strcmp(c_msg, RELEASE_SIGN_MD5) == 0) {
return (env)->NewStringUTF(AUTH_KEY);
} else {
return (env)->NewStringUTF(c_msg);
}
}
可以看到,代码中的注释已经挺详细的了。我们利用jni在C++中校验了应用的包名以及签名的MD5值,若不符合预期,则不返回敏感信息。
其中获取应用签名的MD5值可以使用以下java代码,与包名等信息提前写到C++代码中用于对比。这里提醒一下,注意debug签名与release签名的区别,在平时的调试中也建议统一使用正式版签名。
public static String getSignMd5Str(Context context) {
try {
@SuppressLint("PackageManagerGetSignatures")
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;
Signature sign = signs[0];
return encryptionMD5(sign.toByteArray());
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return "";
}
public static String encryptionMD5(byte[] byteStr) {
MessageDigest messageDigest = null;
StringBuilder md5StrBuff = new StringBuilder();
try {
messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
messageDigest.update(byteStr);
byte[] byteArray = messageDigest.digest();
for (byte aByteArray : byteArray) {
if (Integer.toHexString(0xFF & aByteArray).length() == 1) {
md5StrBuff.append("0").append(Integer.toHexString(0xFF & aByteArray));
} else {
md5StrBuff.append(Integer.toHexString(0xFF & aByteArray));
}
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return md5StrBuff.toString().toUpperCase();
}
编译打包
- gradle配置
ndk {
// 指定输出/支持的abi架构
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
}
- 生成so库
- 查看效果
使用Android Studio的Analyze apk功能可以看到,我们直接在项目中添加C++代码,最终生成的apk文件就包含了我们的so库。当然,直接使用编译出来so文件也是一样的!
客户端获取敏感信息
public class JniUtils {
//加载so
static {
System.loadLibrary("native-lib");
}
public static native String stringFromJNI();
public static native String getSignature();
}
总结
在探索JNI敏感信息保护的过程中遇到了不少麻烦,主要是对C++代码不熟悉的缘故。期间参照了一些前人的代码片段,但大部分资料混乱不堪、千奇百怪,多与本人需求出入。而且代码老旧,并非使用最新的构建工具,已不能在新版IDE中正常编译运行。所以,便有了这一解决方案与源码,毕竟坑要自己一个个踩过才深刻。
最后,安全是相对的,混淆、加壳、关键代码native化也好,只能增加破解的难度,相信最终破解只是时间成本而已。但是,攻与防之间的博弈,总不能让对方轻松取胜吧?做好基本安全策略,让应用更加安全可靠也是我们开发者义不容辞的责任!按照惯例,附送源码一份!
参考资料:
破解Android JNI签名校验
向您的项目添加 C 和 C++ 代码