蕉下客--)

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

浅谈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.BradcastSeverices 等组件的, 并且在这个类中找到一个静态函数。获取当前activityActivityThread 对象

 

 

总结:

* 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. 如果员APKlibSO文件, 需要释放到指定目录下, 代码如下:

 

 

加固版本一原理:

1.由第一个版本,我们知道DexClassLoader可以实现, 于是笔者根据去看DexClassLoader的实现,

 

再进基类BaseDexClassLoader的构造函数

 

在进DexPathList 的构造函数

 

 

在进makeDexElements函数

 

在进makeDexElements函数

 

代码行为分析

 a) 具体加载DexFile 还是使用loadDex 文件进行的, 并且loadDex是个静态函数,猜想DexFile 可能除

在进loadDex函数

 

在进DexFile构造函数

 

发现我们使用了openDexFile , 这是我们最

 

进入Native层看openDexFile 的实现代码

 

 

发现调用核心函数addToDexFileTable, 再进

 

 

行为分析结果:

 

 

2.我们在从使用代码, ClassLoaderloadClass分析函数

 

再进

 

再进

 

再进:

 

再进Native 的实现层:

 

 

原理总结:

1. DexClassLoader 构造函数就通过遍历.APK, .JAR包所有的dex,class 文件,依次通过DexFileopenDexFile, 把DexFiledex文件中添加到一张表中(Hash表)

2. 然后通过 DexClassLoaderloadClass 函数去加载类

 

 

版本一评价:

致命缺陷: 直接暴露文件路径,在新建DexClassLoader类的时候,我们发现需要指定解压好的APK地址。

版本二:

我们知道加固版本一的缺陷在于需要指APK文件路径。 为了更隐蔽写我们有两种改进方法:

1.重写一个ClassLoader, 这个ClassLoader 不须指定APK的路径, 这样我们就不需要释放APK文件了。(代码量比较大)

2.不是ClassLoader替换的方法,而是在原来的ClassLoader 上直接添加一个类。 定义一个类不需要指定.dex 文件路径

版本2的方法就是是使用第二种改进方案. 这个版本仅限在android4.0 - android5.0之间

 

原理: 我们在分析版本一的原理,发现版本利用DexFile.openDexFile() 实现的, 如果能在这个类找到一个相似的函数。 于是代开在 android源码的\libcore\dalvik\src\main\java\dalvik\system 下的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本身具有代码加密功能, 并且在运行前,解密后运行, 这种技术叫做:运行时自修改字节码技术(RSMCRun Self Modify Code) ,利用这种技术就可以把我们核心的代码使用DEX的运行。

 

问题: 运行时自修改字节码技术(RSMCRun Self Modify Code),一个重要的技术难点就是如何在DEX 字节码运行的时候,找到函数的实现地址。

现在 笔者通过分析java.lang.reflect.Method类的invoke 函数字节码存储地址。这是invoke必定会找到函数的自己吗,并且解释执行这个字节码。

1.android源码找到java.lang.reflect.Method类的invoke的代码

 

再进invokeNative 函数查看

 

 

再进invokeNativenative 实现层查看代码

 

再进dvmInvokeMethod 查看: 代码:(dalvik\vm\interp\Stack.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 强制是ARTAndorid Runtime)模式。 ART模式下,APK安装时,就会把字节码编译成汇编码,这个方法就不能使用了, 由于没有字节码

运行时就找不到对象的字节码,这样加固后的android APP 是运行不了了。  所以加固版本三模式不适合单独使用

 

2.特征代码,调用mprotected修改页属性, 利用这点做对抗,

版本四:

版本四就是解决版本三在android5.0 不能使用问。我们可以使用版本3 和版本1或版本2 结合使用

 

 

原理:

1.版本3的运行时解密, 静态反编译找不到代码/或者代码是错误的。

2.版本3的的DEX文件/JAR 文件 使用版本1 或者版本2的方式动态运行。

 

关于ART Dalivk 的简介
DalvikGoogle公司自己设计用于Android平台的Java虚拟机。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。


ART Android操作系统已经成熟,GoogleAndroid团队开始将注意力转向一些底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快执行效率更高更省电的替代ART运行时。 ART代表Android Runtime,其处理应用程序执行的方式完全不同于DalvikDalvik是依靠一个Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

 

ART优点:
1、系统性能的显著提升。
2、应用启动更快、运行更快、体验更流畅、触感反馈更及时。
3、更长的电池续航能力。

4、支持更低的硬件。

ART缺点:
1、更大的存储空间占用,可能会增加10%-20%
2、更长的应用安装时间。

总的来说ART的功效就是空间换时间

 

posted on 2017-03-12 00:56  蕉下客--)  阅读(7792)  评论(0编辑  收藏  举报