Android脱壳技术简介与实战

在 Android 逆向中,大家应该或多或少听说过加壳、脱壳等词,那这个壳是做什么的?如果一个 apk 文件加了壳,我们又该怎么进行逆向?本节我们就学习一下与壳的管理和脱壳相关的技术。

实例引入

我们知道,Android App 的安装包文件是 apk 格式的,从本质上讲,这就是一个压缩包,解压之后,其中包含 Android App 的源码、配置文件和资源文件等。

我们先做一个测试,下载 13.6 节使用的示例 App,得到一个 scrape-app4.apk 文件,然后将文件扩展名修改为 zip,即文件名变成 scrape-app4.zip,随后用解压软件直接解压这个文件,得到的结果如图 13-92 所示。

图13-92 解压 scrape-app4.zip 文件的结果

这里的 classes.dex 文件就是 apk 文件的指令集,只需要简单地反编译一下就能得到 Java 代码,我们直接用 jadx-gui 打开这个 classes.dex 文件,等一会儿,源码就反编译出来了,如图 13-93 所示。

图13-93 示例 App 的源码

由此可见,如果一个 App 能轻而易举地被反编译出源码,那对开发者、运营该 App 的公司无疑是很严重的一击。毕竟 App 里的代码实现一览无遗,加密算法也能被看得一清二楚,如果有人想复制和抄袭,简直是轻而易举。所以,做好防护是必不可少,自目前最常见的防护手段就是加壳。

顾名思义,壳就像一个盔甲,可以起到防护作用,Android App 的壳就是用来保护 App 的源码不被轻易反编译和修改的。也就是说,加壳之后,真正的 App 源码会被 “隐藏” 起来,直接反编译 apk 文件是无法得到的。

我们再做一个测试,打开 https://app7.scrape.center 下载另一个 apk 文件,下载好后尝试用 jadx-gui 打开它,结果见图 13-94。

图13-94 App 加壳后的反编译结果

可以看到,这次没有直接得到 App 的源码,原来的 com.goldze.mvvmhabit 包里只有一个 R.java 文件,另外有了 com.qihoo.util 和 com.stub 两个包,这就是 App 加壳(本案例为 360壳)之后的效果。

加壳的原理

其实壳本身也是一个 dex 文件,我们可以称之为 shell.dex 文件。通常,在加壳之后,原 apk 文件中的 dex 文件会被加密,于是我们没法直接破解和反编译它了。但是,shell.dex 文件可以解密已经加密了的 dex 文件,并运行解密后的 dex 文件,在这个过程中,shell.dex 文件承担了一个入口的角色,对已经加密的 dex 文件进行解密并运行解密结果,从而达到和运行加密前的 dex 文件同样的效果。

加壳过程分为如下三个步骤。

  • 对原 dex 文件加密:从需要加壳的 apk 文件中,可以提取出一个 dex 文件,我们称之为 origin.dex 文件,利用某个加密算法(如异或、对称加密、非对称加密等)对该文件进行加密,可以得到一个 encrypt.dex 文件。

  • 合成新 dex 文件:合并加密得到的 encrypt.dex 文件和 shell.dex 文件,将 encrypt.dex 文件追加在 shell.dex 文件后面,形成一个新的 dex 文件,我们称之为 new.dex 文件。

  • 替换 dex 文件:把 apk 文件中的 origin.dex 文件替换成 new.dex 文件,并重新进行打包签名。

如此一来,原来的 apk 文件就完成加壳了,我们也无法利用 jadx-gui 等工具直接反编译 origin.dex 文件了。那加壳之后的 App 怎么运行呢?

其实很简单,通常在 shell.dex 文件中,有一个继承自 Application 类的类,App 在启动时会最先运行这个类,例如上面的案例中就定义了一个 StubApp 类:

public final class StubApp extends Application

这个类做了什么事呢?其实里面定义的就是一些解密 encrypt.dex 文件和加载解密后的 dex 文件的操作,即解密、还原操作。

