学徒浅析Android开发——SO文件的混淆

.so(SharedObject)作用等同于windows环境下的.dll(dynamic link library)文件,我们在引用第三方SDK时,也会遇到需要调用相应的.so文件的情况,.so文件本事更多的是集成公共处理方法,当然有事也会用来保存重要的数据信息。

对于应用的编译与反编译过程中,本地混淆一直是有效的方法。对于.so文件,同样也适用于混淆,.so文件虽然在使用**工具IDA打开后看到的是汇编数据,但仍然存在着规律可循,不妨碍专业人员的阅读,所以掌握对.so文件的混淆也是必须的。

.so文件的混淆是从两方面入手:

1:方法名混淆

2:数据混淆

方法名混淆

我们在创建构建.so文件所需的.c和.h文件时,NDK会帮助我们自动生成native方法名,这个方法名是格式化,该方法名同时也是索引表中的索引名。因其特殊性,在IDA浏览时,可以通过索引表快速查看到。比如我们创建一个TestSimple,在里面声明一个native方法showDsc(),最终得到的方法名是:
      Java_com_example_xx_TestSimple_showDsc

生成的.so文件在经过IDA反编译后,可以在函数索引表中清楚的看到:

学徒浅析Android开发——SO文件的混淆


好在NDK提供了修改的方法,帮助我们隐藏掉这种显眼的名字。首先说一下两个基础方法:
1、简化方法名,该方法如下:

   继续以TestSimple为例,声明

#defineJNIFUNCTION_NATIVE(sig)  Java_com_example_xx_TestSimple_##sig

   将函数名前边的路径剔除掉,只显示函数名,避免显得直接。

  

[cpp] view plain copy
  1. JNIEXPORT void JNICALL JNIFUCTION_NATIVE (showMath(JNIEnv *env, jobject obj));  

   该方法只能用来简化方法名,在编辑时有用,但是在**中,对隐藏没有作用,修改后,在IDA中依然可以查看到:

学徒浅析Android开发——SO文件的混淆

 

2、替换section索引名,该方法如下:

[cpp] view plain copy
  1. __attribute__((section(".XXXX")))  

其中XXXX表示自定义的索引名,需要在指定的函数名前 声明该方法, 例如:

[cpp] view plain copy
  1. __attribute__((section (".xxxsimple")))  JNICALL jstring show(JNIEnv *env, jclass obj){  
  2.          return realShow(env,obj);  
  3. }  


实际效果图如下:

 学徒浅析Android开发——SO文件的混淆

该方法通过改变属性类型,影响**后相关代码段解析顺序,

 学徒浅析Android开发——SO文件的混淆

NDK的方法名混淆就是将1和2 两种方法通过配合RegisterNatives()注册的方式混合使用,RegisterNatives()可以实现与类关联的本地方法,这步操作必须在调用之前完成,也就是库加载过程中。一般是用JNI_Onload这个函数来完成。通过RegisterNatives()进行关联,将实际执行函数由声明的类函数转变成本地函数,达到隐藏的效果,调用步骤如下:

 

步骤一:

声明待替换的方法集合JNINativeMethod,是一个方法类型集合,

[plain] view plain copy
  1. //指针操作,将java层的showDsc函数指向到Native层的show()函数上  
  2. static JNINativeMethodgetMethods[] = {  
  3.         {"showDsc","()Ljava/lang/String;",(void*)show},  
  4. };  

其中JNINativeMethod的结构定义如下:

[plain] view plain copy
  1. typedef struct{  
  2.    char *name;// 待指向函数名  
  3.    char *signature;// 待指向函数和形参类型  
  4.    void *fnPtr//函数指针,实际执行函数  
  5. }  

 

声明待注册的类名

[cpp] view plain copy
  1. #define JNIREG_CLASS"com/example/xx/Test"  

步骤二:

在实际执行函数前标记自定义属性名称

[plain] view plain copy
  1. __attribute__((section(".xxxfirst"))) JNICALL jstring show(JNIEnv *env, jclass obj){  
  2.         return realShow(env,obj);  
  3. }  

步骤三:

在实际执行函数完成操作。

[plain] view plain copy
  1. jstringrealShow(JNIEnv *env, jclass obj){  
  2.      return (*env)-> NewStringUTF(env,“10086”);  
  3. }  

步骤四:

执行RegisterNatives(),注册与指定类相关联的方法

[plain] view plain copy
  1. static int registerNativeMethods(JNIEnv* env, const char* className,JNINativeMethod* gMethods, int numMethods)  
  2. {  
  3.     jclass clazz;  
  4.     clazz = (*env)->FindClass(env, className);  
  5.     if (clazz == NULL) {  
  6.         return JNI_FALSE;  
  7.     }  
  8.     if ((*env)->RegisterNatives(env, clazz, getMethods, numMethods) < 0) {  
  9.         return JNI_FALSE;  
  10.     }  
  11.     return JNI_TRUE;  
  12. }  

步骤五:对指定的类进行指定方法名注册,注册成功则返回0,失败为1。

[plain] view plain copy
  1. static intregisterNatives(JNIEnv* env)  
  2. {  
  3. if(!registerNativeMethods(env,JNIREG_CLASS,  
  4. getMethods, sizeof(getMethods) /sizeof(getMethods[0])))  
  5. {  
  6.            return JNI_FALSE;  
  7. }  
  8.     return JNI_TRUE;  
  9. }  



 

步骤六:

在JNI_OnLoad中执行注册操作

[plain] view plain copy
  1. JNIEXPORT jintJNI_OnLoad(JavaVM* vm, void* reserved)  
  2. {  
  3.     JNIEnv* env = NULL;  
  4. jint result = -1;  
  5.     if ((*vm)->GetEnv(vm,(void**) &env, JNI_VERSION_1_4) != JNI_OK) {//GetEnv()用以获得一个JNIEnv全局对象  
  6.         return -1;  
  7.     }  
  8.     assert(env != NULL);  
  9.     if (!registerNatives(env)) {  
  10.         return -1;  
  11.     }  
  12.     result = JNI_VERSION_1_4;  
  13.     return result;  
  14. }  



构建成功之后,我们可以看到,在方法名索引表中找不到调用的函数名称。只有找到自定的section区域才能进行下一步解析。

学徒浅析Android开发——SO文件的混淆学徒浅析Android开发——SO文件的混淆

 

数据混淆

通常情况下,在.so库中放置的是方法集合,但是如果存在文本内容时,就需要考虑数据混淆了,一旦使用声明常量的方式设置文本数据,这些数据在使用过程中会被基本类型完整保存,这些数据在通过**后,会直白的出现在**者面前。这种设置让汇编程序在进行地址指引时,指向的是一个数据源,而非指针地址时。如果我们对外提供文本数据时,对数据采取数组拼接的方式,指向数组获得的是一个指针地址,这样**者看到的是地址标记而非直白的数据源。下面是两组对比。

直接以文本形式输出:

[plain] view plain copy
  1. JNIEXPORT jstring JNICALLJava_com_example_xx_Test_showC  
  2. (JNIEnv *env, jobject obj){  
  3.       charsha[100];  
  4.               nstrcpy(sha,H, A, C, E, J, F, NULL);  
  5.       return(*env)-> NewStringUTF(env, sha);  
  6. }  



学徒浅析Android开发——SO文件的混淆

使用数组组合的形式:

[plain] view plain copy
  1. JNIEXPORT jstring JNICALLJava_com_example_xx_Test_showB  
  2. (JNIEnv *env, jobject obj){  
  3.           char str[15];  
  4.           str[0]= O[1];  
  5.           str[1]= N[4];  
  6.           str[2]= P[0];  
  7.           str[3]= N[2];  
  8.           str[4]= D[0];  
  9.           str[5]= Q[0];  
  10.           str[6]= Q[2];  
  11.           str[7]= O[1];  
  12.           str[8]= D[0];  
  13.           str[9]= P[3];  
  14.           str[10]= '\0';  
  15.       return(*env)-> NewStringUTF(env, str);  
  16. }  
学徒浅析Android开发——SO文件的混淆

 

最后,说一下,其实没有**不了的保护,只有**不了的人心,不管安全软件机构如何宣传,碰到有心人士也是无解,我们做开发的,只是在增加**难度而已。