Android 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()函数来更换本地函数指针并加入头文件;我们在上一个工程的基础上来改:
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中结构体的定义:
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
第一个和第三个好理解:对于第二个:
括号里面表示参数的类型,括号后面表示返回值。我们要参照一个表格:
第二步:所更换的本地函数所对应的函数的实现:
__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设置:
ndk{moduleName "egg"ldLibs "log","z","m"abiFilters "armeabi","armeabi-v7a","x86"}
第二:因为要手动ndk-build,需要在android{}中增加jni和jniLibs路径说明:
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函数符号表的隐藏,就可以起到保护的作用。
附件:源码下载处