具体怎么实现的呢?有两个关键的方法。

  • attachBaseContext:这个方法主要负责从 new.dex 文件中读取出 encrypt.dex 文件,然后对其进行解密,利用自定义的 DexClassLoader 对象加载解密后的 origin.dex 文件。

  • onCreate:通过反射机制修改 ActivityThread 类的内容,将 Application 指向 origin.dex 文件中的 Application,然后调用 origin.dex 文件中的 Application 的 onCreate 方法启动原程序。

通过实现这两个关键方法,origin.dex 文件中定义的逻辑就能正常执行了,这就保证了加壳前后整个 App 的运行效果完全一致。

壳的分类

上面介绍的加壳技术应用比较广泛,但是道高一尺魔高一丈,随着越来越多的 App 采用这种加壳技术作为防护,脱壳技术也在不断更选。两个技术不断地抗衡,不断地进化。

目前,壳已经发展到第三代了,上面介绍的只能算第一代。下面简单给三代壳归一下类。

  • 一代壳:整体加壳,整体保护,即上面介绍的加壳技术,对 App 中原本的 dex 文件整体加密后,将其和壳 dex 文件合成一个新的 dex 文件。壳 dex 文件负责对 App 中的加密 dex 文件解密并还原,从而保证 App 可以正常运行。对于这类壳,利用 jadx-gui 这种工具通常只能看到壳 dex 文件,原 dex 文件则看不到。

  • 二代壳:提供方法粒度的保护,即方法抽取型壳。保护力度从整体细化到了方法级别,也就是将 dex 文件中的某些方法置空,这些方法只在被调用的时候才会解密加载,其余时候则都为空。对于这类壳,利用 jadx-gui 反编译的结果中,方法全是 nop 指令。

  • 三代壳:提供指令粒度的保护,即指令抽取型壳。自前主要分为 VMP 壳和 dex2C 壳,就是将 Java 层的方法 Native 化。VMP 壳会对某些代码进行抽离,将其转变为中间字节码,VMP 相当于一个字节码解释器,可以对中间字节码进行解释执行。dex2C 壳几乎把所有 Java 方法都等价进行了 Native 化。

那怎么判断一个壳是第几代呢?

  • 对于一代壳,反编译之后如果只能看到继承自 Application 类的壳代码,其他诸如 Android 四大组件的类都被隐藏了,那就是一代壳。

  • 对于二代壳,反编译之后看看方法的实现是不是为空,如果 Java 代码的方法实现是空的,Smali 代码有很多 nop 指令,那基本可以断定是二代壳。

  • 对于三代壳,反编译之后看看一些方法是不是被 Native 化了,例如 onCreate 方法的声明前面如果有一些 native 关键词,那就是三代壳。至于到底是 VMP 壳还是 dex2C 壳,可以根据方法注册地址等做进一步判断。

脱壳实践

上面讲了壳的原理和分类,那么问题来了?如何给加壳的 App 脱壳呢?

  • 一代壳:目前市面上的一些免费加壳(加固)服务几乎都是一代壳,例如 360 加固、腾讯加固、阿里加固、爱加密,现在已经有较为成熟的查壳工具(如 PKID),选择 apk 文件之后,该工具就可以根据壳里面的一些特征判断是哪家的壳。另外,对于一代壳,现在主流的脱壳工具非常多,有 frida_dump、FRIDA-DEXDump 等,稍后会详细介绍。

  • 二代壳:加这种壳一般是需要付费的,现在有不少银行 App 是用的这种壳。由于方法只有在被调用的时候才会解密加载,因此脱壳的基本思路就是主动调用,现在主流的脱壳工具是 FART。

  • 三代壳:这种壳目前没有成熟的脱壳工具,基本上得靠手工分析,只要工具深,肯钻研,也是 能解开的。

下面我们还是以本节开头的 App 为例介绍一下脱壳方式,由于这个 App 使用的是一代壳,所以这里先介绍 frida_dump 和 FRIDA-DEXDump 两个工具。

frida_dump的使用方法

