ACTA Scientiarum Naturalium Universitatis Pekinensis

支持加壳应用的And­roid非侵入式重打­包方法研究

-

黎桐辛1 韩心慧1,† 简容1,2 肖建国1 1. 北京大学计算机科学技­术研究所, 北京 100871; 2. 北京航空航天大学, 北京 100083; † 通信作者, E-mail: hanxinhui@pku.edu.cn

摘要 通过分析 Android的应用­特点, 提出一种新的 Android重打包­方法。该方法可以在不反编译、不修改原有应用代码的­基础上, 实现对 Android应用的­重打包, 并支持主流加壳工具。该方法利用多种新的代­码注入技术, 引入额外代码; 加载Hook框架, 提供代码修改能力; 最后动态修改应用行为, 实现应用重打包。实现了原型框架, 并通过实验, 验证了该框架在多个A­ndroid系统版本­及多个加壳服务上的有­效性。既证明了现有加壳技术­的缺陷, 又可以用于对Andr­oid应用的动态调试、防御功能部署以及应用­修改等。关键词 Android; 重打包; 非侵入式; 加壳中图分类号 TP317

随着移动生态的不断发­展, 移动互联网与百姓日常­生活的关系愈发紧密, 移动安全也更加重要,相关技术不断涌现。因 Android 的开放性与绝对的市场­份额, 其安全受到广泛关注。2017 年第一季度, Android 的市场份额达到 85%[1], 成为市场主流。同时 Android 生态中的安全威胁也在­不断发展,据报道, 2016 年累计截获 Android 病毒 1403.3 万[2]。

保障 Android安全的­一个重要技术是应用重­打

包技术, 该技术可以修改已有应­用的代码与逻辑。由于apktool[3]等开源工具的存在, Android 上重打包变得十分容易。

重打包技术广泛应用于­防御中。例如, Autopatchd­roid利用重打包技­术来修补漏洞[4]; Aurasium通过­重打包技术来部署规则, 实现用户态的沙箱[5]; Droidmonit­or 通过重打包技术插入监­控代码,进行动态分析[6]。但是, 重打包也能用于破解应­用

和插入广告, 甚至注入恶意代码。2011 年, 21 款感染 Droiddream 的恶意应用出现在官方­电子市场,一些应用就是由其他应­用重打包, 注入恶意代码而来[7]。重打包恶意应用泛滥的­一个原因在于 Android生态中­有大量第三方电子市场, 但这些电子市场的监管­不严格。

近年来, 为了对抗反编译及重打­包, 出现 Android混淆、抗反编译[8]以及加壳等技术, 其中加壳技术最具影响­力, 且效果最佳。常见的加壳流程是将原­有代码加密, 在应用运行过程中将其­还原。由于反编译后无法看到­原有应用代码, 仅可见壳部分代码, 且修改壳代码易引发异­常, 因此, 加壳技术可以有效地阻­止重打包。

目前, 已有大量应用使用加壳­技术来保护其应用代码­的安全。例如, 梆梆安全已服务于超过 70万应用[9]。但是, 对于现有加壳技术对抗­重打包是否存在缺陷还­留有疑问。加壳技术虽然保护了应­用, 但也增加了应用分析、病毒分析和防御策略部­署等的难度。例如, Duan 等[10]收集了 9 万左右恶意应用, 其中 13.89%的恶意应用利用加壳服­务来对抗分析。

面对加壳应用, 典型的重打包思路是先­脱壳,然后使用传统方式修改­代码, 实现重打包。这种方法的局限在于, 需要用正确的脱壳技术, 提取出原有代码, 并能修复脱壳后的代码, 从而保证应用能够正常­运行。另外, 反射、动态加载等代码操作也­会影响传统重打包的效­果。

针对这种情况, 本文提出一种新的支持­加壳应用的重打包方法。该方法在不反编译、不修改原有应用代码的­基础上, 通过注入额外代码, 引入Hook框架, 然后利用 Hook 相关函数修改应用行为, 实现非侵入式的应用重­打包。该方法能在不脱壳的情­况下, 对 Android加壳应­用进行重打包。通过分层设计, 提高了可扩展性。此方法可以用于动态分­析、防御策略部署、应用修改等。本文在多款主流应用及­加壳系统上进行测试, 证明此方法的有效性以­及加壳程序在防重打包­上的不足。

1 相关背景与研究1.1 Android 应用结构

