前言
Android 上的应用安装文件是 apk 格式,这个 apk 文件其实就是个归档文件压缩包,把应用相关的源码、资源文件、配置文件等乱七八糟的东西都简单粗暴的归档打包,其包内文件结构大体如下:
文件或目录 | 作用 |
---|---|
META-INF/ | 存放签名信息,用来保证 apk 包的完整性和系统的安全。 |
res/ | 存放资源文件的目录 |
libs/ | 若存在,则存放的是 ndk 编译出来的 so 文件库 |
AndroidManifest.xml | 程序全局配置文件 |
classes.dex | 最终生成的 dalvik 字节码 |
resources.ars | 编译后的二进制资源文件。通常本地化、汉化资源存储在该文件文件中。 |
因此,我们将 apk 后缀改成 zip 压缩格式并直接解压就能得到 app 的 dalvik 指令集 dex 文件,然后再将其反编译为 smali 文件,还可以再反编译为 java 代码文件,这样就几乎等同于拿到了 app 就能拿到可读性还蛮清晰的源码,导致逆向破解的门槛几乎很低;没有做防范措施的 app 几乎等同于在裸奔,因此后来演变出来一些保护措施,让 apk 被反编译后获取的代码难理解。
一、混淆与加固
混淆和加固是两个非常不同的概念,在对 apk 进行逆向反编译的过程中,判断一个 apk 是被混淆了亦或是加固了则是有必要的,而两者的含义也是不同的,下面是两者的区别:
混淆
混淆,是一种类似障眼法的作用,让反编译后的代码阅读难度增加,本质上来说,并非是防止了反编译,而是增加了阅读难度,使得阅读的人无法通过名称猜测其用途。例如将要混淆类名和函数名,替换为无意义的短名称(如:OrderUtils. createOrder() -> A.b() )。虽然代码混淆可以提高反编译的门槛,但是对开发者本身也增大了调试除错的难度。开发人员通常需要保留原始未混淆代码用于调试。
加固
加固,可以理解为,将 APK 的外层加了一层壳,把重要数据信息隐藏起来,如果想反编译,必须突破这层壳的保护。加固后的 APK ,反编译出来,看到的只是外面那层壳的代码,可以防止应用被各类常见破解工具逆向,安全性要远大于单纯的代码混淆。
以上两种方式,混淆用于让 apk 被反编译后获取的代码难理解,加固用于让 apk 难于被反编译。两种操作都是对项目的安全措施,两个操作是不冲突的,可以选择其一,也可以两个操作都做。
二、APK壳识别
注:此处仅针对对 apk 是否加壳进行分析,对代码混淆判断不做详细介绍,代码混淆最常规的方法就是反编译 dex 后,使用工具浏览查看其中是否有相关类名或函数名被无意义的短名称替换。
在对 apk 是否加壳的判断上,我们可以使用以下几种方法:
方法一:判断 apk 是否加壳,可以先将 apk 后缀改为 zip ,再通过解压工具解压该 zip 文件,查看解压后的文件夹的根目录下是否含有 classes.dex 或 classes2.dex 等。若含有,可以通过 jadx 工具打开是否可以看到对应文件完整代码,这个是最简单的情况。
方法二:反编译 AndroidManifest.xml 文件,然后遍历里面的 activity、service、broadcast、provider 等,看这几个 class 是否都存在于 classes.dex 文件里面。
*方法三:一般做加固的厂商显然也是要在运行时对数据解密的,所以必然会有相应特征的 java 代码或者是特征 so 文件打包在 apk 文件中,可以通过找这些东西来确定是否加固,而这也是主流加固工具判断的依据。常见的一些厂商加固的特征 java 代码或特征 so 文件简单汇总如下:
厂商 | 常见特征so文件或其它文件 | 常见java代码(反编译classes.dex) |
---|---|---|
娜迦 | libchaosvmp.so、libddog.so、libfdog.so | |
娜迦企业版 | libedog.so | |
爱加密 | libexec.so、libexecmain.so、ijiami.dat | s.h.e.l.l.S |
爱加密企业版 | ijiami.ajm | |
梆梆免费版 | libsecexe.so、libsecmain.so、libSecShell.so | com.secneo.apkwrapper.ApplicationWrapper、com.SecShell.SecShell.ApplicationWrapper、com.secneo.apkwrapper.AW |
梆梆企业版 | libDexHelper.so、libDexHelper-x86.so | |
360 | libprotectClass.so、libjiagu.so、libjiagu_art.so、libjiagu_x86.so、libjiagu_x64.so、libjiagu_a64.so | com.stub.StubApp |
通付盾 | libegis.so、libNSaferOnly.so | |
网秦 | libnqshield.so | |
百度 | libbaiduprotect.so | com.baidu.protect.StubApplication |
阿里聚安全 | aliprotect.dat、libsgmain.so、libsgsecuritybody.so、libmobisec.so | |
腾讯 | libtup.so、libexec.so、libshell.so、mix.dex、lib/armeabi/mix.dex、lib/armeabi/mixz.dex | com.tencent.StubShell.TxAppEntry |
腾讯御安全 | libtosprotection.armeabi.so、libtosprotection.armeabi-v7a.so、libtosprotection.x86.so | |
网易易盾 | libnesec.so | |
APKProtect | libAPKProtect.so | |
几维安全 | libkwscmm.so、libkwscr.so、libkwslinker.so | |
顶像科技 | libx3g.so | |
盛大 | libapssec.so | |
瑞星 | librsprotect.so |
主流的 Android app 保护厂商的产品:梆梆、腾讯、爱加密、360、阿里和百度。这些厂商是来实现 app 的保护的相关原理如下:
360:将原有的 dex 文件加密后存储在 libjiagu.so、libjiagu_art.so,在运行时动态释放并解密。
阿里:将原有的 dex 文件拆分为两部分,一部分主体保存为 libmobisecy.so,另一部分包含了一部分 class_data_item 和 code_item。在运行的时候将两部分释放在内存中,并修复相关的指针,恢复数据之间的连接关系。同时一些 annotation_off 被设置为无效的值。
百度:将一些 class_data_item 存储在 dex 文件的外部,在运行时恢复与主体的 dex 的连接关系。在 dex 文件加载后,其头部的魔数,校验和以及签名值会被擦除。同时某些方法被改写,使得其在执行前相关的指令才会被恢复,在执行之后便立即擦除。
梆梆:提前准备了一个 odex 或 oat 文件,并加密保存为外部的 jar 文件,运行时解密;同时 hook 了 libc.so 中的一些函数,如 read,write,mmap 等,监视其操作区域是否包含了 dex 的头部,保证无法使用这些函数对 dex 文件进行操作。
爱加密:同样是加密原有的 dex 文件,在运行时整体释放并解密,只不过其释放的处于固定路径下的临时文件的名字是随机的。
腾讯:提供选项可以指定需要保护的方法。如果某个方法被保护,则在 dex 文件中的相关 class_data_item 中无法看到其数据,即为一个假的 class_data_item;在运行时释放真正的 class_data_item 并连接到 dex 文件上,但是其 code_item 却一直存在于原有的 dex 文件中。同样,一些 annotation_off 和 debug_info_off 被填充为无效值来阻止静态反编译。只支持在 DVM 环境下运行。
方法四:在对应客户端借助相关工具进行辅助判断,相关平台的辅助工具如下:
Android客户端
可借助MT管理器辅助判断 apk 是否加固,以中国建设银行和交管12123 的 apk 文件为例:
Windows客户端
可借助 APK 查壳工具(PKID)进行查看:
PKID 工具参考文章介绍及下载地址:Android查壳工具PKID
三、壳史
第一代壳
DEX加密(也称落地加载)
第一代壳将整个 apk 文件压缩加密到壳 dex 文件的后面,在壳 dex 文件上写上解压代码,动态加载执行,由于是加密整个 apk,在大型应用中很耗资源,因此这代壳很早就被放弃了但思路还是不变。其中这种加密还可以具体划分为几个方向,如下:
- Dex 字符串加密
- 静态 DEX 文件整体加密解密
- 资源加密( xml 与 arsc 文件加密及十六进制加密)
- 对抗反编译(针对反编译工具,如 apktool。利用反编译工具本身存在的缺陷,使得反编译失败,以此实现对反编译工具的抵抗)
- Ptrace 反调试、TracePid 值校验反调试
- 自定义 DexClassLoader(主要是针对 dex 文件加固、加壳等情况)
- 落地加载( dex 可以在 apk 目录下看到)
相关脱壳方法
- 内存 Dump 法
- 缓存脱壳法
- 文件监视法
- Hook 法
- 定制系统法
- 动态调试法
第二代壳
Dex抽取与 So 加固
第二代壳就要聪明的多,首先加密对象就不是整个 apk 而是变成了 apk 内的代码文件 dex,这个时候第二代壳就体现出了其强大的实用性,如果说第一代壳只是一个理论基础而第二代壳就是可以量产的初号机型了。
第一代壳是将其加密的 dex 文件存放在 dex 文件里的,由于 java 可读性较高,这为逆向人员分析脱壳代码降低了难度,因此第二代壳是将 dex 代码加密到 native 层,即 so 文件。大大增加了逆向难度。根据各大厂商以这个思路加密的手段,又可以分为:
- DEX 动态加载(分为利用 jni 和自定义 jni,即自定义底层函数)
- Dex Method 代码抽取到外部(类抽取加密按需解密和动态方法修改替换)
- So 加密
- 反调试,防 HOOK
- 不落地加载 ( apk 目录下不能看到原始 dex,把加密的 dex 文件加载到内存中,然后解密,从始至终不会以文件形式存在)
相关脱壳方法
- 内存重组法
- 内存 Dump 法
- Hook 法
- 动态调试法
- 定制系统法
第二代壳通用脱壳方法,直接上工具:
工具地址及其用法:https://github.com/zyq8709/DexHunter
第三代壳
Dex 动态解密与 so 混淆
第三代壳是对第二代壳的升级改进,属于同一个系列,仍是加密 dex,并且存放到 so 文件里,打个比方,如果第二代壳使得你脱壳思路充满不确定性那么第三代壳不仅给你带来不确定性还给你的每一条思路都设置了多个路障,这个路障叫做反调试。这使得原来可以通过动态调试能够破解的壳再次增加了难度。如如下几种方法:
- DEX 保护:DEX Method 代码抽取、Dex Method 代码动态解密
- So 代码膨胀混淆(指令抽取然后一次性还原,还原到原始 dex 文件。注:指令只有被需要时才还原)
- 动态防护:防内存 dump、防系统核心库 HOOK
- 资源文件保护:H5 文件保护
相关脱壳方法
- dex2oat 法
- 定制系统法
第四代壳
vmp(虚拟机保护)
自定义一套虚拟机指令和对应的解释器,并将标准的指令转换成自己的指令,然后由解释器将自己的指令给对应的解释器。
相关脱壳方法
用 vmp 加固后的还原过程比较复杂和困难,需要用大量的时间作分析。主流工具基本无法自动完成脱壳任务,此处不做具体概述。
目前加固技术基本都发展到第三、四代,前两代的加固技术破解难度不大,且各平台加固厂商的免费版基本是第一、二代壳技术,第三、四代加固技术大多是平台付费服务或企业版。
四、脱壳方法
内存Dump法
因为第一代壳在内存中是完全解密的,可以从内存中 dump 需要解密的 APK 内存,即可完成脱壳工作。其原理是在内存中寻找 dex.035或 dex.036,例如在 /proc/xxx(pid)/maps 中查找对应 classes.dex 后,根据其内存地址值,手动 Dump。
在 Linux 系统中,/proc 文件系统是一种内核和内核模块用来向进程发送信息的机制。它是一个伪文件系统,存在于内存之中而不是硬盘上。在内存中运行的程序,都在该文件系统中的编号子目录记录着相应的状态信息,程序运行时的进程 id(PID) 就是对应该文件系统中的编号文件夹。
这种 dump 方法,已经不适用现在的主流壳了。又因为动态运行,因此内存中会有完整的 dex 文件,并且它可能会存放在一个临时文件中,这个文件就在 /proc/xxx(pid)/maps
中对应一处内存值,然后检索该处内存并拷贝到本地就行了。该方法比较适合第一二代壳,第三代壳已经不适用。
相关工具:
android-unpacker。工具链接:https://github.com/strazzere/android-unpacker
该工具的核心是通过
find_magic_memory()
方法读取/proc/xxx(pid)/maps
内存映射表,找到 DEX 所在的内存起始位置,然后通过dump_memory()
方法将内存 dump 下来。该工具已不推荐使用。
drizzleDumper。工具链接:https://github.com/DrizzleRisk/drizzleDumper
该工具是根据 android-unpacker 优化改造而成,是一款 ndk 写的动态 Android 脱壳的工具,原理简单来说就是 ptrace,然后在内存中匹配特征码和 dex 的 magic ,最后 dump 到文件。
其操作流程可借鉴相关文章:使用drizzleDumper脱去某数字公司的壳
文件监视法
即使用 dex 优化生成 odex 方法,监视 DexOpt 的输出。
相关工具:
inotifywait-for-Android。工具链接:https://github.com/dstmath/inotifywait-for-Android
该工具在 github 上仅提供了源码,尚未找到已编译好的成品工具,自己便使用 ndk-build 来编译了一下成品工具分享出来,下载链接:inotify(阿里云盘)
将该工具使用 ndk-build 进行编译,然后使用 adb 将其导出至对应设备 /data/local/tmp
目录下,赋予该工具可执行权限,使用相关命令监视对应文件的状态,如下图:
该方法局限很大,误报很多,因为文件变化的不止是解密造成的,各种情况都会引起 dex 文件变化而且文件变化也不止是 dex 文件,该方法可作为辅助判断方法,实际脱壳过程中并不推荐使用。
缓存脱壳法
动态加载型壳用 DexClassLoader 方式将加密后的 DEX 在内存中解密后动态加载,但一些软件壳没有处理 DEX 优化时缓存的路径,最终使得系统执行 dexopt 命令对加载的 DEX 进行优化时,将优化结果放到默认的 /data/dalvik-cache
目录。解密时不需要做任何额外的工作,因此只要将此目录下的 ODEX 取出,进行一次 deodex 操作,即可完成脱壳。
Hook法
通过 frida、Xposed 或者其他一些 Hook 工具,Hook 比如 dalvik 时代(代表:Android 4 及以前)的 dvmDexFileOpenPartial 函数,Android 5.0-7.0 的 OpenMemory 函数,Android 8.0-10.0 上的 OpenCommon 函数(使用 Hook 工具强烈建议 Android 版本不要过高,Android 7 及以下版本为最佳选择)。
在 Android 客户端,有一些前大佬开发完成的脱壳工具,这些工具运行依赖 Hook 框架,如 Xposed,此处将其平时使用到的工具进行整合分享。因考虑到不是所有人的 Android 设备已获取 ROOT 权限或已安装框架,遂一并提供一个运行在 Android 系统上的虚拟机 VMOS,内含 Xposed 框架,然后将对应的工具导入到虚拟机中使用。
工具链接:https://pan.baidu.com/s/1ozVyDDC5z8m-K-dIW3Garw
提取码:8888
动态调试法
该方法本质还是内存 Dump 法,需要通过调试器找到合适的 Dump 时机,即 DEX 文件已经在内存中完全解密,且其中的代码还没有开始执行。寻找合适的 Dump 时机是动态调试脱壳法的重点。
主流的设断点的方法有 dvmDexFileOpenPartial()
、 dexFileParse()
等方法,在该方法处设置断点,当执行到该断点时,使用内存 dump 脚本将其 dump 下来,即可完成脱壳操作。动态调试脱壳与 DEX 加载代码的处理方式有关。
相关工具:
IDA。工具链接:https://hex-rays.com/ida-free/
其操作流程可借鉴相关文章:获取dex之dump内存、IDA动态调试脱壳步骤、ida动态调试
内存重组法
代码抽取壳特征:将 DEX 文件的 DexCode 提取后填 0 ,将 DEX 文件的所有内容保存于 APK 文件中,当 APK 运行时,会在内存中进行动态解密,所有解密的方法内容指针都位于 DEX 文件结构体外部的内存中,从而有效避免了在只知道 DEX 文件的起始地址的情况下就可以快速进行 dump 的问题。
脱壳方法:解析内存中 DEX 文件的格式,将其重新组合成 DEX 文件,可以实现 DEX 代码还原。
相关工具:
针对 Dex 脱壳工具:
1、ZjDroid。工具链接:https://github.com/halfkiss/ZjDroid
2、dumpDex。工具链接:https://github.com/WrBug/dumpDex
对付一切内存中完整的 dex ,包括壳与动态加载的 jar。
针对 So 脱壳工具:
1、elfrebuild。工具链接:https://github.com/ylcangel/ElfRebuild
原理:构造映射 soinfo ,然后对其进行重建
dex2oat法
ART 模式下,dex2oat 生成 oat 时,内存中的 dex 是完整的,此时可以用修改后的 dex2oat 文件替换原系统的 dex2oat 文件。
相关工具:
- Dex2oatHunter。工具链接:https://github.com/spriteviki/Dex2oatHunter
定制系统法
修改 Android 源码中的相关函数后刷机实现。
其他工具
youpk:https://github.com/youlor/unpacker
fart:https://github.com/hanbinglengyue/FART
(荐)BlackDex:https://github.com/CodingGay/BlackDex
参考文章与文献
FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法
拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点
ANDROID 逆向学习笔记 (六)- 安卓脱壳之 DVMDEXFILEOPENPARTIAL
记一次APP脱壳重打包过程
attach法脱壳
等等……
文章若有错误,还望大佬指正,感谢!