Android so库中JNI方法混淆

mbxs6504 8年前
   <p>​​默认情况下,使用JNI时与native对应的JNI函数名都是Java <em>包名(点替换为</em> ) <em>类名</em> 方法名,使用javah生成的头文件函数名就是这样的格式。这样的格式的so库被反汇编时很容易就找到对应的方法。</p>    <pre>  <code class="language-cpp">JNIEXPORT jstring JNICALL Java_com_liuling_ndkjnidemo_JniUtils_getStringFromC          (JNIEnv *env, jclass obj) {      return (jstring)(*env)-> NewStringUTF(env, "I am string from jni");  }  </code></pre>    <p>上面是简单的一个JNI方法,我们将生成的so库使用IDA工具进行反汇编之后就能看到如下的内容:</p>    <p>在左边很容易就能找到Java_com_liuling_ndkjnidemo_JniUtils_getStringFromC这个方法:</p>    <p><img src="https://simg.open-open.com/show/33c1cf604982d088eac6f9b2cd19dc9f.png"></p>    <p>双击该方法就能看到该方法反汇编之后的内容,这里返回的字符串”I am string from jni”就暴露出来了,如果是一些敏感信息比如一些key之类的东西,这样就存在着风险。</p>    <p><img src="https://simg.open-open.com/show/586a19f041231044ff2f5c1fbc5b6716.png"></p>    <p>经上网搜索,发现有一种方法可以让JNI中的方法名不适用javah生成的风格,方法名随便取,并且可以将方法隐藏起来,反汇编之后找不到对应的方法,类似于Android中的混淆,加大了破解的难度。</p>    <p>这种方法的特点是:</p>    <ul>     <li>源码改动少,只需要添加JNI_Onload函数</li>     <li>无需加解密so,就可以实现混淆so中的JNI函数</li>     <li>后续可以添加so加解密,使破解难度更大</li>    </ul>    <p>下面来看一个例子:</p>    <p>Java层代码</p>    <pre>  <code class="language-cpp">public class JniUtils {      static {          System.loadLibrary("NDKJNIDemo");//与build.gradle里面设置的so名字,必须一致      }      public static native String getStringFromC();  }  </code></pre>    <p>JNI层代码</p>    <p>第一步:我们要写一个JNI_Onload,来自定义JNI函数的函数名,要加入头文件#include</p>    <pre>  <code class="language-cpp">#include <assert.h>  #include "com_liuling_ndkjnidemo_JniUtils.h"    #define JNIREG_CLASS "com/liuling/ndkjnidemo/JniUtils"//指定要注册的类    /**  * 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;  }  </code></pre>    <p>第二步:Java层函数所对应的函数的实现:</p>    <pre>  <code class="language-cpp">__attribute__((section (".mytext"))) JNICALL jstring getStringc(JNIEnv *env, jclass obj) {      return (jstring)(*env)-> NewStringUTF(env, "I am string from jni22222");  }  </code></pre>    <p>这里的关键是,在函数前加上 <strong>attribute</strong> ((section (“.mytext”))),这样的话,编译的时候就会把这个函数编译到自定义的名叫”.mytext“的section里面去了。</p>    <p>最后一步:隐藏符号表,在Android.mk文件里面添加一句LOCAL_CFLAGS := -fvisibility=hidden</p>    <pre>  <code class="language-cpp">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)  </code></pre>    <p>这样就OK了,程序跑起来的效果和之前没有任何区别。</p>    <p>下面我们用IDA来看一下混淆后的效果:</p>    <p><img src="https://simg.open-open.com/show/94f38825196d31f63ee77b02dc9a5c65.png"></p>    <p>在IDA里面看不到getStringc()函数,其次getStringc()函数的符号表是没有的,这个函数放在.mytext里面,而且整个逻辑是完全混淆的,数据和代码混在一起了(其实是IDA以为是ARM指令),这样就加大了so库破解的难度。</p>    <p>上面混淆方案的实现原理其实很简单,当在系统中调用System.loadLibrary函数时,该函数会找到对应的so库,然后首先试图找到”JNI_OnLoad”函数,如果该函数存在,则调用它。</p>    <p>JNI_OnLoad可以和JNIEnv的registerNatives函数结合起来,实现动态的函数替换。如果在so库中没有找到”JNI_OnLoad”函数,则会在调用的时候解析javah风格的函数。</p>    <p> </p>    <p>来自: <a href="/misc/goto?guid=4959674148481490544" rel="nofollow">http://liuling123.com/2016/06/so_method_mix.html</a></p>    <p> </p>