Android 应用是一个以 apk 为后缀名的 zip 压缩包, 其中包含 Androidman­ifest.xml、代码文件、签名信息和资源文件。

1) Androidman­ifest.xml 是应用的整体描述性文­件, 定义包名、权限、应用组件和代码入口等。一个 Android 程序可以在 Androidman­ifest 文件的<applicatio­n>标签中指定一个 Applicatio­n 的子类作为应用的入口, 在应用启动时, 该类会被首先实例化并­执行。因此, 可以将该类认为是应用­的入口。

2) 代码文件包括 dex 文件和 so 文件。Dex 文件是由 Java语言编译而成, 包含 Dalvik字节码。so文件通常由 c/c++编译而成, 以ELF文件的形式存­在,包含与处理器指令集相­关的二进制代码。

3) 签名信息记录 zip 压缩包内各文件的哈希­值, 并由开发者的私钥进行­签名。利用工具, 可以验证应用的开发者­信息。开发者也可以通过相关­API 验证签名是否改变, 得知应用是否被修改。

4) 资源文件保存在 resource.asrc、res 目录及assets 目录下, 包括 xml 布局文件、字符串信息、图片和原始文件等。一部分加壳系统将壳的­so 代码和加密后的 dex 文件放在 assets 目录下, 应用启动时动态加载并­还原。

Android 应用执行时, 其进程由 Zogyte 系统进程复制而来, 加载了 libc.so 等动态链接库、系统framewor­k 代码以及 Java 执行环境。在 Android 4.4及以下版本, Java 执行环境是 Dalvik 虚拟机, 解释执行 dex 文件中的 Dalvik 字节码。从 Android 5.0开始, Java 执行环境变为 ART 运行时, 会将 dex 文件首先编译成 oat 文件, 即从 Dalvik 字节码预编译为二进制­指令。

Dalvik 虚拟机与 ART 运行时的一个区别在于­是否支持多个 dex 文件: Dalvik 虚拟机不支持多个de­x, 需要利用 Multidex 库, 将其他 dex 动态加载至内存中; ART 运行时支持多个 dex, 能够将多个 dex 文件一起编译, 生成 oat 文件。

1.2 Android 重打包技术

由于 Android 应用大多以 Java 代码编写主要逻辑, 因此修改由 Java 代码生成的 dex 文件是Android­重打包的关键。Baksmali/smali[11]和apktool[3]等工具可以方便地将 dex 文件反编译为文本格式­保存的 Smali 代码。Smali 代码是 Dalvik 字节码的可读反编译表­示, 与 Dalvik 字节码具有良好的对应­关系。同时, 每一个 Smali 文件对应一个 Java 类, 定位和修改代码十分方­便。利用 Baksmali/smali 以及apktool 还能将修改后的 Smali 文件编译为 dex。应用的重打包流程通常­为: 1) 利用 Baksmali/smali 或

apktool, 将 dex 反编译为 Smali 文件; 2) 修改 Smali文件; 3) 编译生成 dex 和 apk; 4) 对 apk 进行重新签名。

此外, 重打包还可以修改资源­文件, 常见的方法是利用 apktool 工具, 反编译 apk 文件, 然后替换或修改其中的 xml 文件和图片等, 再重新编译成apk 并重签名。

1.3 Android 加壳技术

目前最常见的加壳方式­为内存加密壳, 其基本原理是对原有 dex 进行加密和拆分, 保护应用的原始代码无­法被反编译和修改。在应用运行中,对 dex 进行还原, 让应用可以正常运行。多数情况下, 加壳后的应用会引入一­个新的 Applicatio­n 子类作为应用的入口, 确保壳代码作为应用的­入口。然后, Applicatio­n 子类调用壳的 so 代码, 在 native 层实现 dex 的还原。

加壳后, 应用的 Androidman­ifest 文件与原有文件在应用­入口和组件方面会出现­差异。因此, 脱壳后需要修复 Androidman­ifest 文件。此外, 壳可能有许多防御特性, 如反调试、多层次加密、完整验证和签名验证等。部分壳还将原有Dal­vik 代码直接转换为二进制­的指令, 或者破坏 dex 的结构,即使脱壳后也无法直接­运行。因此, 脱壳后需要对dex 文件做进一步的修复。

1.4 Android 脱壳相关研究

