浅谈android代码保护技术_加固
导语
我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk,结果被人反编译了,那心情真心不舒服。虽然我们混淆,做到native层,但是这都是治标不治本。反编译的技术在更新,那么保护Apk的技术就不能停止。现在网上有很多Apk加固的第三方平台,最有名的应当属于:爱加密和梆梆加固了。其实加固有些人认为很高深的技术,其实不然,说的简单点就是对源Apk进行加密,然后在套上一层壳即可,当然这里还有一些细节需要处理,这就是本文需要介绍的内容了。
类装载器ClassLoader
Java中.class 文件类似于我们的windows 的DLL或者Linux下的.SO 文件,在Winodows 下我们使用DLL的服务,我们必须使用使用LoadLibaray函数加载它。同理在java中我们要使用.class
中的的类,我们必须使用类加载器加载后,才能使用。在android中我们如果提供一个DexClassLoader(继承 ClassLoader), 这个DexClassLoader 可以用来加载.jar, .apk, .class 文件。可
以在android源码中找到这个类的实现:
关于的DexClassLoader使用,DexClassLoader 只有一个构造函数,如下。 这里我们特别关注的是ClassLoader parent. 关于ClassLoader 有一个特性:子ClassLoader加载过的类
可以访问/使用 父ClassLoader加载过的类, 而父ClassLoader加载过的类不可以访问父 ClassLoader。对于这个值我们一般获取当前的加载器器,当前的加载器。
类装载器的替换
如何替换原来的ClassLoader, 关于为什么要替换原始ClassLoader,笔者在一个简单加固案例会阐述的。
1.在Android源码 搜索关于ClassLoader 的引用, 由于Android源码中ClassLoade应用特别多,搜索应用仅限Java 源码
2.在众多引用有一个类LoadedApk, 于是查看觉得 这就是我们想要的类
从类作用描述可知这个类,是用来维护已加载的APK的信息, 并且里面有两个类加载器,mBaseClassLoader, 和mClassLoader. 显然我们我想要的是mClassLoader , 要想获得mClassLoader, 我们必须先获取LoadedApk 对象
3.在Android源码 搜索关于LoadedApk的引用, 由于Android源码中LoadedApk应用特别多,搜索应用仅限Java 源码; 在众多引用有一个类ActivityThread, 从类作用描述看,是我们要找的类
从类的描述可知,这个类是用于来调度Activity.Bradcast,Severices 等组件的, 并且在这个类中找到一个静态函数。获取当前activity的ActivityThread 对象
总结:
* 1. 反射 调用这个函数 ActivityThread.currentActivityThread 拿到一个当前ActivityThread对象
* 2. 反射获取 ActivityThread对象的 final HashMap<String, WeakReference<LoadedApk>> mPackages 成员
* 3. 从mPackages获取已在加载的 LoadedApk 对象
* 4. 反射获取LoadedApk 对象 的 LoadedApk.mClassLoader成员
* 5. 用新的ClassLoaded 替换原来的mClassLoader
代码如下:
Android加固简史
版本一:
加固方案
1.源APK文件存放壳APK的资源目录asset下
2.使用DexClassLoader 动态加载APK,并运行
1.重写Application, 默认情况一个android 应用程序启动一个默认的Application 对象, 由于我们加固工具需要替换加固的的程序,我们必须在加固程序的Activity前动态加载我们APK。
我们可以选择重写Application 的虚函数 attachBaseContext 或者onCreate. 笔者选择在attachBaseContext 加载我们的原APK,并启动它
2.释放资源中源APK 到目标目录下(init函数)
3. 我们启动一个动态一个android应用程序程序,通常我们需要在某个组件使用意图Intent 启动, 我们学习开发的时候,我们知道Intent 是两个组件通信的的关键,如果我们要启动android程序,我们必须有一个组件,所以启动代码在加固的工具的Activity。 代码如下:
分析以上代码 Class clsDestActivity = dexClsLoader.loadClass("cr.dest.DestActivity"); 这句代码返回值为null的,为什么会这样, 这是因为我们的当前ClassLoader 并不能加载源APK中类。
所以我们必须替换ClassLoader.
4. 新建一个DexClassLoader , 并且替换员ClassLoader. 关于替换ClassLoader 请参考上.
5. 资源如何替换
方案一:直接替换(手工替换)
方案二:代码替换
6. 如果员APK中lib有SO文件, 需要释放到指定目录下, 代码如下:
加固版本一原理:
1.由第一个版本,我们知道DexClassLoader可以实现, 于是笔者根据去看DexClassLoader的实现,
再进基类BaseDexClassLoader的构造函数
在进DexPathList 的构造函数
在进makeDexElements函数
在进makeDexElements函数
代码行为分析
a) 具体加载DexFile 还是使用loadDex 文件进行的, 并且loadDex是个静态函数,猜想DexFile 可能除
在进loadDex函数
在进DexFile构造函数
发现我们使用了openDexFile , 这是我们最
进入Native层看openDexFile 的实现代码
发现调用核心函数addToDexFileTable, 再进
行为分析结果:
2.我们在从使用代码, ClassLoader的loadClass分析函数
再进
再进
再进:
再进Native 的实现层:
原理总结:
1. DexClassLoader 构造函数就通过遍历.APK, .JAR包所有的dex,class 文件,依次通过DexFile的openDexFile, 把DexFile的dex文件中添加到一张表中(Hash表)
2. 然后通过 DexClassLoader的loadClass 函数去加载类
版本一评价:
致命缺陷: 直接暴露文件路径,在新建DexClassLoader类的时候,我们发现需要指定解压好的APK地址。
版本二:
我们知道加固版本一的缺陷在于需要指APK文件路径。 为了更隐蔽写我们有两种改进方法:
1.重写一个ClassLoader, 这个ClassLoader 不须指定APK的路径, 这样我们就不需要释放APK文件了。(代码量比较大)
2.不是ClassLoader替换的方法,而是在原来的ClassLoader 上直接添加一个类。 定义一个类不需要指定.dex 文件路径
版本2的方法就是是使用第二种改进方案. 这个版本仅限在android4.0 - android5.0之间
原理: 我们在分析版本一的原理,发现版本利用DexFile.openDexFile() 实现的, 如果能在这个类找到一个相似的函数。 于是代开在 android源码的libcoredalviksrcmainjavadalviksystem 下的DexFile.java 文件, 类描述:的DexFile类就是负责把一个把文件中类加载到ClassLoader
于是笔者发现函数 native private static int openDexFile(byte[] fileContents); 加固版本二的核心在openDexFile 的参数, 这个参数不需要指定具体文件,
而是直接文件的字节数组。这就我们隐藏文件的操作。顺藤摸瓜,笔者还发发现这几个函数。
里面有4个重要的静态方法:
native private static int openDexFile(byte[] fileContents);
native private static String[] getClassNameList(int cookie);
native private static Class defineClass(String name, ClassLoader loader, int cookie);
native private static void closeDexFile(int cookie);
于是笔者就可以利用四个函数可以实现加固版的原理:
a) 将源android应用程序的 lasses.dex 存放在加固工具工程asset 目录下, 并把源android应用程序的的资源替换加固工具的资源
b) 将classes.dex 的信息读取到ByeArrayOutputStream 字节数组流中。
d) 调用native private static int openDexFile(byte[] fileContents);得到DexFile 的cookie
e) 调用 native private static String[] getClassNameList(int cookie); 获取DexFile文件中所有的类名
f) 遍历e步骤的获取的类名信息, 调用native private static Class defineClass(String name, ClassLoader loader, int cookie); 想当前的加载器注册类
g) 调用native private static void closeDexFile(int cookie); 关闭DexFile文件
实现代码案例:
然后就可以通过Class.forname 或者ClassLoader.loadClss 得到DexFile 的主Activity类,最后通过意图Inetent启动,调用startActivity
原理探索:
关于DexFile 是具体怎么实现,这就需要分析native 函数的实现, 笔者在此就不再探索,笔者会另辟一篇文章来探究的
native private static int openDexFile(byte[] fileContents);
native private static String[] getClassNameList(int cookie);
native private static Class defineClass(String name, ClassLoader loader, int cookie);
native private static void closeDexFile(int cookie);
版本二评价:
运行版本要求高(android4.0 -android5.0之间版本,不包含android5.0) 由于版本而依赖DexFile类中3个私有静态函数,由于这个四个私有函数并没有公开,
所以并不是所有版本都兼容。具体是否支持请查看对应android源码,笔者发现在android是支持的,但是在android5.0 就不支持了。 如果不支持,笔者建议深入
native private static int openDexFile(byte[] fileContents);
native private static String[] getClassNameList(int cookie);
native private static Class defineClass(String name, ClassLoader loader, int cookie);
native private static void closeDexFile(int cookie);
的实现,自己重写一个。 所以android源码的重要性不言而喻。
版本三:
版本一,版本二,都有个致命缺点,那就是在内存中有Dex 文件,利用这点,我们可以在内存中找到Dex 文件头,然后把文件dump下来,这样这两种加固都失效了。为了防止这一点我们,我们希望能在我们的Dex本身具有代码加密功能, 并且在运行前,解密后运行, 这种技术叫做:运行时自修改字节码技术(RSMC,Run Self Modify Code) ,利用这种技术就可以把我们核心的代码使用DEX的运行。
问题: 运行时自修改字节码技术(RSMC,Run Self Modify Code),一个重要的技术难点就是如何在DEX 字节码运行的时候,找到函数的实现地址。
现在 笔者通过分析java.lang.reflect.Method类的invoke 函数字节码存储地址。这是invoke必定会找到函数的自己吗,并且解释执行这个字节码。
1.android源码找到java.lang.reflect.Method类的invoke的代码
再进invokeNative 函数查看
再进invokeNative的native 实现层查看代码
再进dvmInvokeMethod 查看: 代码:(dalvikvminterpStack.cpp)
//nativeFunc 指的函数地址为字节码解释后的结构代码(JTI)
再进函数 void dvmInterpret(Thread* self, const Method* method, JValue* pResult)
发现当前线程的pc = method->insns 可知insns 是自己地址 , 结果存储在方法的字节码存储Method 结构的insns中
2.找到JNI编程jmethodID 和结构Method的关系, 在Java层我们是无法拿到Method 结构体的,所以我们必须使用JNI编程。在JNI编程我们只能拿到一个函数jmethodID, 所以我们要找到这两个关系。
通过分析函数是如何GetMethodID是如何获取jmethod的。
所以加固版本三的原理:
a) 通过GetMethodID 获取获取jmethodid, 并强转Method*
b) 修改a步返回Method 的insns 所在页开启可写权限
d) 解密insns内容为源DEX 的功能
e) 解密后,又将代码加密回去,防止被Dump下
版本三案例:代码如下:
Java 层代码:
函数Sub:解密前是两数加法, 经过decode 运行时解密为两数加法, encode 后字节码在此称为减法, 修改字节码代码使用JNI本地代码实现。
本地解密代码的实现
代码行为分析:
a)调用GetMethodID获取结构体Method
b)调用mprotected 修改Method 结构中insns所在页属性
c)解密操作-》 加法:0x90 减法:0x91
同理解密代码: 不贴图了
版本三加固评价:
1.在android5.0 以上版本不再适用了, 因为android5.0 强制是ART(Andorid Runtime)模式。 ART模式下,APK安装时,就会把字节码编译成汇编码,这个方法就不能使用了, 由于没有字节码
运行时就找不到对象的字节码,这样加固后的android APP 是运行不了了。 所以加固版本三模式不适合单独使用
2.特征代码,调用mprotected修改页属性, 利用这点做对抗,
版本四:
版本四就是解决版本三在android5.0 不能使用问。我们可以使用版本3 和版本1或版本2 结合使用
原理:
1.版本3的运行时解密, 静态反编译找不到代码/或者代码是错误的。
2.版本3的的DEX文件/JAR 文件 使用版本1 或者版本2的方式动态运行。
关于ART 和Dalivk 的简介
Dalvik是Google公司自己设计用于Android平台的Java虚拟机。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
ART: Android操作系统已经成熟,Google的Android团队开始将注意力转向一些底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快执行效率更高更省电的替代ART运行时。 ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。
ART优点:
1、系统性能的显著提升。
2、应用启动更快、运行更快、体验更流畅、触感反馈更及时。
3、更长的电池续航能力。
4、支持更低的硬件。
ART缺点:
1、更大的存储空间占用,可能会增加10%-20%。
2、更长的应用安装时间。
总的来说ART的功效就是“空间换时间”。