apk加固是什么意思(apk加固了解)

1

为什么要加壳

对android的项目来说来说,在编译时所有用到的java文件都会编译成class文件后装载组成dex文件最后打包进入生成的apk,因此本质上来说一个普通apk就是一个压缩包,而通过apktool工具可以完成对apk的解压反编译,反编译后dex文件将生成一个smali目录。其中包含所有原class被反编译生成的smali文件,而对这些文件进行相应修改然后重新打包就可以完成对原apk修改代码。而对smali语法不熟悉也可以用dex2jar工具对dex文件进行反编译,然后用JD-GUI查看生成的jar文件也能看到dex中的class文件,这样也能清楚的看出apk对应项目的目录结构以及实现。从上可以看出,对apk不进行一定的加固,很容易会导致源码泄露,使得其他人会很容易对其进行修改或模仿。

那么我们先大致了解一下apk中包含了哪些信息。在windows下可以直接将.apk后缀修改为.rar后直接打开,在Mac下也可以通过解压工具打开apk文件。一个典型的apk文件内部结构如下图所示:

apk加固是什么意思(apk加固了解)

其中assets目录下存放了项目中res/assets下的文件以及引用到的第三方库项目中的assets下的文件(没有assets的不会有这个路径),lib中储存的是项目用到的.so库(没引用.so库的不会有这个路径),META-INF下的文件从Java jar文件引入的描述包信息的目录,主要用于签名验证,而res目录下包含了项目中的资源——即项目以及引用库中的drawble、layout、anim、menu以及color的xml和png文件,AndroidManifest文件为原项目的Manifest经过特殊处理后生成的xml文件,resources.arsc是一张资源文件索引表,记录了R文件中对应的资源id,而classes.dex为apk执行时的可执行文件,里面包含apk运行时用到的class。以上就是apk包含的所有信息。

2

常见apk加固方案

通过之前对为加固的apk简单分析可以看出要保证apk的安全可以从以下几个考虑:

1. 通过特殊处理使得apk无法被正确解压

2. 通过签名对比

3. 对dex文件进行特殊处理使得解压后不可见或

见到的不是正确的dex文件

因而基于以上三种思路产生了不同的加固方案:

方案1:

伪加密。其修改原理是修改连续4位字节标记为”P K 01 02”的后第5位字节,奇数表示不加密偶数表示加密,这样加密出来的apk无法被反编译工具apktool正常解压。但Android4.2.x系统由于修改了签名验证的方式无法安装伪加密的APK。因此现在流行的加固方案大多基于思路2或3。

方案2:

通过与服务器对比apk签名来验证真伪。

方案3:

代码混淆。在proguard文件中定义好相应混淆规则后编译打包,这样的apk被反编译出来的代码变量会根据混淆规则变得比较难以识别(例如之前正常的变量名变成a、b、c之类无特殊意义的名称),这样会增加重写的难度。这也是当前最常见的做法。

花指令。花指令是程序中有一些指令,由设计者特别构思,希望使反汇编的时候出错,让破解者无法清楚正确地反汇编程序的内容,迷失方向。比如以下的代码:

最后一句的变量虽然是冗余的,但他可以使得apk被反编译时报错,从而保护到核心的dex文件。

so保护。将关键代码使用jni实现。由于生成的.so库相较java代码更难以被反编译,从而增加破解难度,也保护了dex文件。

而我们采用的落地加壳的方案也是基于对dex的保护来实现,下面将具体介绍。

3

落地加壳方案

与普通文件一样,dex文件可以在其数据部分的头或尾部拼接数据。但其特殊性在于dex文件可通过一些特殊的控制让反编译时只能读到其部分数据,因此,我们可以做这样一种设想:在原来dex尾部拼接实际用到的apk,然后修改某些参数使得反编译时只能获取到非拼接部分的dex数据,在正常运行时我们在通过特殊处理将拼接部分的数据获取并执行。这就是本方案的大体思路,下面我们将逐步介绍具体的操作。

首先我们熟悉一下dex文件的结构:

其中标红的三个字端比较重要:checksum校验码,采用adler32算法,检查从这个字段开始到文件结尾,这段数据是否完整,有没有人修改过,或者传送过程中是否有出错等等;signature签名,使用 SHA-1 算法 hash 除去 magic ,checksum 和 signature 外余下的所有文件区域 ,用于唯一识别本文件 ;file_size记录dex文件的大小。

而当我们在数据区结尾添加一个新的数据块后只变更文件头中这三个字段,被反编译后出现的文件只含有原来数据区对象而没有新加入的数据块,据此,我们就可以用到之前说过的方案:对于要被加密的原apk,我们生成一个新的壳apk,将原apk作为数据块拼接到壳apk的dex数据区结尾,并将壳apk作为原apk发布。这样保证了他人获取到apk后反编译获取的并非真实原apk数据而是壳apk的数据。运行时再通过动态加载的方法将原app以类似插件的形式运行起来。这样保证了对apk的加密和app的正常运行。

总结一下加密过程大致如下:制作壳dex->用壳dex代替壳apk中的classes.dex->重新签名生成新apk。而制作壳的过程就是将原apk拼接到壳dex结尾然后对checksum、signature和file_size三个字段变更。首先修改file_size:

接着修改signature:

最后修改checksum:

至此,壳dex的制作完成,然后用其替换壳apk中的dex,再对该apk重新签名即可。而后,启动壳app时在attachBaseContext时解析出原apk并存储在指定位置(即落地过程),而后对其进行动态加载即可。下面将结合app详细介绍如何动态加载出原app。

4

动态加载

动态加载的目的,是在已安装的app中调用未安装的app。其中涉及到关键问题就是如何启动未安装的app的四大组件以及调用未安装的app中的资源。

而为安装apk中所有的类都在其apk的dex中有对应的类,因此无可避免要使用dex的加载来完成。而android系统中的类加载器有PathClassLoader以及DexClassLoader,而PathClassLoader只能作用于已安装的软件,因此我们选择DexClassLoader作为我们的dex的类加载器。之后我们来看看怎样逐步完成动态加载

4.1 从dex解压出需要加载的apk

当前apk中获取dex文件内容,其实也就是从一个压缩包中获取名为classes.dex的文件的过程,可以用专门的zip流来处理:

根据之前制作壳dex过程可知获取到的dex的数据区结尾然即是需要被加载的apk,所以再从这个dex中加载出apk,并存储到指定位置(即为落地过程):

4.2 application替换

对于android应用来说启动launch activity之前会先启动application,因此我们要动态启动应用先要启动应用的application,在宿主application的onCreate过程中完成application的替换就势在必行。而这个过程分为以下几步:

1.获取application信息

通过xmlParser解析原apk中AndroidMenifest.xml

文件,依次读取其中不同tag下的关键信息,存到自定义的数据结构中。需要存储的信息包括ApplicationInfo以及四大组件和权限声明的信息。

2. 拼接ClassLoader

之前我们讲过用DexClassLoader可以完成对未安装的apk的加载,但本项目中的动态加载的特殊性在于宿主应用本身还有activity需要在宿主本身的dex来加载,因此不能直接替换掉宿主的ClassLoader。因此我们采用拼接的方式,将要用到的类加载器与原类加载器拼接到一起形成一个新的类加载器作为该应用的类加载来使用。拼接成的加载器如下:

mBase即为宿主的加载器,而新的加载器通过add不断添加到list中。在读取要加载的应用的application过程中用DexClassLoader来加载该apk:

然后后通过反射设置LoadedApk的mClassLoader为拼接好的ClassLoader:

3.移除原初始application

android的入口为ActivityThread的main函数,也就是说在ActivityThread中会初始化app运行的关键信息,包括主application和application列表(一个app可以存在多个application),而查看源码会发现在ActivityThread在除了用一个mInitialApplication来记录app运行时启动的application,还使用了一个AppBindData(是ActivityThread的内部类,并且ActivityThread记录了一个名为mBoundApplication的这样的类),AppBindData内还有一个名为info的LoadedApk对象,而每个LoadedApk都会保有一个名为mApplication的application对象来记录该apk对应应该运行的application,因此在application替换时首先要找到mBoundApplication中info对象的mApplication并将其置为空:

4.生成新的application信息

通过反射调用LoadedApk中的makeApplication方法可以生成新的application,而且源码会将这个application会自动加入到currentActivityThread 的mAllApplications中。需要注意的是makeApplication时会要求传入一个Instrumentation作为参数,如果传null则默认使用系统的Instrumentation,之后这个Instrumentation将作为整个应用的Instrumentation存在。而由于我们之后启动activity等操作中还涉及到在Instrumentation中挂钩资源,因此我们此处需要传入自定义的Instrumentation:

5. 替换原application

反射设置currentActivityThread中的mInitialApplicatio为上一步生成的application:

到此即整个application的替换完成,之后需要做的就是对具体组件进行挂钩

4.3 activity挂钩

1. 替换Instrumentation

对于activity来说,其启动过程是绕不开Instrumentation的。在调用Activity.startActivity后通过一些列调用后会调用到Instrumentation中的execStartActivity以及newActivity还有callActivityOnCreate等。我们可以以此为切入,为app的baseContext注入我们自定义的Instrumentation,然后在自定义Instrumentation中完成资源以及环境的挂钩。首先在app的Application.onCreate中,我们要完成将自己的Instrumentation与整个app挂钩,这一步可以在反射调用makeApplication时完成(这一步可以查看替换application中的步骤4)

2. 重构execStartActivity方法

在execStartActivity,将其跳转的目标跳到一个代理activity中(这个代理activity不需要有实体,只需要在壳的AndroidManifest中声明过即可):

3. 环境设置

自定义一个ContextWrapper,其中的资源即为未安装的app的资源:

4. 资源以及环境绑定

在复写的callActivityOnCreate中根据类名找到对应的activity然后完成与环境以及资源的绑定:

4.4 绑定ContentProvider

在解析applicationInfo时反射调用currentActivityThread中installContentProviders方法即可完成ContentProvider的绑定:

5

需要注意的问题

1.在初始化applicationInfo时需要通过反射将primaryCpuAbi设置为Build.CPU_ABI。

如果不设置默认为null会导致cookieManager.getInstance时往下调用到VMRuntime.is64BitAbi(ApplicationInfo.primaryCpuAbi)时出错抛出Unsupported abi:null的错误。

2. 原app中有shortcutinstall的话需要hook ActivityManager,然后识别broadcastIntent方法中的intent,如果为shortcutinstall,将跳转的class设置为壳app的launchActivity。hook ActivityManager需要在attachBaseContext时完成:

以上就是落地加壳方案的简要介绍。

发表评论

登录后才能评论