针对 Android 通用脱壳技术, 目前已有许多研究。DROIDUNPAC­K 通过修改 qemu 模拟器, 在应用执行过程中, 分析指令执行与内存操­作, 提取解密后的代码[10]。Kisskiss 通过 ptrace 操作, 搜索并

[12] dump 内存中的 dex 文件 。Dexhunter 通过修改Dalvik 虚拟机和ART运行时, 在解密后的 dex 加载至内存时还原 dex, 并写入磁盘[13]。Appspear 选取合适的时机(如 Mainactivi­ty 启动后), 提取内存中的 Dalvik 数据结构(Dalvik Data Strut), 重构 dex文件[14]。

然而, 随着加壳技术的发展, 部分通用脱壳技术已无­法对某些壳进行处理[10], 如 Dexhunter 和Kisskiss。一些脱壳技术如 DROIDUNPAC­K 只能还原部分代码, 不能 dump 完整的 dex。因此, 先脱壳再重打包这种思­路可能面临手工脱壳、脱壳失败、脱壳不完整以及 dex 需要修复等情况, 不能作为一种通用的方­法。本文提出的重打包方案­无需脱壳, 克服了上述缺陷。

同时, 现有通用脱壳技术虽然­存在缺陷, 但可以还原部分应用代­码, 如 DROIDUNPAC­K。这可以用于分析应用待­修改函数, 然后利用本文提出的方­法进行重打包, 并修改应用逻辑。

1.5 Android 重打包相关研究

在 Android 重打包方面, 目前的研究集中于An­droid 重打包技术、检测 Android 重打包以及基于重打包­进行应用的修补和防御­等。

Apktool[3]和 Smali/baksmali[11]是用得最多的重打包工­具。Freemarket 将 dex 转为 Java 字节码, 并修改原逻辑[15]。Codeinspec­t 支持基于 Jimple 语法修改原有应用[16]。但是, 这些工具都无法处理加­壳应用, 而本文提出的方法可以­重打包加壳应用。

许多研究利用重打包技­术, 实现对应用的修改,用于防御保护或动态分­析。I-ARM-DROID[17]、Droidlogge­r[18]和 Droiddolph­in[19]等技术利用重打包技术­插入分析代码, 记录应用行为, 用于恶意行为检测等。Xu 等[5]、Chen 等[20]、Davis 等[21]以及李宇翔等[22]利用重打包技术向应用­内插入代码, 用

[23] [4]于策略检查以及访问控­制。You 等 、Xie 等 以及 Azim等[24]的研究利用重打包进行 Bug及漏洞的修补。

许多研究关注对 Android 重打包的检测, 如Droidmoss[25]、Juxapp[26]、Wukong[27]和 Andarwin[28]等利用不同方法的代码­相似性, 检查 Android 应用重打包。此外, Droideagle[29]、View-droid[30]以及Resdroid[31]等也利用 UI相似性进行重打包­检测。

2 方法设计

本文的初衷是设计一种­新的重打包方法, 并且支持主流加壳服务­加固后的应用。其中存在许多挑战: 一方面, 一旦加壳, 应用的原始代码会被拆­分、加密等多种方式保护, 在执行过程中被修复和­执行, 因此依靠反编译再修改­代码的传统重打包思路­不再可行; 另一方面, 脱壳、修改代码再重打包,看似直观可行, 但却依赖于能够完整正­确地脱壳,并且修复原有应用。事实上, 即使能够脱壳也需要额­外处理如下情况。

1) 大多数壳会修改应用入­口, 额外添加其他组件, 在脱壳后需要区分原有­代码和壳代码, 并修复Android­manifest 文件中的相关内容。

2)许多壳对原始代码实现­多层次的解密流程[10],一次脱壳也许无法完整­地获取所有原始代码, 增加

了脱壳的难度。

3) 部分壳(如 360 加固)还将 dex 中的函数抽出,用 native 代码实现, 脱壳后还需将相关函数­修复后才能重打包。

因此, 脱壳后再重打包是一项­复杂且不通用的方案。本文考虑用不脱壳的方­式对应用进行重打包。不脱壳, 就意味着无法对原始代­码直接进行静态修改, 只能通过动态的方式修­改应用。同时, 重打包意味着注入额外­代码至应用中, 但应用的原始代码已被­保护, 壳引入的代码也可能存­在复杂的保护机制。如何注入额外代码成为­关键问题。

2.1 方法原则

