Android so庫中JNI方法混淆

mbxs6504 8年前發布 | 33K 次閱讀 JNI 安卓開發 Android開發 移動開發

??默認情況下,使用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

 

 本文由用戶 mbxs6504 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!