Android SO文件保护加固——混淆篇(一)

    xiaoxiao2021-03-25  127

           继上次基于源码级别和二进制级别的SO文件的核心函数保护后,没看的网友可以点击:点击打开链接;这篇是针对我们在JNI开发过程中利用javah生成本地层对应的函数名类似于java_com_XX这种形式,很容易被逆向者在逆向so的时候在IDA的Exports列表中找到这样一个问题,我们的目的就是让IDA在反汇编过程显示不出来,以及就算找到函数实现也是乱码的形式接下来开始搞;有问题欢迎大家批评指正和讨论。

    问题篇:

    比如拿上一篇中的例子来说,我们在破解分析的时候,在Java层看到调用libegg.so文件,我们用IDA打开,很容易的看到:

    造成的结果会使得破解者很容易的切入主题,因此我们接下来就要解决这个问题。

    原理篇:

    我们知道JNI就是在java层与本地层之间起着一个桥梁的作用,因为java层是运行在Dalvik虚拟机中,而本地层则不会,因此这里在进入主题前很有必要理解一些几个问题: 1.java层虚拟机需要使用到哪些的本地层的lib库?

    2.java层与本地层是怎么建立起一个对应的映射关系?

    这时候我们不得不分析Android源码:

    首先我们知道对于第一个问题,在JNI开发的过程中我们会在java层编写这种形式:

    第一:告诉虚拟机去加载用static里面的libegg.so的动态链接库;

    第二:告诉虚拟机用native声明的getStringFromNative的方法是在本地层实现的;

    对于第二个问题,当虚拟机加载这个libegg.so这个库的时候,从java层进入本地层首先会执行JNI_Onload这个函数,所以可以在JNI_OnLoad函数中完成一些native层组件的初始化工作,同时更加重要的是,通常在JNI_jint JNI_OnLoad(JavaVM* vm, void* reserved)函数中会注册java层的native方法,提到注册就不得不提到一个很重要的一个静态函数registerNativeMethods:

    传统java  Jni方式:1.编写带有native方法的Java类;--->2.使用javah命令生成.h头文件;--->3.编写代码实现头文件中的方法,这样的“官方” 流程,是我们认识到这样会带来java_com_xxxx这样很容易被逆向者发现的弊端; 通用方式:RegisterNatives方法能帮助你把c/c++中的方法隐射到Java中的native方法,而无需遵循特定的方法命名格式。应用层级的Java类别透过VM而呼叫到本地函数。一般是仰赖VM去寻找*.so里的本地函数。如果需要连续呼叫很多次,每次都需要寻找一遍,会多花许多时间。此时,组件开发者可以自行将本地函数向VM进行登记。

    VM调registerNativeMethods()函数的用途有二:  

    (1)更有效率去找到函数。  

    (2)可在执行期间进行抽换。

    由于gMethods[]是一个<名称,函数指针>对照表,在程序执行时,可多次呼叫registerNativeMethods()函数来更换本地函数之指针,而达到弹性抽换本地函数之目的。这就引出了本文解决以上问题所采用的办法:

    第一步:自定义JNI_Onload,来自定义JNI函数的函数名,通过registerNativeMethods()函数来更换本地函数指针并加入头文件; 第二步:所更换的本地函数所对应的函数的实现。 第三步:隐藏符号表,在Android.mk文件里面添加一句LOCAL_CFLAGS := -fvisibility=hidden

    实现篇:

    第一步:自定义JNI_Onload,来自定义JNI函数的函数名,通过registerNativeMethods()函数来更换本地函数指针并加入头文件;我们在上一个工程的基础上来改:

    [cpp]  view plain  copy void* getStringc(JNIEnv *env, jobject obj, jstring str);   static JNINativeMethod gMethods[] = {           { "getStringFromNative""(Ljava/lang/String;)Ljava/lang/String;", (void*)getStringc},      };   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;   }   static int registerNatives(JNIEnv* env)   {       if (!registerNativeMethods(env, JNIREG_CLASS, gMethods,                                  sizeof(gMethods) / sizeof(gMethods[0])))           return JNI_FALSE;          return JNI_TRUE;   }   void anti_debug(){       ptrace(PTRACE_TRACEME,0,0,0);   }   jint JNI_OnLoad(JavaVM* vm,void* reserved){       //anti_debug();       JNIEnv* env;       if ((*vm)->GetEnv(vm,(void**)(&env), JNI_VERSION_1_6) != JNI_OK)          {           return -1;       }       assert(env != NULL);          if (!registerNatives(env)) {//注册           return -1;       }          return JNI_VERSION_1_6;   }   这里我们要注意一点:

    JNINativemethod中结构体的定义:

    [cpp]  view plain  copy typedef struct {          const char* name;     const char* signature;     void* fnPtr;     } JNINativeMethod;     第一个变量name是Java中函数的名字。 第二个变量signature,用字符串是描述了Java中函数的参数和返回值 第三个变量fnPtr是函数指针,指向native函数。前面都要接 (void *) 第一个变量与第三个变量是对应的,一个是java层方法名,对应着第三个参数的native方法名字:就像在本文中

    第三步:隐藏符号表,在Android.mk文件里面添加一句LOCAL_CFLAGS := -fvisibility=hidden

    第一个和第三个好理解:对于第二个:

    括号里面表示参数的类型,括号后面表示返回值。我们要参照一个表格:

    第二步:所更换的本地函数所对应的函数的实现:

    [cpp]  view plain  copy __attribute__((section (".mytext")))  JNICALL jstring getStringc           (JNIEnv *env, jobject obj, jstring str)   {      // jstring   CharTojstring(JNIEnv* env,   char* str);       //首先将string类型的转化为char类型的字符串       const char *strAry=(*env)->GetStringUTFChars(env,str,0);       if(strAry==NULL){           return NULL;       }       int len=strlen(strAry);       char* last=(char*)malloc((len+1)* sizeof(char));       memset(last,0,len+1);       //char buf[]={'z','h','a','o','b','e','i','b','e','i'};       char* buf ="beibei";       int buf_len=strlen(buf);       int i;       for(i=0;i<len;i++){           last[i]=strAry[i]|buf[i%buf_len];           if(last[i]==0){               last[i]=strAry[i];           }       }       last[len]=0;       return (*env)->NewStringUTF(env, last);   }   这里的关键是,在函数前加上attribute((section (“.mytext”))),这样的话,编译的时候就会把这个函数编译到自定义的名叫”.mytext“的section里面,由于我们在java层没有定义这个函数因此要写到一个自定义的section里面。

    第三步:隐藏符号表,在Android.mk文件里面添加一句LOCAL_CFLAGS := -fvisibility=hidden

    注意的是在android studio中在build.gradle中

    第一:defaultConfig{}中增加ndk设置:

    [cpp]  view plain  copy ndk{             moduleName "egg"             ldLibs "log","z","m"             abiFilters "armeabi","armeabi-v7a","x86"         }   第二:因为要手动ndk-build,需要在android{}中增加jni和jniLibs路径说明:

    [cpp]  view plain  copy sourceSets {           main {               jni.srcDirs = []               jniLibs.srcDirs = ['src/main/libs']           }}   第三:在/src/main/jni中进行ndk-build手动编译生成对应的.so文件。

    程序跑起来跟之前一样,我们用IDA打开对应的.so文件可以看出:

    总结篇:

    优点:

    1.源码改动少,只需要添加JNI_Onload函数;

    2.无需加解密so,就可以实现混淆so中的JNI函数(使得IDA分析紊乱);

    3.可以加上前面说到的基于源码的函数的加解密,从而增加破解者的难度;

    步骤:

    第一步:自定义JNI_Onload,来自定义JNI函数的函数名,并加入头文件; 第二步:Java层函数所对应的函数的实现。 第三步:隐藏符号表,在Android.mk文件里面添加一句LOCAL_CFLAGS := -fvisibility=hidden

    原理本质:

    当java层调用System.loadLibrary函数时,函数会找到对应的so库,然后试着去找“JNI_Onload函数”;JNI_OnLoad可以和JNIEnv的registerNatives函数结合起来,实现动态的函数替换,再加上getStringc函数符号表的隐藏,就可以起到保护的作用。

    附件:源码下载处:点击打开链接

    转自:http://blog.csdn.net/feibabeibei_beibei/article/details/52668534
    转载请注明原文地址: https://ju.6miu.com/read-5748.html

    最新回复(0)