第一种方式:混淆策略
混淆策略是每个应用必须增加的一种防护策略,同时他不仅是为了防护,也是为了减小应用安装包的大小,所以他是每个应用发版之前必须要添加的一项功能,现在混淆策略一般有两种:
对代码的混淆
我们在反编译apk之后,看到的代码类名,方法名,已经代码格式看起来不像正常的Android项目代码,那么这时候就会增加阅读难度,增加破解难度,像这样的代码混淆:
我们一般现在的破解查看Java层代码就是两种方式:
一种是直接先解压classes.dex文件出来,使用dex2jar工具转化成jar文件,然后再用jd-gui工具进行查看类结构
一种是使用apktool工具直接反编译apk,得到smali源码,阅读smali源码
不过这种代码混淆有时候在一定程度上能够增加混淆策略,但是有时候也不是很安全,因为我们知道我们在破解的过程中一般是找程序的入口,那么这些入口一般都是Application或者是MainActivity之类的,但是这些Android中的组件类是不能进行混淆的,所以我们还是有入口可寻,能够找到入口代码,然后进行跟踪。
2、对工程资源的混淆
我们上面说到了对代码的混淆能够增加一定的代码阅读难度,有时候我们为了防止资源的保护也是可以做混淆的,这个资源混淆原理这里就不多解释了,微信团队已经将这个功能开源,不了解的同学可以转战github查看:
https://github.com/shwenzhang/AndResGuard
主要是对资源的混淆
第二种方式:应用的签名
我们知道Android中的每个应用都是有一个唯一的签名。但是这个签名在之前是可以被伪造,并实现为此打包的。为了防止应用被二次打包,或者是需要破解我们的apk的操作,在入口处添加签名验证,如果发现应用的签名不正确就立即退出程序,我们可以在应用启动的时候获取应用的签名值,然后和正规的签名值作比对,如果不符合就直接退成程序即可。
public static String getSign(){ Context context= StockApplication.getInstance(); try { PackageInfo packageInfo=context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); Signature[] signatures=packageInfo.signatures; StringBuilder builder=new StringBuilder(); for (Signature signature:signatures){ builder.append(signature.toCharsString()); } return builder.toString(); } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return ""; }
我们可以在application启动的时候,如果不是我们app的签名,那么app直接退出
private void checkSign() { if (!Utils.isMyApp()){ //直接退出 } }
public static boolean isMyApp(){ String signStr=getSign(); return SIGN.equals(signStr); }
第三种方式:修改Naitve函数名
这个方法其实不太常用,因为他的安全措施不是很强大的,但是也是可以起到一定的障眼法策略,在说这个知识点的时候,我们先来了解一下so加载的流程:
在Android中,当程序在java层运行System.loadLibrary("jnitest");这行代码后,程序会去载入libjnitest.so文件,与此同时,产生一个"Load"事件,这个事件触发后,程序默认会在载入的.so文件的函数列表中查找JNI_OnLoad函数并执行,与"Load"事件相对,当载入的.so文件被卸载时,“Unload”事件被触发,此时,程序默认会去在载入的.so文件的函数列表中查找JNI_OnUnload函数并执行,然后卸载.so文件。需要注意的是,JNI_OnLoad与JNI_OnUnload这两个函数在.so组件中并不是强制要求的,用户也可以不去实现,java代码一样可以调用到C组件中的函数,之所以在C组件中去实现这两个函数(特别是JNI_OnLoad函数),往往是做一个初始化工作或“善后”工作。可以这样认为,将JNI_ONLoad看成是.so组件的初始化函数,当其第一次被装载时被执行(window下的dll文件也可类似的机制,在_DLL_Main()函数中,通过一个swith
case语句来识别当前是载入还是卸载)。将JNI_OnUnload函数看成是析构函数,当其被卸载时被调用。由此看来,就不难明白为什么很多jni C组件中会实现JNI_OnLoad这个函数了。 一般情况下,在C组件中的JNI_OnLoad函数用来实现给VM注册接口,以方便VM可以快速的找到Java代码需要调用的C函数。(此外,JNI_OnLoad函数还有另外一个功能,那就是告诉VM此C组件使用那一个JNI版本,如果未实现JNI_OnLoad函数,则默认是JNI 1.1版本)。
应用层的Java类别通过VM而调用到native函数。一般是通过VM去寻找*.so里的native函数。如果需要连续呼叫很多次,每次都需要寻找一遍,会多花许多时间。此时,C组件开发者可以将本地函数向VM进行注册,以便能加快后续调用native函数的效率.可以这么想象一下,假设VM内部一个native函数链表,初始时是空的,在未显式注册之前此native函数链表是空的,每次java调用native函数之前会首先在此链表中查找需要查找需要调用的native函数,如果找到就直接使用,如果未找到,得再通过载入的.so文件中的函数列表中去查找,且每次java调用native函数都是进行这样的流程,因此,效率就自然会下降,为了克服这样现象,我们可以通过在.so文件载入初始化时,即JNI_OnLoad函数中,先行将native函数注册到VM的native函数链表中去,这样一来,后续每次java调用native函数时都会在VM中的native函数链表中找到对应的函数,从而加快速度
第四种方式:反调试异常检测
这种方式其实是为了应对现在很多破解者使用IDA进行动态方式调试so文件,从而获取重要的信息,如果还不知道如何使用IDA进行动态调试so文件的同学可以查看这篇文章:Android中使用IDA进行动态调试so文件 ,看完这篇文章之后,我们可以知道IDA进行so动态调试是基于进程的注入技术,然后使用Linux中的ptrace机制,进行调试目标进程的,那么ptrace机制有一个特点,就是如果一个进程被调试了,在他进程的status文件中有一个字段TracerPid会记录调试者的进程id值,比如:
查看文件:/proc/[myPid]/status
在第六行,有一个TracerPid字段,就是记录了调试者的进程id
那么我们就可以这么做来达到反调试的功效了,就是我们可以轮训的遍历自己进程的status文件,然后读取TracerPid字段值,如果发现他大于0,那么就代表着自己的应用在被人调试,所以就立马退出程序。原理知道了,代码实现也很简单,这里用pthread创建一个线程,然后进行轮训操作:
使用pthread_create创建一个线程,线程启动之后执行thread_function函数
看看thread_funcation函数:
开始轮训,读取TracerPid字段的值,发现大于0,就立马退出程序,我们运行结果看看: