前言
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下的脱壳点