思路
现在的开发,需要硬编码的字符串资源越来越多,包括SDK的Key、与服务器交换数据时生成签名的秘钥、敏感信息等。
这些key写在Java文件中,通过反编译可以轻易获得,为了提升破解者的破解难度,通常做法是写入cpp文件,通过JNI的方式调用获取。
但是写入JNI也不是绝对安全的,so文件如果被人拿出来直接调用native方法,那么也会被攻破(当然走到这一步已经不是一般人了)。
为了防止上面这种情况,我们可以利用so文件加载时会自动调用JNI_OnLoad()
方法的特性,在JNI_OnLoad()
方法中植入APK签名检查,如果APK签名检查通不通过,直接返回-1crash掉。
当然,在cpp里进行签名验证时需要使用一个context对象,在这里的context对象不能使用java方法传入,需要我们使用反射获取,否则root过的手机可以通过伪造context绕过签名。(具体破解原理可以参考 https://www.cnblogs.com/goodhacker/p/4842215.html)
签名验证流程
这样重要的信息保护在so里,而你的应用又不得不依赖这个 so 库进行获取 key,因此它又不能直接剥离,如果别人反编译了你的代码,发现你使用 so 进行签名验证,便直接把这个 so 文件摘掉,这样做的结果是,App 获取不到你存在 so 中的 key 了,便无法正常工作了
开搞
为了能在AS控制台中看到C++打印出的log,需要定义一个方法,方便接下来使用
#define LOG_TAG "native-dev"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
植入签名检查
const char *APP_SIGNATURE = "签名信息";//google
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
// 初次使用时,执行下面两行,在控制台中获得签名,填入APP_SIGNATURE
vm->GetEnv((void **) (&env), JNI_VERSION_1_6);
checkSignature(env);
// 打正式包时,将上面两行注释掉,将下面的代码反注释
// if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
// 获取不到正确的环境,crash
// return -1;
// }
//
// if (checkSignature(env) != JNI_TRUE) {
// 签名验证失败,crash
// return -1;
// }
return JNI_VERSION_1_6;
}
void ByteToHexStr(const char *source, char *dest, int sourceLen) {
// LOGI("ByteToHexStr");
short i;
char highByte, lowByte;
for (i = 0; i < sourceLen; i++) {
highByte = source[i] >> 4;
lowByte = source[i] & 0x0f;
highByte += 0x30;
if (highByte > 0x39) {
dest[i * 2] = highByte + 0x07;
} else {
dest[i * 2] = highByte;
}
lowByte += 0x30;
if (lowByte > 0x39) {
dest[i * 2 + 1] = lowByte + 0x07;
} else {
dest[i * 2 + 1] = lowByte;
}
}
}
// byte数组转MD5字符串
jstring ToMd5(JNIEnv *env, jbyteArray source) {
// LOGI("ToMd5");
// 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"));
// update方法,这个函数的返回值是void,写V
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);
// 将byte数组转换成16进制字符串,发现这里不用强转,jbyte和unsigned char应该字节数是一样的
ByteToHexStr((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
free(char_result);
env->DeleteLocalRef(classMessageDigest);
env->DeleteLocalRef(objMessageDigest);
return stringResult;
}
//获取应用签名
jstring loadSignature(JNIEnv *env, jobject context) {
// LOGI("loadSignature");
// 获得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);
// 获得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);
// 得到toCharsString方法的ID
mid = env->GetMethodID(cls, "toByteArray", "()[B");
// 返回当前应用签名信息
jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod(signature, mid);
return ToMd5(env, signatureByteArray);
}
//检测签名是否匹配
jboolean checkSignature( JNIEnv *env, jobject context) {
LOGI("checkSignature");
jstring appSignature = loadSignature(env, context); // 当前 App 的签名
jstring releaseSignature = env->NewStringUTF(APP_SIGNATURE); // 发布时候的签名
const char *charAppSignature = env->GetStringUTFChars(appSignature, NULL);
const char *charReleaseSignature = env->GetStringUTFChars(releaseSignature, NULL);
// 打正式包时记得将这句log信息注释掉
LOGI("%s",charAppSignature);
jboolean result = JNI_FALSE;
// 比较是否相等
if (charAppSignature != NULL && charReleaseSignature != NULL) {
if (strcmp(charAppSignature, charReleaseSignature) == 0) {
result = JNI_TRUE;
}
}
env->ReleaseStringUTFChars(appSignature, charAppSignature);
env->ReleaseStringUTFChars(releaseSignature, charReleaseSignature);
// LOGI("check result: %d", result);
return result;
}
static jobject getApplication(JNIEnv *env) {
// LOGI("getApplication");
jobject application = NULL;
jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
if (activity_thread_clz != NULL) {
LOGI("activity_thread_clz != NULL");
jmethodID currentApplication = env->GetStaticMethodID(
activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
if (currentApplication != NULL) {
LOGI("currentApplication != NULL");
application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
} else {
LOGI("Cannot find method: currentApplication() in ActivityThread.");
}
env->DeleteLocalRef(activity_thread_clz);
} else {
// LOGI("Cannot find class: android.app.ActivityThread");
}
return application;
}
/**
* 检查加载该so的应用的签名,与预置的签名是否一致
*/
static jboolean checkSignature(JNIEnv *env) {
LOGI("checkSignature 检查加载该so的应用的签名,与预置的签名是否一致");
// 调用 getContext 方法得到 Context 对象
jobject appContext = getApplication(env);
if (appContext != NULL) {
jboolean signatureValid = checkSignature(
env, appContext);
return signatureValid;
} else {
// LOGI("appContext is null");
}
return JNI_FALSE;
}
结尾福利
加入签名验证后的App,已经收集到一些root手机试图进行破解的操作了。我们的App以广告收入为主,还没有平服务器花销和口粮钱,一些用户又不愿意付费支持还要破解我们加入的广告,真是无奈。
签名验证失败,直接crash