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

来源:互联网 发布:旺草胡蜂醋官方淘宝 编辑:程序博客网 时间:2024/06/07 10:56

        继上次基于源码级别和二进制级别的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
  1. void* getStringc(JNIEnv *env, jobject obj, jstring str);  
  2. static JNINativeMethod gMethods[] = {  
  3.         { "getStringFromNative""(Ljava/lang/String;)Ljava/lang/String;", (void*)getStringc},  
  4.   
  5. };  
  6. static int registerNativeMethods(JNIEnv* env, const char* className,  
  7.                                  JNINativeMethod* gMethods, int numMethods)  
  8. {  
  9.     jclass clazz;  
  10.     clazz = (*env)->FindClass(env, className);  
  11.     if (clazz == NULL) {  
  12.         return JNI_FALSE;  
  13.     }  
  14.     if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {  
  15.         return JNI_FALSE;  
  16.     }  
  17.   
  18.     return JNI_TRUE;  
  19. }  
  20. static int registerNatives(JNIEnv* env)  
  21. {  
  22.     if (!registerNativeMethods(env, JNIREG_CLASS, gMethods,  
  23.                                sizeof(gMethods) / sizeof(gMethods[0])))  
  24.         return JNI_FALSE;  
  25.   
  26.     return JNI_TRUE;  
  27. }  
  28. void anti_debug(){  
  29.     ptrace(PTRACE_TRACEME,0,0,0);  
  30. }  
  31. jint JNI_OnLoad(JavaVM* vm,void* reserved){  
  32.     //anti_debug();  
  33.     JNIEnv* env;  
  34.     if ((*vm)->GetEnv(vm,(void**)(&env), JNI_VERSION_1_6) != JNI_OK)  
  35.   
  36.     {  
  37.         return -1;  
  38.     }  
  39.     assert(env != NULL);  
  40.   
  41.     if (!registerNatives(env)) {//注册  
  42.         return -1;  
  43.     }  
  44.   
  45.     return JNI_VERSION_1_6;  
  46. }  
这里我们要注意一点:

JNINativemethod中结构体的定义:

[cpp] view plain copy
  1. typedef struct {    
  2.     
  3. const char* name;    
  4. const char* signature;    
  5. void* fnPtr;    
  6. } JNINativeMethod;    
第一个变量name是Java中函数的名字。

第二个变量signature,用字符串是描述了Java中函数的参数和返回值

第三个变量fnPtr是函数指针,指向native函数。前面都要接 (void *)

第一个变量与第三个变量是对应的,一个是java层方法名,对应着第三个参数的native方法名字:就像在本文中

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

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

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


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

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

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

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

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

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

[cpp] view plain copy
  1. sourceSets {  
  2.         main {  
  3.             jni.srcDirs = []  
  4.             jniLibs.srcDirs = ['src/main/libs']  
  5.         }}  

第三:在/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/52729136

 


0 0
原创粉丝点击