android study 6

Author Avatar
Xzhah 6月 23, 2022
  • 在其它设备中阅读本文章

安卓的加固与脱壳

1. Dex壳

​ Dex壳可以分为三种类型:

​ 1. 整体加壳。将目标Apk里的整个dex文件内容都进行加密/压缩。运行的时候壳dex负责将App的dex解密还原并加载到虚拟机中。加载的方式可以采用文件加载或者内存加载。如果发现找不到组件类的实现,那就可以判断是加了整体壳。

  1. 函数粒度加壳。把dex中的某些方法抽空,使用nop指令填充,壳会在合适的时机把方法内容还原,这有点像SMC。

  2. 指令粒度加壳。比如VMP,用自己实现的解释器来执行加壳后的smali指令。以及dex2C, 可以把java的方法转换为native方法。

1.1 整体壳的工作流程

​ 整体加壳需要完成三个任务。1.把原始的dex文件加密/压缩。2.获得控制权并且访问原始dex的加密后数据,将其解密。3. 动态加载解密后的dex文件,并且让java虚拟机可以识别到dex文件中的四大组件。

任务1

​ 壳将原始的dex文件加密添加到壳dex文件的末尾,并且更新壳dex文件的checksum,signature和file_size反映出这些变化,因为在dex文件的头部信息中会保留这些值来检查dex文件格式的正确性。修改了这三个值以后,壳dex文件的末尾就可以添加一些和dex格式定义中无关的信息,比如原始dex文件的个数,大小以及加密以后原始dex文件的内容。然后将壳dex替换掉原始的dex文件对Apk进行重打包即可。

任务2

​ 壳会把自己的Application类替换掉原来的Application类,因此壳的Application类的onCreate()attachBaseContext()会在App进程启动的时候率先执行拿到控制权。而访问被隐藏加密的dex可以通过getApplicationInfo().sourceDir来获取apk的路径,解压apk得到壳dex文件从而解密出壳dex文件里边附带的原始dex文件。

任务3

​ 壳会使用类加载器动态加载解密后的dex文件,为了让组件的生命周期函数也正常运行,还要对类加载器进行修正。

​ 类加载器可以在运行的时候加载在编译时未知的类,而Android里边的BaseDexClassLoader可以实现在运行的时候加载在编译时未知的dex文件,经过此加载器的加载,ART虚拟机内存中会形成相应的数据结构,对应的dex文件也会由mmap映射到虚拟内存当中,通过此加载器的loadClass(String className, boolean resolve)方法就可以得到类的Class对象,从而可以使用该类。

​ PatchClassLoader可以加载开发者自己编写的类,也就是classpath下的类。他有一个兄弟类叫DexClassLoader。他们都是继承自BaseDexClassLoader。壳会使用DexClassLoader来动态加载解密后的dex文件。PathClassLoader是Android默认使用的类加载器,一个APK的Activity等类是在其中加载。DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更加灵活,是实现热更新、热修复、dex加壳的重点。

​ 对类加载器的修正:加载完原始的dex以后还需要对ClassLoader进行修正,否则加载组件类运行的时候会报ClassNotFoundException,为什么会报这种错误呢? 这就涉及到了组件类的创建过程,比如对于Activity来说,应用程序的Activity对象是在ActivityThread类的performLaunchActivity()方法中通过调用mInstrumentation.newActivity()创建出来的。newActivity是通过ClassLoader先加载Activity类,再通过newInstance()来实例化类对象。
​ 由于组件相关的Activity实际上是由PathClassLoader进行加载的,虽然可以通过反射拿到需要动态加载的Activity,但是却造成ActivityManager在管理上无法找到我们要加载的Activity,因为是先加载AndroidManifest才到onCreate的!
为了解决上面的问题,可以有两种解决方案(核心目的都是使得组件类的加载交由DexClassLoader完成):

​ 1.替换系统的组件类加载器为DexClassLoader,同时设置DexClassLoader的parent为系统组件类加载器。

​ 2. 打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入DexClassLoader即可;

1.2 指令抽取型壳的工作流程

1.2.1 android免root权限hook系统函数,从而修改程序运行时的内存指令

​ 大概流程如下:

1. 每个方法要运行,首先会将类加载到内存中,那么就需要调用安卓系统中的dexFindClass函数,这个函数的返回值是一个DexClassDef结构体信息, 通过里面的成员class_data找下去会有一个code_item成员,存了方法的指令信息。
 2. 把相关的内存区域改为可写,修改指令为自定义指令,覆盖原指令即可。

1.2.2 抽取型壳工作原理

​ 1.首先通过解析dex文件,将其指定方法内容指令置空。然后重新计算checksum和signature信息写回到头部。

​ 2.参考1.2.1篡改内存指令即可。

1.2.3 art中存在的问题

​ art由于存在quick code,所以如果还原指令的时机不对,art就执行quick code去了,还原的指令自然不生效。这种情况下要么禁用dex2oat,要么在dex2oat之前就还原dex文件,但是这样的话只要在dex2oat的流程中进行脱壳就能dump下来完整的dex文件了。

1.3 vmp/java2c

​ 其实就是自定义指令集,然后自己写解释器去解释执行。或者也可以把dex里的指令转成c,c++,然后去执行。

2. 脱壳

​ 脱壳的总体思路就是:在某个时间,内存里是会有完整的解密后的dex文件。脱壳就是把这个解密后的dex给dump下来。

art:DexFile

​ 这是art虚拟机里一个关键的数据结构,它有两个成员变量,分别代表着dex文件加载到内存后的大小以及起始地址。类加载器是会用到这个数据结构的。即使是art的quick code模式(被编译到机器码)也会有完整dex文件存在。

2.1 修改源码脱壳

​ 找到libart.so文件中所有导出函数中带有art::DexFile参数或者返回值的函数,那么这就是一个可以脱壳的点。在这些函数里把dex写入文件即可。

2.2 Frida脱壳

​ 网上看到一篇文章用Frida进行内存搜索,从内存中把已经解密的dex给dump下来。通过在内存里搜索dex文件特征。或者通过DexFile.java类的mCookie变量来获取。对于通过使用BaseDexClassLoader来加载的程序来说,DexFile.java类的mCookie变量在native层的表现其实是一个jlong类型的指针的数组,数组的个数为此ClassLoader加载的dex文件个数 + 1,第一个元素类型为OatFile*,剩余的元素为对应的art::DexFile*,因此可以通过获取mCookie变量来得到art::DexFile*列表,并且通过art::DexFile的begin和size来dump。利用 Java.enumerateClassLoadersSync()来枚举所有类加载器,从中进行判断,把BaseDexClassLoader加载的Dex都dump下来。

References

http://www.juziss.cn/2020/09/22/app%E7%9A%84%E5%8A%A0%E5%A3%B3%E4%B8%8E%E8%84%B1%E5%A3%B3/

https://www.52pojie.cn/forum.php?mod=viewthread&tid=1116812&extra=page%3D1%26filter%3Dtypeid%26typeid%3D343

https://www.anquanke.com/post/id/195869

https://blog.csdn.net/m0_46204016/article/details/104579953