背景
隨著移動網際網路的發展,移動應用的安全問題越來越突顯,特別是涉及到錢相關的產品,前段一段時間,我們的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);
// 在末尾補\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);
return stringResult;
}
jstring loadSignature(JNIEnv *env, jobject context) {
// 獲得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);
}
void ByteToHexStr(const char *source, char *dest, int sourceLen) {
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;
}
}
}
複製程式碼
以下就是在native層實現簽名檢驗的邏輯,下面接下來說一下如何編譯。
如何編譯,mk vs gradle
建立jni的過程,這裡就不多說,網上有很多相關的文章,簡單地說,就是 src/main/
路徑下,建立jni資料夾,然後把native的程式碼放在這個目錄下,在根目錄下在,建立對應的Android.mk和Application.mk檔案,關於 Android.mk
和 Application.mk
的說明,請參考Android開發的官方文件:
如果是使用gradle構建的話,需要作一點配置,新增一個 ndk
block,gradle裡面的配置會覆蓋 mk 中設定的。
defaultConfig {
...
ndk {
// so庫的名字
moduleName 'libAppRuntime_V1_0'
// 支援armeabi和armeabi-v7a
abiFilters("armeabi", "armeabi-v7a")
// 依賴的類庫
ldLibs("log")
}
}
複製程式碼
- moduleName:so庫的名字
- abiFilters:so庫的平臺
- ldLibs:依賴的類庫,這裡需要輸出Log到logcat中,所以依賴log這個Android提供的庫
Android Gradle外掛如何編譯出library module的debug包
為什麼需要debug模式的library呢?因為我是想主工程打debug模式的包,library也是走debug的配置,如果主工程是release配置模式,那麼library也是release的配置,這樣做的目的是為了定義 RELEASE_MODE=1 這樣的巨集,是為了控制在不同的版本的so庫執行不同的業務邏輯。
由於上述的功能邏輯是放到一個library module中的,那麼就面臨一個問題,library module如何打出debug包,對於library Android預設是打出release模式的,這一點可以從編譯出來的BuildConfog.DEBUG
恆為false可以看出。
那麼到底要如何才能打出debug的aar呢?為了實現這個功能,再真費了點勁。直接上結論!
參考文件:Gradle外掛不能編譯出library模組的DEBUG模式
文中也有人說到了不能打debug模式的包:
Well, Gradle Android plugin simply can't build the debug version of dependent library modules. This is a well-known, old issue and this is not resolved yet.
You can try to use some workarounds from the discussion I mentioned, specifically take a look at posts #35 and #38.
解決方案也大概如文中所說的,也進行了多次嘗試:
主工程依賴方式需要改:
通常是這樣引用libraray module
compile project(':AppLibrary')
需要改成這樣:
debugCompile project(path: ':AppLibrary', configuration: 'debug')
releaseCompile project(path: ':AppLibrary', configuration: 'release')
再看看library modlue的gradle配置:
- 首先
buildTypes
增加debug
和release
配置塊 - 在
android
塊裡面增加publishNonDefault true
這樣改後,我們編譯後就可以發現生成的檔案中會有debug和release兩個資料夾了,如下圖所示:
JNI開發的一些tips
官方的tips請點選這裡:JNI Tips
在開發過程中,遇到了一些比較蛋疼的問題,給大家說說,避免踩坑。
1、生成 .h 標頭檔案
如果native方法中引用了android的類,例如Context之類的,需要顯示指定--classpath
參考連結:android - javah doesn't find my class
If you are on Linux or MAC-OS, use ":" to separate the directories for classpath rather than ";" character: Example:
javah -cp /Users/Android/android-sdk/platforms/android-xy/android.jar:. com.test.JniTest
2、JNI so庫未找到方法實現
如果實現是C++(後續是cpp),沒有標頭檔案(.h)的話,需要在介面實現處新增上 extern "C"
,簡單地說,C++的實現需要向前相容C的實現,關於 extern "C"的作用,這裡不多講,
可以參考:extern "C"用法解析
extern "C" JNIEXPORT jboolean JNICALL
Java_com_xxxx_android_AppRuntime_checkSignature(
複製程式碼
總結
1、上述的東西,可以再進一步封裝,獨立成為一個libaray module,提供一個sdk,輸出的就是aar,Java層面上就是一個NativeContext類,這個類的setAppContext()
介面必須由業務方來呼叫。
2、上述提到的 so 最大的一個好處是自己具備識別簽名的能力,只能在我們自己的app裡面使用,別人是無法用的