frida_dump 的基本原理是通过文件头的内容搜索 dex 文件并 dump 下来,其 GitHub 地址是: https://github.com/lasting-yang/frida_dump。

要使用这个工具,需要先下载其源码:

git clone https://github.com/lasting-yang/frida_dump.git

然后在手机上安装下载好的 apk 文件,运行 App,另外还需要在手机上运行 frida-server,具体的配置见 13.5 节。

之后在电脑上运行如下命令:

frida -U -f com.goldze.mvvmhabit -l dump_dex.js --no-pause

这里指定了脱壳脚本 dump_dex.js 和 App 的包名,运行之后就开始脱壳了,控制台的输出结果如图 13-95 所示。

图13-95 控制台的输出结果

脱壳完毕后的结果都在手机的 /data/data/com.goldze.mvvmhabit/files 文件夹里,我们可以运行如下命令把它们拉取到电脑上:

adb pull /data/data/com.goldze.mvvmhabit/files ~/dexes

这里我们将结果放到了电脑的 ~/dexes 文件夹中,如图 13-96 所示。

图13-96 电脑的 ~/dexes 文件夹

可以看到,这里一共得到了 8 个 dex 文件,一般而言,核心逻辑存在于较大的 dex 文件里。我们使用 jadx-gui 打开一个 dex 文件,看看还原效果。如图 13-97 所示,这个 dex 文件中就包含一些核心逻辑,比如 com.goldze.mvvmhabit 包里定义的内容。

图13-97 反编译一个 dex 文件的结果

通过文本搜索,我们也能找到一些关键的代码,如图 13-98 所示。

图13-98 搜索调用 “/api/movie” 的代码 图13-98 中有两个搜索结果,我们转到第一个,这就是之前分析(13.2 节)过的 index 方法的还原结果:

可见还原度还是比较高的。

FRIDA-DEXDump

FRIDA-DEXDump 也是一款比较不错的脱壳工具,同样是基于 Frida,由于 Frida 提供了在电脑上对手机 App 进行内存搜索的支持,因此 FRIDA-DEXDump 根据一些暴力内存搜索的原理实现了脱壳。对于完整的 dex 文件,暴力搜索 “dex035” 即可找到壳;对于一些抹头的 dex 文件,可以通过特征匹配找到壳,例如搜索 DexHeader 中的长度信息、索引指向的位置顺序等。

FRIDA-DEXDump 的 GitHub 地址是 https://github.com/hluwa/FRIDA-DEXDump ,可以直接使用 pip3 工具安装它:

pip3 install frida-dexdump

同样地,在手机上启动 frida-server 和示例 App 后,运行如下命令即可完成脱壳:

frida-dexdump -n com.goldze.mvvmhabit -f

运行结果如图 13-99 所示。

图13-99 利用 FRIDA-DEXDump 脱壳的结果

脱壳之后的文件就直接保存在电脑上了,控制台会输出 dex 文件的保存路径。之后利用和 frida_dump 那里同样的方式对得到的 dex 文件进行反编译即可,这里不再赞述。

FART

对于二代壳,目前主流的解决方案是 FART,这是 ART 环境下基于主动调用的自动化脱壳方案,本节不再展开讲解,如果感兴趣可以参考 https://github.com/hanbinglengyue/FART ,这个项目中介绍了 FART 的原理和使用方法。

总结

本节总结了 Android 脱壳技术的原理和解决方案,熟练掌握脱壳技术已经是现在 Android 逆向和爬虫开发者的必备技能之一。

对于脱壳技术,我们不但要知其然,还要知其所以然,这个技术领域涉及了非常多 Android 底层的知识,要想深入研究是需要下一定功夫的。

本节内容的参考来源如下。

  • 掘金网站上的 “Android 脱壳之整体脱壳原理与实践” 文章。

  • CSDN 网站上的 “基于 FRIDA 的几种安卓脱壳工具” 文章。

  • 吾爱破解网站上的 “FRIDA-DEXDump:一吻便杀一个人,三秒便脱一个壳” 文章。

  • 博客园网站上的 “VMP壳基础原理” 文章。