概述
背景
随着移动互联网的发展,移动应用的安全问题越来越突显,特别是涉及到钱相关的产品,前段一段时间,我们的Android客户端产品被人破解了,修改了一些代码,重新打包签名后,就可以免费获得资源,对我们的收入造成了一些影响,移动产品安全性就不得不提重视,安全性这个话题很大,包括客户端、服务端、数据存储、协议等很多方面,这里只是从客户端的角度来讨论一上如何保证客户端产品的安全性,抛砖引玉,也希望大家多提意见和建议。
下面主要从以下几个方面来展开讨论:
C/S协议安全
在不使用https的前提下,要保证C/S协议的安全,一般都会进行参数的校验,以及参数加密,客户端和服务端会约定一个固定的字符串作为key,对于客户端来说,这个key应该放到哪里?最早之前,我们是直接放到Java代码中,这样可以说没有什么安全性,后来为了稍微更加安全一点,把这些key都统一放到so库中实现,虽然也不能保证绝对安全,但起码可以增加破解的难度。
你肯定要说,如果这样做,就会有一个问题,如果别人把so拿出来,在直接调用这些native接口,也同样可以获得key,所以也同样不安全,怎么办呢?
能不能让 so 库只能在我们自己的app运行,别人调用就是砖头呢?
各位看官,接着往下看。
so库校验签名
一般的情况下,都会在 Application.onCreate()
方法里面检查当前应用的签名是否合法,如果不合法就直接退出,这种情况其实无法正在防止破解,因为破解的可以找到调用入口,把相应的代码删除,所以这样方法也就失效了,那有没有更好的方案呢?
想到的一种思路就是,so库本身就具体签名校验的机制,当so库被加载时 (JNI_OnLoad()
方法),如果签名不合法,直接失败,so库根本加载不起来。
大概的思路如下代码所示:
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
LOGI("Library JNI_OnLoad begin =========");
if (checkSignature(env) != JNI_TRUE) {
LOGE("
The app signature is NOT correct, please check the apk signture. ");
LOGI("Library JNI_OnLoad end ===========");
return -1;
} else {
LOGI("
The app signature is correct.");
}
LOGI("Library JNI_OnLoad end ===========");
return JNI_VERSION_1_6;
}
复制代码
说明:这里有一个问题,需要注意,在开发过程中,我们不需要检查签名的合法性,只有在release版本才检查,所以上述逻辑还再完善,需要添加上DEBUG和RELASE的判断。
要怎么判断呢?我目前的思路是通过宏来判断,如果定义了宏并且为JNI_TRUE
的话,就认为是release版本。
以下是完整的实现:
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
if (vm->GetEnv((void **) (&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}
LOGI("Library JNI_OnLoad begin =========");
// RELEASE_MODE这个宏是通过编译脚本设定的,如果是release模式,
// 则RELEASE_MODE=1,否则为0或者未定义
#ifdef RELEASE_MODE
if (RELEASE_MODE == 1) {
// 检查当前应用的签名是否一致,如果不签名不一致的话,则直接退出
if (checkSignature(env) != JNI_TRUE) {
LOGE("
The app signature is NOT correct, please check the apk signture. ");
LOGI("Library JNI_OnLoad end ===========");
return -1;
} else {
LOGI("
The app signature is correct.");
}
} else {
// Do nothing
}
#endif
LOGI("Library JNI_OnLoad end ===========");
return JNI_VERSION_1_6;
}
复制代码
RELEASE_MODE 在哪里定义的?
那问题又来了? RELEASE_MODE
宏要在哪里定义?不能改代码吧?
很容易想到通过编译中的buildTypes
来控制,如果当前打release包,那么就定义这个宏。请看Android Stuido的build.gradle中的buildTypes
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
ndk {
// release包定义RELEASE_MODE=1宏,so库中会使用
cFlags "-DRELEASE_MODE=1"
}
}
debug {
// do nothing
}
}
复制代码
我们在buildTypes中添加了一个ndk块,里面定义了 RELEASE_MODE
宏,这里使用了 cFlags。
注意:-DRELEASE_MODE=1
中前面的 -D 不能省略。
checkSignature()如何实现?
最主要的就是解决如何得到当前app的context,我的做法是反射Java层的一个特定的方法,返回 getAppContext()
方法得到context。其中 setAppContext()
方法在 Application.onCreate()中调用。
public class NativeContext implements NoProGuard {
/**
* App context
*/
private static Context sAppContext;
/**
* 得到 app context
*/
public static Context getAppContext() {
return sAppContext;
}
/**
* Set the app context
*/
static void setAppContext(Context appContext) {
sAppContext = appContext;
}
}
复制代码
Native这一层的实现如下:
/**
* 检查加载该so的应用的签名,与预置的签名是否一致
*/
static jboolean checkSignature(JNIEnv *env) {
// 得到当前app的NativeContext类
jclass classNativeContext = env->FindClass(CLASS_NAME_NATIVECONTEXT);
// 得到getAppContext静态方法
jmethodID midGetAppContext = env->GetStaticMethodID(classNativeContext,
METHOD_NAME_GETAPPCONTEXT,
METHOD_SIGNATURE_GETAPPCONTEXT);
// 调用getAppContext方法得到context对象
jobject appContext = env->CallStaticObjectMethod(classNativeContext, midGetAppContext);
if (appContext != NULL) {
jboolean signatureValid = Java_com_xxxx_android_AppRuntime_checkSignature(env, NULL, appContext);
if (signatureValid == JNI_TRUE) {
LOGI("
checkSignature() return true");
} else {
LOGI("
checkSignature() return false");
}
return signatureValid;
}
return JNI_FALSE;
}
复制代码
这里调用了 Java_com_xxxx_android_AppRuntime_checkSignature
方法,它的实现如下所示,核心的思路是将从 Context
里面得到当前app的签名MD5字符串,然后再与预置的常量作比较,调用了 strcmp
C函数。
extern "C" JNIEXPORT jboolean JNICALL
Java_com_xxxx_android_AppRuntime_checkSignature(
JNIEnv *env, jclass clazz, jobject context) {
jstring appSignature = loadSignature(env, context);
jstring releaseSignature = env->NewStringUTF(APP_SIGNATURE);
const char *charAppSignature = env->GetStringUTFChars(appSignature, NULL);
const char *charReleaseSignature = env->GetStringUTFChars(releaseSignature, NULL);
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);
return result;
}
复制代码
APP_SIGNATURE 是const char*的常量,是release版本的签名字符串。
完整的代码如下:
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"));
// 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);
// 在末尾补
*(char_result + intArrayLength * 2) = '