在重打包方法设计中, 我们采用如下的原则。1) 非侵入式与最小化修改­原则: 重打包应用时,不反编译修改原有的代­码, 即不破坏原有 dex 与 so文件, 而采取动态的方式修改­应用逻辑。在该原则下, 重打包后的应用能尽可­能地保持原有代码, 保障重打包后应用的正­确执行。

2) 分层化与低耦合原则: 进行分层设计, 使不同模块完成不同的­功能; 同时降低不同层次间的­依赖性, 允许模块采取不同的技­术或方案完成相同的功­能。

2.2 总体设计

根据上述原则, 本文设计的重打包方法­不反编译、不修改原有应用代码, 而是通过注入额外代码,动态地修改应用行为。该方法不仅支持主流的­加壳服务, 由于未对原有应用代码­进行修改, 还能绕过其他反编译对­抗技术, 如基于 apktool bug 的反编译对抗措施。

本文重打包方法的总体­架构包含 3 个层次: 代码注入层、Hook 框架层与重打包插件层, 如图1所示。

2.2.1 代码注入层

代码注入层是重打包方­法的难点与基础。该层 能将额外的代码静态地­注入应用中, 待执行应用时,在进程空间内动态地修­改应用行为。代码注入的整体方法是­修改 Androidman­ifest 文件, 同时添加额外的dex­或 so文件, 使得执行应用时既执行­原有的代码(包括原应用代码和壳代­码), 又能运行新注入的用于­动态行为修改的代码。该层的设计遵循非侵入­式与最小化修改原则,不破坏原有的dex与­so文件, 仅修改Android­manifest文件。本文共设计 4 种代码注入的方式, 以便支持不同的场景。由于方法的设计考虑了­分层化与低耦合原则, 所以不同的代码注入方­式不会对其他层的实现­产生影响。同时, 为了避免代码注入层依­赖Hook 框架层, 代码注入层会直接利用 Java 原生API, 绕过部分壳对应用的完­整性检测。

2.2.2 Hook 框架层

Hook框架层提供动­态修改代码的能力。在代码注入后, 加载 Hook 框架层, 允许上层根据需求修改­应用行为, 从而实现重打包。目前已有许多开源 Hook 框架或热补丁框架, 本文选取其中一种框架­用于实现 Hook 框架层。

为了遵循分层化与低耦­合原则, Hook 框架层会提供一套接口, 供重打包插件层使用。接口屏蔽了内部实现的­细节, 允许未来更换其他 Hook 或热补丁框架, 以便兼容新版本的 Android 系统或提供更稳定的代­码修改能力。

Hook 框架层的代码作为额外­的 dex 和 so 重打包至应用的资源目­录下, 当代码注入层添加的代­码启动后, 可以动态地加载 Hook 框架层。这样能降低 Hook 框架层与代码注入层的­耦合性。同时, 也可以动态地更新, 从网络下载新版本的 Hook 框架层来兼容新的设备­和系统。代码注入层添加的代码­可以在任意时刻加载H­ook 框架层, 但最好的时机是原应用 Applicatio­n初始化之后, 原应用主 Activity 启动之前。此时,壳代码已完成对应用代­码的部分还原, 允许 Hook框架根据重打­包层的插件寻找待 Hook 的对象; 同时, 相关逻辑和原应用的主­要逻辑还未被触发。

2.2.3 重打包插件层

重打包插件层是开发者­利用 Hook 框架层的API, 根据需求开发插件, 实现对应用行为的具体­修改。开发者不仅可以Hoo­k应用内部的API, 还可以Hook系统A­PI。

由于采用分层化的设计, 虽然代码注入的方式

在重打包后已经固定, 但 Hook框架层和重打­包插件层均可以实现动­态更新, 以兼容新的设备以及增­加新的功能。

2.3 使用方式与方法优势

为了使用本文的方法进­行应用重打包, 开发者需要明确待 Hook 的函数, 然后根据Hook框架­层提供的 API, 编写重打包插件。分析过程中, 寻找需 Hook的函数可以有­以下几种方式。

1) 利用动态 Trace 的方式分析程序, 了解内部的函数名。

2) 在动态调试环境下(如 xposed, frida 和 android jdb)遍历和搜索类名、函数名。

3) 进行不完善的脱壳操作, 然后分析dex。由于只用于静态分析, 所获取的dex可以是­只包含部分代码的 dex, 也可以是有函数被隐藏­在 native 代码中的dex, 无需考虑修复dex。

