从前面的笔记我们已经知道Frida如何安装并可以用来进行Native层注入,Frida其实还有一个很强大的功能,就是脱壳。这篇文章就来认识一下Android的壳与记录一下Frida脱壳的基本步骤。
0. Android壳的类型
壳的种类非常多,根据其种类不同,使用的技术也不同,这里稍微简单分个类:
- 一代整体型壳:采用Dex整体加密,动态加载运行的机制;
- 二代函数抽取型壳:粒度更细,将方法单独抽取出来,加密保存,解密执行;
- 三代VMP、Dex2C壳:独立虚拟机解释执行、语义等价语法迁移,强度最高。
先说最难的Dex2C
目前是没有办法还原的,只能跟踪进行分析;VMP
虚拟机解释执行保护的是映射表,只要心思细、功夫深,是可以将映射表还原的;二代壳函数抽取目前是可以从根本上进行还原的,dump出所有的运行时的方法体,填充到dump下来的DEX
中去的,这也是fart
的核心原理;一代壳大部分情况下可以用目前我们推荐的几个内存中搜索和dump出DEX
的Frida工具进行脱壳。
目前主流的一些加固厂商有:娜迦、梆梆、爱加密、通付盾、360加固、百度加固、阿里加固、腾讯加固、顶象加固、盛大加固、瑞星加固、网秦加固、国信灵通加固、apkprotect加固。
某些加固厂商的加固特征:https://blog.csdn.net/c_kongfei/article/details/114297479
1. 加固技术
1.1 第一代壳 DEX加密
1.1.1 一代壳加固原理
一代壳也称落地加载,就是对源APK进行加密,再套上一层壳,在运行时对源APK进行解密并动态加载。
加固过程需要三个对象:
- 待加固APK,也就是源APK
- 脱壳程序APK,负责对源APK进行解密
- 加密工具(加壳程序),负责加密源APK并与脱壳DEX合并成新的DEX
在DEX中可进行的加固方法有:字符串加密、资源加密、对抗反编译、反调试、自定义DexClassLoader。
DEX整体加固的本质是文件加载和内存加载。
1.1.2 加固的主要步骤
拿到源APK和脱壳程序APK,用加密算法对源APK进行加密,再将加密后的APK文件与脱壳APK中的DEX文件进行合并得到一个新的DEX文件,最后替换脱壳程序APK中的DEX文件即可。此时得到一个新的APK文件,那么该APK就是加固后的APK,它的主要工作是在运行时解密源APK,然后动态加载,让其正常运行起来。
合并时主要关注DEX文件的dex header中的checksum、signature、file_size字段。因为我们需要将一个文件写入到DEX中,那么我们肯定需要修改文件校验码(checksum),因为它用于检查文件是否有错误。同样需要修改signature,它唯一识别DEX文件。还有就是需要修改DEX文件的大小(file_size)。
- checksum:文件校验码。使用alder32算法校验文件中除maigc和checksum外剩下的所有文件区域,用于检查文件错误。
- signature:使用SHA-1算法对除magic 、checksum和signature外剩下的的所有文件区域做HASH运算,用于唯一识别DEX文件。
- file_size:DEX文件大小。
不过将源APK写入到DEX文件以后还需要将加密后APK的大小添加在文件末尾,目的就是在脱壳时根据文件大小得到正确的源APK。所以合并以后得到的新DEX文件结构如下图所示:
1.1.3 一代壳脱壳工具dexdump
脱壳方法:一种是从内存中找到DEX;另一种是基于Hook然后判断是否是DEX文件,dump_dex && Frida_fart。
1.1.3.1 环境配置
1.1.3.2 脱壳步骤
如果在Android设备上下载的APK,先将APK使用adb pull
命令将它从Android设备拉出来。
1 | adb pull xxxx.apk |
如果需要安装至Android设备则使用adb install
命令。
1 | adb install xxxx.apk |
用Android查壳工具查看APK程序是否加壳,APK使用了360加固。
载入jadx,核心代码都被藏起来了。
那我们可以先来脱壳。运行frida-server。
1 | bullhead:/data/local/tmp # ./frida-server-12.8.0-android-arm64 |
在jadx中查看它的包名为com.coolapk.market。运行App,使用objection工具连接Android设备中的App。
1 | root@kali# objection -g com.coolapk.market explore |
动态加载dexdump工具,进行脱壳:
1 | com.coolapk.market on (Android: 9) [usb] # plugin load root/.objection/plugins/frida-dexdump/frida_dexdump |
但貌似新版的frida-dexdump不支持作为objection插件了?那就直接利用frida-dexdump进行脱壳:
1 | root@kali# frida-dexdump -U -f com.coolapk.market |
脱壳成功后会在当前目录生成一个以包名命名的目录,里面是一堆DEX文件。一般都是找其中DEX文件最大、并且内容含有“MainActivity”字符串的DEX文件来分析。
1 | grep -ril "MainActivity" * |
将DEX文件载入jadx分析,发现校验码出错,使用010Editor修改其校验码即可。
修改为0x5f3f35fa。
动态加载与jadx反编译后的类名有很大不同,很有可能是做了混淆,但这个DEX文件相比之前未脱壳的APK已经多了很多内容了。
1.2 第二代壳 DEX抽取与so加固
对抗第一代壳常见的脱壳法、Dex Method代码抽取到外部(通常企业版)、Dex动态加载、so加密。
函数抽取:在函数粒度完成代码的保护
1.3 第三代壳 DEX动态解密与so混淆
Dex Method代码动态解密、so代码膨胀混淆、对抗之前出现的所有脱壳法。
VMP和Dex2C:Java函数Native化
1.4 SO加固
SO加固能很好地保护客户代码,避免被静态分析。第三方SO加固都是处于无源码的环境,加固功能都依赖于壳代码的实现。在对抗动态分析时,可配置防调试功能减少被动态调试和dump内存的可能性。在SO加固时,对关键代码添加一些指令混淆和VMP的处理,即使在攻击者绕过反调试的情况下也能最大限度地保护代码。
so加固种类:
- 基于init、init_array以及JNI_Onload函数的加壳
- 基于自定义linker的加壳工具
1.4.1 so加固方法
1.4.1.1 Section加密
Section加密的主要分两步:
- 将需加密的代码放入特定Section,在加密代码被执行前执行解密逻辑。
- 编译后的文件,使用工具对特定Section进行加密,即对Section区间的内容加密。
这种加密方式适用于开发者对自实现的代码进行保护,因为Section信息以及解密代码都需要开发者主动添加和编译,需要源码支持。
1.4.1.2 UPX以及类UPX的so加固
开源的SO加固较为常见的是UPX,很多人会根据UPX源码修改一些特征改成自己的版本,避免直接被脱壳。
UPX加固SO的做法是只加固代码段,数据段和重定位相关的结构都保留在文件中,利用原SO的重定位信息完成SO重定位,通过插入的Init节对代码进行解密,本质上和Section加密类似,只是放大了加密范围,并添加修改Init节的操作。通过命令readelf -d {sopath}
,仍可以看到原SO内的数据和原SO一致,但readelf -S {sopath}
时,Section已被破坏无法查看。
1.4.1.3 自实现Linker方式加固
自实现Linker的方案目前来说是加固的主要手段。因重定位过程完全自己实现,这样既可以加密SO中的代码,也可以加固SO中的数据。由于自实现Linker,SO结构可以完全破坏和自定义,可以防止被dump出完整的SO。加固后SO静态分析只能看到壳的结构,对于原SO的结构完全隐藏。
1.4.1.4 代码混淆
有源码的代码混淆一般可通过插入花指令或通过带混淆功能的编译器进行编译,对生成的代码进行混淆。无源码的代码混淆需要借助壳,对原SO的指令进行抽离,然后对抽离的代码做混淆转换。
1.4.1.5 VMP
有源码的VMP方案一般都是借助编译器,在IR层对指令进行虚拟化并插入解释器。无源码的SO加固方案目前还未普及,但原理和有源码方案类似,将原指令处理成虚拟数据,并且插入解释器对虚拟数据解释执行。由于VMP对性能损耗大,对移动端来说,性能和兼容性都需要衡量,实用性不及代码混淆。
2. 加壳技术的特征
整体加固:是所有壳的基础,关键在于怎么区分函数抽取、VMP以及Dex2C甚至是多种技术混合的混合型壳。
函数抽取:获取到保护的DEX后,函数体的内容是无效的,注意这里说的是无效而不是无意义,有的App加壳后函数依然是有意义的,但不是我们想要的。
VMP:获取到保护的DEX后,函数的属性由Java属性变为Native,典型的有onCreate函数Native化。
Dex2C:获取到保护的DEX后,和VMP一样,被保护函数的属性由Java属性变为Native,如开源的DCC(Dex-to-C Compiler)。
混合型壳:多种加固技术混合使用,比如先将原有Smali指令流使用VMP或Dex2C保护,然后再经过函数抽取进一步保护。
3. 编译安卓源码
为什么要编译源码脱壳?
- Frida检测非常难
- root检测非常难
- 可以直接使用源码中的函数,大大提高开发效率
4. 沙箱脱壳机的核心原理
4.1 脱壳的本质
Android App脱壳的本质就是对内存中处于解密状态的DEX的dump。
首先要区分脱壳与修复。脱壳指的是对加固APK中保护的DEX的整体的dump,不管是函数抽取、Dex2C还是VMP,首先要做的就是对整体DEX的dump,然后再对脱壳下来的DEX进行修复。要达到对APK的脱壳,最为关键的就是准确定位内存中解密后的DEX文件的起始地址和大小。达到对APK的成功脱壳,最为关键的要素是:
- 内存中DEX的起始地址和大小。只有拿到这两个要素,才能够成功dump下内存中的DEX。
- 脱壳时机。只有正确的脱壳时机,才能够dump下明文状态的DEX。否则即使是正确的起始地址和大小,dump下来的也可能只是密文。
4.2 关键类流程分析
脱壳时机可以从下面几个方法分析。脱壳点可以通过插入以下函数找到。
4.2.1 InMemoryDexClassLoader源码分析
- static object CreateSingleDexFileCookie()
- static const DexFile * CreateDexFile()
- DexFile::Open()
- OpenCommon()
- DexFile::DexFile()
4.2.2 DexClassLoader加载DEX源码分析
- OpenAndReadMagic()
- DexFile::OpenCommon()
- DexFile::DexFile()
4.2.3 Dex2oat
Dex2oat(Dalvik excutable file to optimized art file)是一个对DEX文件进行编译优化的程序,在我们的Android手机中的位置是/system/bin/dex2oat
。通过编译优化,可以提升用户日常的使用体验(包含安装速度、启动速度、应用使用过程中的流畅度等),是Android Art Runtime中的一个重要模块。
Android虚拟机可以识别的是DEX文件,应用使用过程中如果每次将DEX文件加载进内存,解释性执行字节码,效率会很低,严重影响用户体验。通过Dex2oat优化后,可以在系统运行之前利用合适的时机将DEX文件字节码提前转化为虚拟机可以执行运行的机器码,后续直接从效率更高的机器码中运行,则运行阶段更加流程,优化用户体验。
5. 二代壳对方法体的静态抽取与动态回填
5.1 抽取壳的完整运行流程
- 解析DEX文件,保存所有方法的指令结构信息
- 通过方法所属的类名和方法签名信息,获取其对应的指令结构信息
- 获取方法指令的个数和指令的开始位置,然后将其指令全部置空
- 重新计算文件的checksum和SHA值,回写到DEX的文件头部
- 将抽空的DEX文件,放到手机指定目录下,然后进行加载运行
- 在Native层hook
libdvm.so
中的dexFindClass()
函数 - 在Java层使用DexClassLoader加载DEX并且反射运行被抽空的类的方法
- 通过DexFile结构体一次获取被抽空的方法对应的指令内存地址
- 修改内存为可读写属性,还原指令到内存块中
5.2 手动进行函数抽取
写一个简单的APK程序,Make Project。
在C:\Users\v5le0n9\AndroidStudioProjects\MyApplication\app\build\outputs\apk\debug
生成的APK程序解压。将解压出来的DEX文件载入jadx中查看函数信息,可以看到我们刚才编写的testFunc()
中的内容。
将该DEX文件载入010 Editor,使用DEX模板打开。壳进行函数抽取时,是将DEX文件中的method_ids与class_defs中的内容全部置空。
为了减少工作量,我们只将testFunc()
函数置空。
更新完后载入jadx发现出错,DEX文件中的checksum值不对:
所以还要在010 Editor中修改该DEX文件dex header中的checksum值。错误提示我们原本文件的checksum为0xd89949be,修改过后的checksum经过计算为0x01dc48ab,所以应将修改过的DEX文件的checksum值改为0x01dc48ab,注意小端存储。
此时再次打开就没有错误警告了。可以看到testFunc()
函数已经被抽空了。
5.3 动态回填
5.3.1 类加载时机
类加载有两种加载形式:
- 隐式加载:
- 创建类的实例
- 访问类的静态变量,或者为静态变量赋值
- 调用类的静态方法
- 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象
- 初始化某个类的子类
- 显示加载:两者又有所区别
- 使用
LoadClass()
加载 - 使用
forName()
加载 - 通过源码分析Android类加载的流程
- 使用
5.3.2 类装载流程
- 装载:查找和导入class文件
- 链接:其中解析步骤可选
- 检查:检查载入的class文件数据的正确性
- 准备:给类的静态变量分配存储空间
- 解析:将符号引用转成直接引用
- 初始化:调用
函数,对静态变量、静态代码块执行初始化工作
进行动态回填必须要使用进程内Hook技术。
5.4 实现函数抽取壳的前置技术
如果dex2oat对抽取的DEX进行了编译生成了OAT文件,那么动态修改的DEX中的Smali指令流就不会生效。所以art下的抽取型壳首先就是要禁用dex2oat。
如何禁用dex2oat:
- Hook关键函数,使其不进入dex2oat流程
- 直接使用InMemoryDexClassLoader
如果不禁用dex2oat,那么函数体必须在进入dex2oat流程前恢复好,才可以在dex2oat流程之中进行脱壳,脱下完整的函数体恢复之后的DEX。
GenerateOatFileNoChecks -> Dex2Oat -> Exec -> exec_utils.cc -> Exec -> ExecAndReturnCode -> execve。
使用Native hook,hook住execve,使其不运行dex2oat进程。
6. 更强抽取壳的dump
6.1 Fart整体dump思路的演进
6.1.1 v1.0:ClassLoader中dump
- 时机:App中的Applocation类中的
attachBaseContext()
和onCreate()
函数是App中最先执行的方法,因此需要选在Application的onCreate()
函数执行之后才开始被调用的任意一个函数中。比如选择在ActivityThread中的performLaunchActivity()
函数作为时机,来获取最终的应用的ClassLoader。 - 方式:hook -> 反射和dump。获取到应用解密后的DEX文件最终依附的ClassLoader之后通过Java的反射机制最终获取到对应的DexFile的结构体,并完成DEX的dump。
6.1.2 v2.0:更多“海量”的脱壳点
- 时机:所有类和方法的装载和链接/编译和执行流程之中。
- 方式:基于hook -> dump。ART下DexFile类中定义了两个关键的变量:begin_、size_以及用于获取这两个变量的
Begin()
和Size()
函数。这两个变量分别代表着当前DexFile对象对应的内存中的DEX文件加载的起始位置和大小。只要有了这两个值,我们就可以完成对这个DEX的dump。
6.1.3 v3.0:优中选优后的脱壳点
- 时机:找到绕过dex2oat的时机,类的初始化函数始终运行在ART下的interpreter模式。
- 方式:在解释执行
时进行脱壳,实现“绕过”dex2oat,因此必然进入到 interpreter.cc
文件中的Execute()
函数,进而进入ART下的解释器解释执行。
6.1.4 v4.0:优中选优+双保险
- 时机和方式:同时在dex2oat和类的初始化流程函数设置hook。
6.2 Youpk的整体dump思路
6.2.1 v4.0:优中选优+双保险
- 时机:App启动后10s开始。
- 方式:禁用dex2oat,在dex2oat中设置CompilerFilter为仅验证compileroptions -> SetCompilerFilter(),从ClassLinker中遍历DexFile对象并dump。
6.3 Youpk+Fart整体dump进一步提升
6.3.1 每个脱壳点都可以脱壳DexFile
- 时机和方式:在安卓8上禁用dex2oat,结合Fart所提出的海量脱壳点,每个脱壳点都可以脱壳。