Android so庫中JNI方法混淆
??默認情況下,使用JNI時與native對應的JNI函數名都是Java 包名(點替換為 ) 類名 方法名,使用javah生成的頭文件函數名就是這樣的格式。這樣的格式的so庫被反匯編時很容易就找到對應的方法。
JNIEXPORT jstring JNICALL Java_com_liuling_ndkjnidemo_JniUtils_getStringFromC
(JNIEnv *env, jclass obj) {
return (jstring)(*env)-> NewStringUTF(env, "I am string from jni");
}
上面是簡單的一個JNI方法,我們將生成的so庫使用IDA工具進行反匯編之后就能看到如下的內容:
在左邊很容易就能找到Java_com_liuling_ndkjnidemo_JniUtils_getStringFromC這個方法:
雙擊該方法就能看到該方法反匯編之后的內容,這里返回的字符串”I am string from jni”就暴露出來了,如果是一些敏感信息比如一些key之類的東西,這樣就存在著風險。
經上網搜索,發現有一種方法可以讓JNI中的方法名不適用javah生成的風格,方法名隨便取,并且可以將方法隱藏起來,反匯編之后找不到對應的方法,類似于Android中的混淆,加大了破解的難度。
這種方法的特點是:
- 源碼改動少,只需要添加JNI_Onload函數
- 無需加解密so,就可以實現混淆so中的JNI函數
- 后續可以添加so加解密,使破解難度更大
下面來看一個例子:
Java層代碼
public class JniUtils {
static {
System.loadLibrary("NDKJNIDemo");//與build.gradle里面設置的so名字,必須一致
}
public static native String getStringFromC();
}
JNI層代碼
第一步:我們要寫一個JNI_Onload,來自定義JNI函數的函數名,要加入頭文件#include
#include <assert.h>
#include "com_liuling_ndkjnidemo_JniUtils.h"
#define JNIREG_CLASS "com/liuling/ndkjnidemo/JniUtils"http://指定要注冊的類
/**
* Table of methods associated with a single class.
*/
//綁定,注意,V,Z簽名的返回值不能有分號“;”
//這里就是把JAVA層的getStringFromC()函數綁定到Native層的getStringc()函數,就無需使用原生的Java_com_xx_xx_classname_methodname這種惡心的函數命名方式了
static JNINativeMethod gMethods[] = {
{ "getStringFromC", "()Ljava/lang/String;", (void*)getStringc},
};
/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
return JNI_FALSE;
}
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* Register native methods for all classes we know about.
*/
static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env, JNIREG_CLASS, gMethods,
sizeof(gMethods) / sizeof(gMethods[0])))
return JNI_FALSE;
return JNI_TRUE;
}
/*
* Set some test stuff up.
*
* Returns the JNI version on success, -1 on failure.
*/
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) {//注冊
return -1;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;
return result;
}
第二步:Java層函數所對應的函數的實現:
__attribute__((section (".mytext"))) JNICALL jstring getStringc(JNIEnv *env, jclass obj) {
return (jstring)(*env)-> NewStringUTF(env, "I am string from jni22222");
}
這里的關鍵是,在函數前加上 attribute ((section (“.mytext”))),這樣的話,編譯的時候就會把這個函數編譯到自定義的名叫”.mytext“的section里面去了。
最后一步:隱藏符號表,在Android.mk文件里面添加一句LOCAL_CFLAGS := -fvisibility=hidden
LOCAL_PATH := $(call my-dir)
local_c_includes := \
$(NDK_PROJECT_PATH) \
include $(CLEAR_VARS)
LOCAL_CFLAGS := -fvisibility=hidden #隱藏符號表
LOCAL_MODULE := NDKJNIDemo
LOCAL_SRC_FILES := com_liuling_ndkjnidemo_JniUtils.c
P
include $(BUILD_SHARED_LIBRARY)
這樣就OK了,程序跑起來的效果和之前沒有任何區別。
下面我們用IDA來看一下混淆后的效果:
在IDA里面看不到getStringc()函數,其次getStringc()函數的符號表是沒有的,這個函數放在.mytext里面,而且整個邏輯是完全混淆的,數據和代碼混在一起了(其實是IDA以為是ARM指令),這樣就加大了so庫破解的難度。
上面混淆方案的實現原理其實很簡單,當在系統中調用System.loadLibrary函數時,該函數會找到對應的so庫,然后首先試圖找到”JNI_OnLoad”函數,如果該函數存在,則調用它。
JNI_OnLoad可以和JNIEnv的registerNatives函數結合起來,實現動態的函數替換。如果在so庫中沒有找到”JNI_OnLoad”函數,則會在調用的時候解析javah風格的函數。
來自: http://liuling123.com/2016/06/so_method_mix.html