很多時候,為了保護一些核心代碼或者增加效率,我們通常會把一些可以在Java層實現的代碼寫到C層,通過Jni來調用。
因為Java代碼很容易被反編譯,但so包的代碼相對而言沒有那么容易被破解(雖然花費一點時間還是可以的,度娘有好多這種資料),所以很多加密解密等代碼放在so里面是個比較不錯的選擇。
然而,我們都知道,so包也是可以隨便通過System.loadLibrary來加載的(so放在lib文件夾下的情況)。
舉個例子,比如我們有一段加密的操作是封裝到so里面,假如我是一個破解者,我通過反編譯java文件得到native方法的調用,然后我直接加載so包,并在我們的包下面創建相同的包名(假設已經破譯出混淆后代碼的包名),這樣一來我們就不需要知道so里面的代碼,直接使用就可以了。
因此,對于so包的使用者,我們就需要做一個身份驗證。
工程搭建
本文需要準備兩個工程(ndk環境等等的配置這里不詳細說明):
- 原工程,包含兩個module(包名:reazerdp.com.mytestso,native包名:reazerdp.com.mynative)
- 測試工程(包名:reazerdp.com.myhacktest)
- Native方法:initLib(),getPassword()
在Native的module里面創建出我們的測試代碼:
首先寫出native的方法,這時候方法名紅名先不管
然后寫出我們的測試類,so包的名字我們暫定為MyTestLib
按照傳統方法,我們需要通過javah
來生成頭文件,但我們這次就不這么做了,一來麻煩而來實在不太喜歡長長的方法名,因此我們是需要接下來我們直接新建一個c++文件,并且引入常用的幾個頭文件和定義一些宏
接著寫入我們的MK文件,其中MODULE名字就是取我們MyTest.java里面的那個("MyTestLib")
同樣新建一個Application.mk,定義需要輸出的平臺
隨后右鍵我們的cpp,選擇link c++,選擇ndk并選到我們的mk文件,等待as幫我們添加到gradle就好了。
cpp連接
step1:定義包名
既然我們不采用常規的“包名_方法名”的寫法,那么我們就需要做一個方法映射,首先把我們的包名定義好,就是把點換成斜杠
#define PACKATE_PATH "reazerdp/com/mynative/MyNative"
step2:定義方法
然后編寫我們的native方法,至于命名隨意,我這里就統一以native_開頭,其中前面兩個都是固定的,第三個是傳入來的參數,除了基本變量外,基本上都是object(對應到jni就是jobject)
JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {
}
JNICALL jstring native_getPassword(JNIEnv *env, jobject obj) {
return env->NewStringUTF("返回了一個測試密碼:123");
}
step3:定義映射表
具體格式是:類方法名,函數簽名,cpp里面的函數
static JNINativeMethod nativeMethods[] = {
{"initLib", "(Landroid/content/Context;)Z", (void *) native_initLib},
{"getPassword", "()Ljava/lang/String;", (void *) native_getPassword}
};
查看方法簽名可以用javap -s xxx.class
查看。
具體操作時先build一次工程,然后在對應module的build文件夾下生成的classes找到
如:
step4:加載方法
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv *env) {
return registerNativeMethods(env, PACKATE_PATH, nativeMethods, NELEM(nativeMethods));
}
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("JNI_ONLOAD:%s", "failed");
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) {//注冊
return -1;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;
return result;
}
so編譯并測試
復制我們的cpp所在文件目錄,在命令行下編譯,具體命令只有一個:ndk-build(需要配置環境)
編譯出的so包會在對應module下的libs文件夾下,編譯完之后我們就可以將so包復制到我們的工程里面了
接下來我們測試一下so包是否可用
首先去gradle加一下我們的jnilib:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
然后log一下我們的pass:
可以看到現在已經返回了我們的密碼了。
so的驗證方式
身份驗證的方式有很多種,聯網的話方法更是多樣,這里我們不談聯網驗證,只談談單機存在的app如何進行so身份驗證。
就目前而言,比較流行的是包簽名驗證,至于簽名驗證的方法這里就簡單說下,具體網上也有很多。
我們這里在initLib里面先簡單的填寫一下當前流行的身份驗證:
JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {
jclass contextClass = env->FindClass("android/content/Context");
jclass signatureClass = env->FindClass("android/content/pm/Signature");
jclass packageNameClass = env->FindClass("android/content/pm/PackageManager");
jclass packageInfoClass = env->FindClass("android/content/pm/PackageInfo");
jmethodID getPackageManagerId = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jmethodID getPackageNameId = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
jmethodID signToStringId = env->GetMethodID(signatureClass, "toCharsString", "()Ljava/lang/String;");
jmethodID getPackageInfoId = env->GetMethodID(packageNameClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jobject packageManagerObject = env->CallObjectMethod(contextObject, getPackageManagerId);
jstring packNameString = (jstring) env->CallObjectMethod(contextObject, getPackageNameId);
jobject packageInfoObject = env->CallObjectMethod(packageManagerObject, getPackageInfoId, packNameString, 64);
jfieldID signaturefieldID = env->GetFieldID(packageInfoClass, "signatures", "[Landroid/content/pm/Signature;");
jobjectArray signatureArray = (jobjectArray) env->GetObjectField(packageInfoObject, signaturefieldID);
jobject signatureObject = env->GetObjectArrayElement(signatureArray, 0);
jstring signatureStr = (jstring) env->CallObjectMethod(signatureObject, signToStringId);
const char *signStrng = env->GetStringUTFChars(signatureStr, 0);
env->DeleteLocalRef(contextClass);
env->DeleteLocalRef(signatureClass);
env->DeleteLocalRef(packageNameClass);
env->DeleteLocalRef(packageInfoClass);
if (strcmp(signStrng, RELEASE_SIGN) == 0) {
env->ReleaseStringUTFChars(signatureStr, signStrng);
auth = JNI_TRUE;
return JNI_TRUE;
} else {
auth = JNI_FALSE;
return JNI_FALSE;
}
}
代碼有點長,其實主要都是把java的獲取簽名方法翻譯一遍而已。
其中RELEASE_SIGN
這個數據是事先打包獲取的簽名,在java層獲取簽名代碼如下:
public static String getSignature(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
Signature[] signatures = packageInfo.signatures;
return signatures[0].toCharsString();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
最后修改一下我們獲取密碼的代碼:
JNICALL jstring native_getPassword(JNIEnv *env, jobject obj) {
if (auth) {
return env->NewStringUTF("返回了一個測試密碼:123");
} else{
return env->NewStringUTF("驗證不通過,不返回密碼");
}
}
最后我們測試的時候就可以看到驗證信息:
嘗試繞過驗證
雖然現在看起來挺安全的,但是我們有沒有想過這個是不是可以繞過驗證,直接獲取方法呢?
很簡單,我們試試就知道了。
接下來切換到我們的hack工程,把so包復制過去并配置gradle。
接下啦我們需要新建一個跟原工程一樣路徑的包,并把java代碼復制過去:(reazerdp.com.mynative)
做到這里,我們就相當于偽造了一個包。。。
接下來我們開始盜用我們測試應用的context,至于怎么盜用,非常簡單。。。
代碼如下:
Context context=this.createPackageContext("reazerdp.com.mytestso",CONTEXT_INCLUDE_CODE|CONTEXT_IGNORE_SECURITY);
因為我們是在activity下寫的這句代碼,所以直接用this代替context即可。
接著我們用創建出的這個context來進行盜用。
通過圖片上的代碼我們不難看出,通過createPackageContext創建出來的context毫無疑問通過了驗證,而傳入我們hack工程的context則是無法通過驗證的。
分析
從上面的測試不難看出,簡單的身份驗證這個方法似乎不能有效的防止盜用,從根本上來說,是因為我們可以使用到別的項目的context,只要調用類的包名對得上,就可以繞過這種驗證方式了。
我們都知道,相同包名的兩個應用是不能共存的,而通過一個context創建出來的context也必然會有一些自己的消息。
所以我們debug一下我們創建出來的context
我們可以看到,packageinfo里面的包名是我們創建出的context的包名,但同時有個mBasePackageName是屬于我們這個工程的包名。
那么我們是否可以在這方面入手呢?比如我們在驗證簽名的同時還要驗證傳進來的context的basepackagename
————答案是:不可取- -
原因有二:
- 首先,在api17或者以下,contextWrapper沒辦法獲取到這個字段
- 其次,既然我們在java層創建出了context,也就意味著我們可以直接反射改掉這個字段,從而繞過驗證。
雖然從驗證字段這方面不能入手,但是從這個方向上來說,我們如果可以知道當前運行代碼的程序的包名,然后再通過它來驗證的話,似乎就可以解決這個問題。
事實上,很幸運,我們確實可以做得到。
包名驗證
我們都知道,Binder是安卓系統里IPC的方式之一,既然如此,我們必定可以通過Binder獲取一些運行著的應用的信息。
比如pid,uid等。
而如果平時有碰到過packageManager的同學,應該知道packageManager可以通過uid獲取到包的名字。
而我們正是通過這個方式來解決上面說的問題。
在java層,獲取包名的方式是這樣的:
this.getPackageManager().getNameForUid(Binder.getCallingUid());
而翻譯到C,我們則需要下面幾步:
- 獲取到packageManager對象
- 獲取到Binder對象
- 調用getCallingUid()方法
在我們上面的cpp中,我們其實已經獲取過packageManager了,所以這里我們只需要補充一下Binder的即可
JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {
//binder
jclass binderClass = env->FindClass("android/os/Binder");
...獲取packageManager等,跟上面代碼一樣,略過
//反射packageManager的getNameForUid方法
jmethodID getRunningPackageName = env->GetMethodID(packageNameClass, "getNameForUid", "(I)Ljava/lang/String;");
//反射Binder的getCallingUid方法(該方法是靜態方法))
jmethodID getUid = env->GetStaticMethodID(binderClass, "getCallingUid", "()I");
//得到uid
jint uid = env->CallStaticIntMethod(binderClass, getUid);
...獲取簽名等方法,跟上面一樣,略過
//獲取uid對應的app的包名
jstring mRunningPackageName = (jstring) env->CallObjectMethod(packageManagerObject, getRunningPackageName, uid);
//跟我們的包對應并判斷
if (mRunningPackageName) {
const char *runPackageName = env->GetStringUTFChars(mRunningPackageName, 0);
LOGI("rPackageName:%s", runPackageName);
if (strcmp(runPackageName, "reazerdp.com.mytestso") != 0) {
return JNI_FALSE;
}
env->ReleaseStringUTFChars(mRunningPackageName, runPackageName);
} else {
LOGE("rPackageName:%s", "is null");
return JNI_FALSE;
}
...返回值,跟上面的一樣,忽略
}
最后打包我們在測試一次,
這一次,即使是創建出來的context,我們也沒法繞過驗證了。
寫在最后
這個方法我不清楚是否很好,但目前來說可以解決我的需求,如果您有更好的方法,歡迎一起探討-V-
附錄:CPP完整代碼:
//
// Created by 大燈泡 on 2017/1/16.
//
#include <jni.h>
#include <string.h>
#include <assert.h>
#include <android/log.h>
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
#define LOG_TAG "MyNativeLib"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
#define PACKATE_PATH "reazerdp/com/mynative/MyNative"
const char *RELEASE_SIGN = "3082033130820219a0030201020204596e28f5300d06092a864886f70d01010b05003049310b3009060355040613023836310a30080603550408130161310a30080603550407130161310a3008060355040a130161310a3008060355040b130161310a30080603550403130161301e170d3137303131373039303231365a170d3432303131313039303231365a3049310b3009060355040613023836310a30080603550408130161310a30080603550407130161310a3008060355040a130161310a3008060355040b130161310a3008060355040313016130820122300d06092a864886f70d01010105000382010f003082010a02820101009f1f3731ef4c65ccd6c4a7589eaffe813117d2112cc92279f41a22f210398baa2ddae52fd61c736b51b21c01d4a3233fd34b2b29365723bdb285bf0eddd043b7a9dd2829366974a690aa885b859a2d3fb272baf8c3ab94024f97117b6d6a68b74f2ed35daca41ef601a48c9f3393d92a4c3bb6f26152142e03290ef1d607361b0a2759479a7f0b94425bd885db49bcbb777f7dc10e7d3eff1fa4cc3080b4c8524ca6b761732100347b80d56a9bd5f6e7d503debe5c25c60194bd1c34c54f40172f2add9cf7e934aa7e64467c362d87fc91069fd29afc5e3445f609daf4fb99905c6ec17bea73252f6b264fdbb6963f5822997b36af9caccb2869a8b87a942df50203010001a321301f301d0603551d0e041604144aaa523ada5947919a2f7dbbe8cd3711b8dbb08e300d06092a864886f70d01010b050003820101008e6153b54104503b04a04d2746c35ce094688c2f05cd6f8c7edbcabb0d801a57c55f75930081294e63bbe27af5705511d8b7e5e263f0c6a9af58fd8c87fa43e22358c92ec4378ced89aa164f9770ebde94f865572bb846ce2cdf48ec5f6ddd1e4a733a5faca96244cd8e250cec6c0a16740e5bb7907db19d1db260806b4efd890c264ec59d46135b4f82077d3f233f5b349601b217f28d8392d90ae1fd5f462ec7e5889677bbd6c0054ea680b6dc9746077d8d536d7bc5a39dbb3074658c986a8ca14b6110599808d6f4532e32e179af558df1305880d97599d23eda5f25b0b82f091cfd702d187cfbdffc3f5bbbb9f17ae660683b07c566df5622d6e19462f8";
static jboolean auth = JNI_FALSE;
JNICALL jboolean native_initLib(JNIEnv *env, jobject obj, jobject contextObject) {
jclass binderClass = env->FindClass("android/os/Binder");
jclass contextClass = env->FindClass("android/content/Context");
jclass signatureClass = env->FindClass("android/content/pm/Signature");
jclass packageNameClass = env->FindClass("android/content/pm/PackageManager");
jclass packageInfoClass = env->FindClass("android/content/pm/PackageInfo");
jmethodID getPackageManagerId = env->GetMethodID(contextClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
jmethodID getPackageNameId = env->GetMethodID(contextClass, "getPackageName", "()Ljava/lang/String;");
jmethodID signToStringId = env->GetMethodID(signatureClass, "toCharsString", "()Ljava/lang/String;");
jmethodID getPackageInfoId = env->GetMethodID(packageNameClass, "getPackageInfo", "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
jmethodID getRunningPackageName = env->GetMethodID(packageNameClass, "getNameForUid", "(I)Ljava/lang/String;");
jmethodID getUid = env->GetStaticMethodID(binderClass, "getCallingUid", "()I");
jint uid = env->CallStaticIntMethod(binderClass, getUid);
jobject packageManagerObject = env->CallObjectMethod(contextObject, getPackageManagerId);
jstring packNameString = (jstring) env->CallObjectMethod(contextObject, getPackageNameId);
jobject packageInfoObject = env->CallObjectMethod(packageManagerObject, getPackageInfoId, packNameString, 64);
jfieldID signaturefieldID = env->GetFieldID(packageInfoClass, "signatures", "[Landroid/content/pm/Signature;");
jobjectArray signatureArray = (jobjectArray) env->GetObjectField(packageInfoObject, signaturefieldID);
jobject signatureObject = env->GetObjectArrayElement(signatureArray, 0);
jstring mRunningPackageName = (jstring) env->CallObjectMethod(packageManagerObject, getRunningPackageName, uid);
if (mRunningPackageName) {
const char *runPackageName = env->GetStringUTFChars(mRunningPackageName, 0);
LOGI("rPackageName:%s", runPackageName);
if (strcmp(runPackageName, "reazerdp.com.mytestso") != 0) {
return JNI_FALSE;
}
env->ReleaseStringUTFChars(mRunningPackageName, runPackageName);
} else {
LOGE("rPackageName:%s", "is null");
return JNI_FALSE;
}
jstring signatureStr = (jstring) env->CallObjectMethod(signatureObject, signToStringId);
const char *signStrng = env->GetStringUTFChars(signatureStr, 0);
env->DeleteLocalRef(binderClass);
env->DeleteLocalRef(contextClass);
env->DeleteLocalRef(signatureClass);
env->DeleteLocalRef(packageNameClass);
env->DeleteLocalRef(packageInfoClass);
if (strcmp(signStrng, RELEASE_SIGN) == 0) {
env->ReleaseStringUTFChars(signatureStr, signStrng);
auth = JNI_TRUE;
return JNI_TRUE;
} else {
auth = JNI_FALSE;
return JNI_FALSE;
}
}
JNICALL jstring native_getPassword(JNIEnv *env, jobject obj) {
if (auth) {
return env->NewStringUTF("返回了一個測試密碼:123");
} else{
return env->NewStringUTF("驗證不通過,不返回密碼");
}
}
static JNINativeMethod nativeMethods[] = {
{"initLib", "(Landroid/content/Context;)Z", (void *) native_initLib},
{"getPassword", "()Ljava/lang/String;", (void *) native_getPassword}
};
static int registerNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *gMethods, int numMethods) {
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
static int registerNatives(JNIEnv *env) {
return registerNativeMethods(env, PACKATE_PATH, nativeMethods, NELEM(nativeMethods));
}
extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("JNI_ONLOAD:%s", "failed");
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) {//注冊
return -1;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;
return result;
}