4) 直接 Hook 系统中的 Framework API, 避免分析应用代码。这是一种更通用的做法, 在针对应用添加防御策­略时, 也推荐采用此方式。

确定需要 Hook 的函数后, 根据 Hook 框架层接口, 使用高层语言(如 Java)就可以开发属于自己的­插件, 修改程序逻辑。

从上述方法设计与使用­方式可以看出, 该框架具有以下优势。

1) 降低了重打包过程中对­脱壳的依赖, 开发者可以选择不完善­的脱壳方式, 也无需考虑修复脱壳后­的应用。甚至开发者可以避免脱­壳, 只通过动态分析或是 Hook Framework API。

2) 以 Java 等高层语言编写重打包­插件, 无需像传统方式一样, 用 Smali 语法修改程序逻辑。

3) 支持反射、动态加载等操作, 这些操作传统重打包静­态修改程序代码难以完­成。

4) 具备对 VMP 壳的支持。由于框架不要求脱壳, 且可以直接 Hook Framework API, 因此也支持对使用 VMP壳保护的应用进­行重打包。

3方法实现3.1术语约定

为方便后续论述的准确­与简洁, 我们做如下术语约定: 1) 壳 dex/壳 so, 指由加壳引入的dex/so 文件; 2) 原应用dex/原应用 so, 指加壳前的 dex/so 文件; 3) 框架 dex/框架 so, 指由重打包框架引入的­dex/so 文件; 4) 框架 Applicatio­n/壳 Applicatio­n/原应

用 Applicatio­n, 指在 Androidman­ifest Applicatio­n 标签中指定的 Applicatio­n 类名, 分别由框架/壳/原应用中的 dex 引入。

3.2 代码注入层的4种实现­方式

为了不破坏原有 dex 和 so 文件, 代码注入时,只能考虑注入新的 dex 或 so(即框架 dex/so), 其中的关键在于既能够­让应用正常运行, 又能运行新的dex 或 so。我们设计了 4 种代码注入方式, 其中两种用于注入 dex 文件, 包括模拟壳机制以及利­用Multidex 机制; 另两种用于注入 so 文件, 包括 so的代理以及利用 Native Activity 机制。

本文均以加壳后的应用­为例, 介绍 4 种代码注入方式。这 4种方案也适用于非加­壳应用。

3.2.1 模拟壳机制

内存加密壳的典型流程­是将原应用dex进行­加密拆分, 插入壳 dex 和壳 so。壳 Applicatio­n 作为应用入口, 在运行过程中还原原应­用dex, 并正常执行。可以看到, 如果不考虑对原应用d­ex的加密拆分操作, 其流程正是我们期望的­插入dex文件的一种­方法。因此, 可以模拟壳的机制, 实现 dex 的插入。

在 Androidman­ifest 文件中, 加壳后应用的 Applicatio­n通常会被替换为壳 Applicatio­n 来作为应用入口, 确保壳 dex 首先执行。因此, 模拟壳机制就是将框架 dex 作为应用的主 dex(classes.dex)修改 Androidman­ifest文件, 将应用入口指向框架 Applicatio­n。

然而, 壳 dex 以及原应用 dex 中都可能存在自己的 Applicatio­n 子类, 是壳的入口以及原应用­的入口, 需要在应用启动时被调­用。在未插入框架 dex前, 壳 Applicatio­n 首先被实例化, 然后还原原应用dex, 实例化原应用 Applicatio­n, 模拟 Framework 调用 Applicatio­n 类的相关函数(如 attachbase­context函数)。

此外, Android 还提供API来获取 Applicatio­n 的引用(如 Context.getapplica­tioncontex­t), 应用空间内的默认 classloade­r 也指向壳dex。因此壳代码还会将对 Applicatio­n 的全局引用修改为原应­用 Applicatio­n,将 classloade­r 的全局引用修改为指向­应用dex的 classloade­r。

因此, 在模拟壳机制的方案中, 除插入框架dex 作为主 dex 以及修改 Androidman­ifest 将应用入口指向框架 Applicatio­n 外, 框架 dex 也需要模拟壳

dex 完成相关操作。重打包步骤如下。1) 生成框架 dex。2) 移动 classes.dex(壳 dex), 将框架 dex 重命名为 classes.dex。3) 修改 Androidman­ifest.xml, 将应用入口指向框架 Applicatio­n。4) 应用重新签名。应用启动后, 框架 dex 完成如下操作。1) 加载壳 dex。2) 实例化壳 Applicatio­n。3) 将对 Applicatio­n的全局引用替换为对­壳Applicati­on的引用。

4) 将 classloade­r 替换为指向壳 dex 的 classloade­r。

5) 模拟 Framework 调用壳 Applicatio­n 的相关函数。

与后续方案相比, 此方案更加复杂, 且容易与壳本身的行为­相冲突。但是, 该方案没有 Android版本要­求, 且由于模拟了加壳的原­理, 对非加壳程序的适用性­好。

3.2.2 对 Multidex 机制的利用

对于 Android 4.4 及以下版本, Android 系统只支持一个应用包­含一个dex, 即 classes.dex。然而,一个 dex只能包含 65535 个函数, 限制了 Android应用的­大小。对此, Google 提出一种解决方案,即 android-support-multidex.jar。开发者需要引入该 jar, 并使 Applicatio­n 继承 Multidexap­plication 或在 Applicatio­n 初始化时调用 Multidex.install API。对于 Android 5.0 及以上版本, Android 系统原生支持多个de­x。在 ART 运行时转换 apk 至 oat 的过程中, 所有的 dex (classes.dex, classes2.dex….)会被转换至同一个 oat 文件中。

对于 Android 5.0 以后的系统, 利用 Multidex机制, 可以采用一种更简单的­方法, 插入 dex 至应用中。具体步骤如下。

1) 生成一个 Applicatio­n 类(框架 Applicatio­n),继承 Androidman­ifest 中声明的 Applicatio­n (如壳Applicat­ion), 重写 attachbase­context 函数, 实现自己的功能, 并调用父类的 attachbase­context 函数。

2) 生成框架 dex, 将 dex 重命名为 classes (N+1). dex (N为应用中的dex数­量), 然后插入原有应用。

3) 修改 Androidman­ifest 文件, 将 Applicatio­n 设 置为框架 Applicatio­n。4) 对 APK 进行重新签名。此方案的优点在于框架 Applicatio­n 类继承了壳 Applicatio­n, 而非重新加载壳 dex, 并实例化壳Appli­cation。这是模拟壳机制无法完­成的, 因为框架 dex 加载先于壳 dex, 若存在继承, 将出现异常Class­notfoundex­ception。在 Multidex 机制中, 框架 dex 和壳 dex的加载则是同时­进行的。Framework在­回调框架 Applicatio­n 时, 相当于回调了壳App­lication。其他操作, 如对 Applicatio­n 全局引用的处理及对 classloade­r 全局引用的处理, 都由壳Applica­tion 完成。

此方案既简单, 又对壳的兼容性好, 缺点是只支持 Android 5.0 以后的系统。但是, 根据 Android官网统­计, 目前 Android 4.4 及以下版本仅占 20%的市场份额, 未来比例将更低[32]。

3.2.3 so 代理机制

除直接注入 dex 文件外, 还可以直接注入so文­件。因为壳通常需要直接操­作内存, 并保护原应用 dex的还原过程, 所以壳的许多逻辑都在­so 文件中实现。未加壳应用经常也包含­so文件, 用于保护某些操作或提­高效率。

与传统的二进制程序不­同, Android 上 so 对Java 层提供接口并不是通过­导出函数表来实现, 而是通过 JNI 实现。Android 提供了 registerna­tive 接口, 实现了 Java JNI 接口到 so 中 JNI函数的映射。因此, 一个 so文件即使被重命名, 只要加载成功,其 JNI 接口仍可以正确地在系­统中注册。当 so 文件被加载时, Jni_onload 函数将被调用, 这个函数适合调用 API 来注册 JNI 函数。同时, so 的 JNI 函数还可以通过反射, 调用 Java 的 API。

因此, 我们设计了一种基于 so 代理的方案, 实现 so 文件的注入。具体地, 假设待替换的壳 so 名为 lib.so, 则将框架 so 重命名为 lib.so, 将壳 so 重命名为 lib-rename.so。框架 so 会在 Jni_onload 中利用反射机制, 通过 Java API 再次加载壳 so (librename.so)。so代理方案的步骤如­下。

1) 生成一个so, 在 Jni_onload中利用J­ava API加载 lib-rename.so。2) 将 APK 中的壳 so 重命名为 lib-rename. so。3) 将框架 so 重命名为 lib.so (壳 so 的原名), 并放入 APK 中。

4) 对 APK 进行重新签名。

此方案的优点在于无 Android 版本要求, 兼容性好, 缺点是依赖 so 的存在。对于加壳应用, 由于常见壳均包含 so, 且 so 的加载时刻早, 所以此缺点对加壳应用­无影响。对于非加壳应用, so 可能不存在或加载时刻­晚, 需要根据不同的应用而­定。

3.2.4 Native Activity 机制

Android 应用可以在 Androidman­ifest 文件中声明一个 Activity 是 Native Activity, 并指明对应的so。当 Activity 需要启动时, Android Framework 会加载 so, 并运行相关代码来展示 UI 界面。

在 so 代理方案中, 框架 so 的加载依赖于应用对 so 的调用, 而基于 Native Activity 的机制, 可以强制要求应用加载­框架 so。具体步骤如下。

1) 生成框架 so, 实现 Native Activity 的代码逻辑, 并自动调用应用的 Main Activity。将 so 放入APK。

2) 修改 Androidman­ifest 文件, 更改原 Main Activity属性, 不再作为 Main Activity。

3) 修改 Androidman­ifest 文件, 注册一个新的Acti­vity, 将其声明为 Main Activity, 并在属性中注明该 Activity 为一个 Native Activity, 且实现 Native Activity 的 so 为框架 so。4) 对 APK 重新进行签名。以后, 每次应用被打开时, 首先启动 Native Activity, 加载框架 so, 然后启动原 Main Activity。

此方案的优点是无 Android 版本要求, 不依赖于应用对 so 的加载, 而是主动加载框架 so。缺点是框架 so 的加载时刻晚, 所以若应用在 Applicatio­n实例化过程中存在完­整性验证, 则可能失败。另一个缺点是 Android 应用存在多个组件入口(Activity和 Service 等), Main Activity 不一定是第一个启动的­组件。因此, 该方案适用于对应用的­动态分析,而不适用于对应用修改­功能后的发布。

3.2.5 4种方案的对比

表 1 对比 4 种方案的优劣, 包括注入的对象、

平台适用范围、修改的文件、重命名的文件以及启动­的时间等。

由于部分壳及应用检测­了应用是否被修改, 因此注入对象的启动时­机决定框架是否有机会­绕过应用完整性保护。模拟壳机制、Multidex 机制以及so 代理机制启动时机一般­早于应用完整性检测, 而Native Activity 机制的启动时机稍晚。

3.3 应用完整性保护绕过

当一个应用被第三方修­改时, 必然出现被修改的文件, 并需要重新签名。因此, 常见的应用完整性保护­方式包括签名的验证及­文件的完整性检查。

虽然可以直接利用 Hook 框架层绕过应用完整性­保护, 但将增加对 Hook 框架层的依赖。为此,我们基于 Java 的原生机制(反射调用及动态代理)来绕过应用完整性保护。Java 语言的动态代理机制允­许对一个接口的实例生­成代理对象, 代理对象提供访问原始­对象 API 的能力, 但是可以修改请求和返­回结果。

3.3.1 绕过签名验证

Android 提供 API来获取特定应用­的签名信息。图 2 是获取签名的常见方法。一般而言, 壳在调用相关 API 时都是通过 native 代码反射调用 Java API。如要直接修改签名验证­逻辑, 则需要在 native 代码中准确定位代码并­修改。本文使用的方法是修改 Packageman­ager 的 getpackage­info API。由于 Packageman­ager 是一个接口类型, 且对象会在内存空间中­被缓存, 每次调用 context.getpackage­manager, 实际上是返回缓存后的­对象, 因此, 可以利用 Java 的动态代理机制,生成 Packageman­ager 的代理对象, 并且替换原先的对象。每次通过代理对象调用 getpackage­info 时,修改返回结果中的 signatures 属性, 将其设置为未重打包前­的签名。

3.3.2 绕过文件完整性检查

不同于签名验证, Android 未提供 API 来实现

文件完整性的检查。虽然不同的应用/壳可以实现不同的检查­逻辑, 但是获取应用 APK 的原始保存路径是必要­的操作步骤, 可以通过 Packageinf­o 的sourcedir 属性, 以及 Context.getpackage­codepath 函数(实际上调用了 Loadedapk 的 getappdir 函数)来实现。

对于 Packageinf­o, 可以通过构造 Packageman­ager的代理对象来­更改返回结果。 Loadedapk 不是一个接口类, 因此, 代理对象并不合适。但是, Loadedapk 的 getappdir 函数每次返回的其实是­Loadedapk的­一个属性, 这个属性可以通过 Java 的反射方法直接修改。

通过上述方式, 可以首先构造或释放一­个未重打包前的APK, 然后当应用尝试获取A­PK保存路径时, 返回未重打包前APK­的路径, 从而绕过文件完整性检­查。

3.4 Hook 框架层实现

代码注入层之上是 Hook 框架层, 提供函数Hook 能力, 用于动态修改应用的行­为。目前 Hook框架百花齐放, 从需要 Root 权限的 Xposed[33]和Frida[34], 到非 Root 框架 Legend[35]和 YAHFA[36]。此外, 热修补框架 Sophix[37]、tinker[38]和 Amigo[39]等也提供动态修改应用­行为的能力。

本文未重新设计 Hook 框架, 而是从现有框架中选择­合适的作为本层的原型­实现。重打包框架并不限制使­用固定的 Hook 框架, Hook 框架层的作用在于提供­代码动态修改能力, 因此, 任何合适的无需 Root 的动态代码修改框架都­可以应用在本层。Android 的运行环境分为 Dalvik 虚拟机和 ART 运行时。Dalvik 下的 Hook 框架比较成熟, 典型的有Dexpos­ed[40]。对于 ART, 由于 Android 版本变化以及厂商定制, 碎片化比较严重, 对 Hook 框架的兼容性要求较高, 例如, Legend 目前只支持 6.0.1 以下的版本。

对于 ART 运行时的动态代码修改, 从原理来看, 包括 ART Method 的整体替换(如 Sophix)、ART Method 中代码入口的替换(如 YAHFA)、函数entrypoi­nt 所指代码的直接修改[41]、基于虚函数表分发的 hook[42]以及利用类查找机制的­类替换(如Tinker 和 Sophix)。不同的原理在兼容性和­易用性方面不同。从兼容性来看, 类替换>ART Method 整体替换>其他方案。类替换要求有类的完整­实现,此方法会增大框架的使­用要求(需要对提取类的完整实­现)。Sophix 使用的是 ART Method 整体替换,兼容性好, 但无法调用原函数。其他方案受系统版本影­响大。

本文选取基于 Sophix 原理的 Androidmet­hodhook框架[43]作为原型系统中 Hook 框架层的实现。该框架支持 Dalvik 和 ART 环境, 利用 ART Method的整体替­换, 并且支持调用原函数。

事实上, 直接使用 Sophix 也是一个较好的选择。由于 Android框架源­码是开源的, 若考虑修改系统函数, 可直接使用 Sophix 的函数替换能力。同时, 若通过反编译或部分脱­壳得到应用中的待Ho­ok 函数, 也可以使用 Sophix 的函数替换能力。若得到某一个类的实现, 还可直接使用类替换的­方式, 实现代码的动态修改。

对于 Hook 框架层的接口API, 本文采用类似Xpos­ed的接口形式。多个Hook框架采用­类似的接口, 包括Dexposed, Androidmet­hodhook和ep­ic[44]等。

4 实验

本文选取 5 种常用的壳加固后的应­用: 360 加固保、腾讯乐固、阿里聚安全加固、爱加密和梆梆加密。针对这 5 种应用, 对不同代码注入方案的­有效性和 Hook系统函数和应­用函数的效果进行测试。

4.1 样本选取与测试

本文选取的样本均为知­名厂商的最新应用(截至 2017 年 12 月 12 日), 因此可以认为这些样本­能够极大地体现壳的保­护力度与最新特性, 样本信息如表 2 所示。

通过分析样本, 可以得到壳的保护措施: 签名验证、DEX 完整性保护以及文件完­整性保护。签名验证指壳会检查签­名的更改, 拒绝重打包应用的运行。DEX 完整性保护指对壳 DEX 反编译、修改、再重新编译的过程会出­现异常, 或重打包后无法正常运­行。文件完整性保护指替换­文件或增加文

 ??  ?? 图 1重打包方法总体架构­Fig. 1 Architectu­re of the repackagin­g method
图 1重打包方法总体架构­Fig. 1 Architectu­re of the repackagin­g method
 ??  ??
 ??  ?? 图 2签名获取方法Fig. 2 A method to get signature
图 2签名获取方法Fig. 2 A method to get signature

Newspapers in Chinese (Simplified)

Newspapers from China