聊聊Android的ART运行时:借由ART构建用户态rootkit

华盟原创文章投稿奖励计划

作为大自然的搬运工,啊不是,西方往东方的搬运工,我在常年搬运着一些有趣的研究内容,从半导体到光学,从硅到玻璃(主体还是在搬运半导体行业),现如今也在搬运着安全行业的研究(还有FreeBuf安全快讯中的各路国家大事),供各位分享评述。除了偶尔表述些想法,主体上还是贡献一些语言的力量。这个世界有趣的东西有很多,比如Kirin 950可能是有史以来效能最好的麒麟芯片;比如Windows 10中藏着一个Linux内核;再比如Android现在所用的ART(Android Runtime)运行时也对Android安全产生了一些影响。

这篇文章实际上是IBM X-Force高级研究实验室的Paul Sabanal发表的一篇论文,原文题为《Hiding behind ART》,似乎是2015年Black Hat大会的议题之一。如果你对Android的ART运行时不熟,下面也会有些简单的介绍。期望这份研究对各位有帮助,毕竟ART虽然不是什么新东西,在当年的Android 4.4(Kitkat)/5.0(棒棒糖)也算是相当大的改动,我觉得也很有意思。

就我个人来说,这份研究谈具体实现的部分过于枯燥(因为我对实操的确是毫无兴趣),不过无耐这也是这份研究的核心所在。如果要概括研究方向的话,大致上是替代ART生成的的原生代码,构造用户态的rootkit。

稍稍了解过ART运行时的人就知道,ART相较Dalvik的改进主要体现在性能方面,就是每款App在安装时就提前编译为原生代码(实际情况不止于此),最终生成OAT文件。如果能将OAT文件中的代码换成恶意代码,隐藏自身并且实现持久感染,感觉就会比较有意思了。当然期间还是要解决很多实际的问题的。

比如说,即便不考虑如何修改OAT文件的问题,这种修改也无法随心所欲地进行,如ART会检查相应DEX文件的CRC32校验和,如果检查发现不对的话,ART就会重新从原有JAR中再度生成OAT。那么起码得保证这些细节能过得了关。再比如说如果我们替换App的OAT文件,则除非系统升级或者App更新,否则OAT文件就不会被重新编译替换,这基本上也就实现了“持久性”感染。

另外下面介绍的这些方案也存在局限性,比如如果你要改某个App编译后的原生代码,或者说将其中的原生代码替换成我们自己的代码,则其操作权限也仅限于这个App本身的权限。

尤为值得一提的是,这篇paper发表的时间比较早:是2015年,我并不知道其后的Android 6.0棉花糖和Android 7.0牛轧糖是否已经增加安全特性来杜绝这份研究提到的攻击方案。

不过至少,这份研究对ART做了些更为具体的普及,主要是思路分享,而且我向来也不是个实用派。下面是针对这篇paper的全文编译。个人水平有限,若有错误,欢迎指出。

45A5CEF6-41FB-46D1-A083-F17C17A306EE.png

摘要

新版Android Runtime(ART)的引入,为Android带来了不少新的变化。不过就像许多新技术的引进一样,随这些新技术一起来的是更强的恶意行为。在这篇文章中,我们会从其中一个角度来聊聊。

一名攻击者,或者一个恶意程序一旦获得了Android设备的访问权,下一步就是找办法来隐蔽自身,并实现持久感染——一般来说这一点是通过安装rootkit实现的。绝大部分rootkit都是内核态的rootkit,要实现持久的感染一般是通过篡改系统分区的文件实现的。不过Android系统近期对于安全的一些提升,比如说verified boot认证启动,都让上述过程变得越来越有难度。这份研究会演示,如何在内核态和系统分区范围之外来搞定问题。文章将展示如何利用ART机制来构建用户态rootkit。

我们首先会探讨先前的Android rootkit研究,以及在当代Android系统中这些技术要实现起来为什么变得越来越困难。随后我们会深入到ART内部,讨论文件格式与相关rootkit构建的机制。在理解相关机制之后,我们再探讨构造rootkit的方法,比如在实现系统的持久感染问题上,要怎么做。同时我们也会探讨这种方式存在的局限性,及未来可以进一步开展哪些工作。

介绍

Android先前有个安全特性提升,名为dm-verity。这项特性最早是在Kitkat系统中引入的,让内核去确认启动部分的完整性,确认这部分未被篡改,保护设备不会受到rootkit影响。rootkit一般会在系统分区中添加或者篡改二进制文件,来维持感染的持久性。要了解这项特性,可前往参考Nikolay Elenkov的文章。

这份研究的目标之一,是探讨安装rootkit的攻击者,是否能在不考虑dm-verity的情况下给系统安装rootkit。虽然截止到文章撰写之前,dm-verity还不是系统的默认特性(译者注:据我所知,从Android 7.0牛轧糖开始Verified Boot已经成为强制流程;这篇论文的撰写比较早,那个时候7.0尚未发布),但率先了解攻击者的行动也是件好事。我们需要了解,在不触碰系统分区的情况下,是否有可能执行rootkit操作,这样就避开了dm-verity的防护。

除非特别声明,这篇研究报告中所述的技术思路,首先假定攻击者已经在目标设备上获得了root shell访问(”soft root”)权限,攻击是在Nexus 7 2012 Wifi(”grouper”)平板上进行的,平板预装原生Android 5.1系统。

ART简述

在我们讨论rootkit之前,首先来了解一下ART架构和机制。不过这里我们不会深入到ART编译与代码生成的深度细节中。如果说读者对Android系统原本就比较熟悉,阅读本文也会比较轻松。先前Dalvik、DEX文件格式和其它概念方面的知识储备,对于理解这部分也是有帮助的。如果读者对于这些概念并不熟悉,我们强烈推荐以下阅读:

The Android Hacker’s Handbook by Joshua Drake, et al.

Android Internals by Jonathan Levin.

Android Security Internals by Nikolay Elenkov.

Embedded Android by Karim Yaghmour.

Official Android documentation.

ART的实验版本最早是在2013年10月份的Kitkat系统中引入的,当时用户还可以在ART和Dalvik运行时之间进行选择。从棒棒糖系统开始,ART就已经成为默认的运行时。ART相较Dalvik的最大优势在于更好的系统性能,因为提前进行了编译。

提前编译

Dalvik是依赖于翻译和JIT编译的,而ART则将应用Dalvik字节码预编译为原生代码。

每次设备系统升级或者在首次启动时,所有应用都会编译。而每个应用则在安装时就进行编译。

负责将应用编译进OAT的命令为dex2oat,位置在 /system/bin/dex2oat ,支持两种类型的编译器后端:quick和portable。通过 -compiler-backend 参数可指定后端。

CF7EA33F-F400-40E7-B23F-E6A85D221BD4.png

Quick编译

默认后端为Quick,它将Dalvik字节码(MIR,medium level intermediate representation)译为LIR(低层级IR),再转为原生代码,期间会进行一些优化。

而Portable后端则采用LLVM作为其LIR。优化是通过LLVM优化器进行的,代码生成则由LLVM后端完成。

WX20170623-181322@2x.png

最终的OAT文件会在 /data/dalvik-cache/<arch> 文件夹中生成,这里arch为编译目标的架构(如设备架构)。

要了解ART的更多细节,可点击这里查看谷歌的解释。

ART镜像文件格式

镜像文件(boot.art)包含来自框架JARS的预初始化类和对象。镜像文件就位于内存boot.oat之前。已编译OAT中的代码直接与该镜像关联,来调用框架中的方法(译者注:这里的方法就是methods,类方法,当下文再度出现class’s methods时,会特别再用英文注释;也有人将之译作类函数;如果出现“方法”时并未注释英文,则这个地方的方法一般是指approach)或者访问预初始化的对象。

Image Header

WX20170623-181553@2x.png

以 magic “art\n” 开头的镜像 header 后跟 version 版本,截止到撰文之时版本号是 “012″ 。其后跟描述关联OAT文件 (boot.oat) 的字段。patch_delta 则是镜像基址迁移(OAT header部分提到的)的量。image_roots 是需要重新初始化的对象数组地址。

OAT文件格式

下面这部分描述OAT文件格式,讨论内容也包括ELF和DEX文件格式。这部分内容的撰写,已假定读者对这两者本来就已经比较熟悉。如果你并不熟悉这两种文件格式,则推荐阅读这两份文档:ELF, DEX 。这部分的所有信息都可以在AOSP源码art文件夹中找到。相关文件包括:

• dex2oat/dex2oat.cc

• runtime/oat.h

• runtime/oat.cc

• runtime/oat_file.h

• runtime/oat_file.cc

• runtime/image.h

• runtime/image.cc

OAT文件实际上就是ELF共享对象文件,其中包含OAT数据的不同组成部分。OAT数据包含描述OAT文件结构的header,以及DEX代码和编译的原生代码。ELF文件有3个动态符号表,分别名为oatdata, oatexec以及oatlastword。这几个项目提供的信息在于,不同组成部分包含的相应OAT数据。下面这张表是.oat ELF文件中动态符号示例:

WX20170623-181634@2x.png

这几个部分的情况分别是这样的:

• oatdata – 包含OAT header和插入的原有DEX文件;

• oatexec – 包含针对已编译方法(method)生成的原生代码;

• oatlastword – 这个实际上是end marker末尾标记,包含生成原生代码的最后4个字节;

符号表项目的sym_value字段指出了每部分的位置。或者也可以在 .rodata 部分对oatdata进行定位,oatexec 和 oatlastworkd 则可以在 .text 部分中找到。这几个部分是挨着的,本文接下来的部分将其作为单独的对象(blob)讨论。下面header中的所有offset(如excutable_offset, code_offset等)都相关此blob的开头部分。

OAT header

WX20170623-181813@2x.png

WX20170623-181835@2x.png

OAT header描述了OAT数据的整体结构;起始为magic字段 “oat\n” ,后跟OAT文件格式现有版本,这是的版本号是 “045\0″。adler32_checksum就是OAT header字段的校验和。instruction_set字段则指明了编译目标设备的指令集架构,支持的架构包括有:

WX20170623-181948@2x.png

dex_file_count是输入APK或JAR中的DEX文件数量。executable_offset指明了生成的原生代码部分(和ELF动态符号表中的oatexec部分一样)。image_patch_delta则是相关image_begin字段ART镜像(boot.art)迁移的量。该字段每次启动时都会变化,所以ART镜像的地址并不固定。在棒棒糖系统之前,此基址固定在0×70000000,所以是有可能搞定ASLR或者拿下oatexec部分(包含大量原生代码),用以发送ROP攻击的。key_value_store是个字典,存储有关OAT文件的元数据,比如dex2oat创建时使用的参数。其余的字段(*_trampoline_offlet, *__bridge_offsec等)则用于运行时,通常设定为0。

OAT Dex File Header

WX20170623-182137@2x.png

接下来的Oat Header是OatDexFileHeaders数组,每个项目代表目标APK或JAR文件中的每个DEX文件。dex_file_location_data字段包含了该OAT文件编译源APK的路径。dex_file_location_checksum是此DEX文件的CRC32校验和;用于确认dex_file_location_data的APK和该OAT文件为相同的DEX。整个DEX文件也可以在dex_file_pointer指向offset的oatdata部分中找到。要从OAT中获取DEX文件,就可以到这个位置,从(dex_file_pointer + 0×20) 获取DEX文件尺寸,再获取之。classes_offsets是OatClass header的offset数组,如下表所示。每个类offset偏移对应DEX文件中的一个class_def_item,顺序相同。

OAT Class header

WX20170623-182358@2x.png

OatClass包含有关类的信息。status字段在编译期间使用,type字段指明了类方法(method)编译状态,如下面这张表所示,kOatClassAllCompiled表明此类中的所有方法(method)都已编译。

WX20170623-182550@2x.png

bitmap字段,表达的是长度bitmap_size字节的bitmap(译者注:这句话翻译得很无语,我仔细斟酌了很久;这句话描述了bitmap这个字段,该字段本身就是个bitmap,就是位构成的map,但我们觉得翻译成位图是不妥的,所以保留了原意),每个位表达某个方法(method)是否已编译。每个位都对应于该类的一个方法(method)。如果type字段为kOatClassAllCompiled或kOatClassNoneCompiled,那么bitmap_size和bitmap字段就不存在,type字段后会跟method_offsets字段。如果type字段为kOatClassSomeCompiled,则意味着至少一个但并非所有方法(method)已编译。在这种情况下,method_offset字段就会跟在bitmap后面。bitmap中的每一个位,都对应于该类中的一个方法(method) – 首先是direct_methods,然后是virtual_methods。其顺序和该类class_data_item中的一样。对每个设置位而言,method_offsets字段都有对应的条目。

method_offsets是个offset偏移列表,针对每个已编译方法(methods)指向生成的原生代码。值得一提的是,OTA文件OATHeader->instruction_set为kThumb2(绝大部分遇到的OAT文件都可能是这样)(译者注:指令集?kThumb2貌似是目标设备指令集取值),该方法(method)偏移会有其最低位设定。比如说如果offset为0×00143061,则原生代码的起始位置在offset 0×00143060。

Oat Quick Method Header

方法(methods)的原生代码前面(code_offset – 0x1c字节)就是QuatQuickMethod header,这是针对Quick后端编译代码生成的。其中包含如帧大小,以及原生代码与Dalvik字节代码中寄存器和指令指针的映射(译者注:我不能确定这里的register是否是指寄存器,原文 mapping between registers and instruction pointers in the native code and Dalvik bytecode);也包含生成原生代码的尺寸。

WX20170623-182652@2x.png

用户态Rootkit

方法是使用dex2oat,由已安装App或者系统框架的修改版本,生成OAT文件,然后用它来替代原有的OAT文件。我们有两个选择:

1.生成新的boot.art和boot.oat,文件中包含我们自己的代码,将之替换已安装的boot.oat;

2.生成新的OAT文件,其中针对某个已安装应用包含了我们自己的代码,然后替换已安装的OAT文件。

这种方法有不少好处。其一是我们不需要处理低层级代码。所有的修改都是在Java中完成的,而且只在用户态运行,相比与更低层级的内核作交互,面临的问题也更少;另外就是架构和操作系统版本方面的变数也更少,因为我们是依赖于ART特性来构建代码的。最后,因为应用已经安装和认证,那就不需要管代码签名方面的问题了。我们要做的就是修改应用代码,这部分代码早已是应用package之外的范畴了。

值得一提的是,不管我们采用何种方法,我们的代码都只在相应App的环境下运行。也就是说在App运行我们的代码时,代码也采用相同的用户id和应用权限。比如说,如果我们用某个App OAT替换方法,替换“设置”App的OAT,那么我们的代码就运行在系统用户环境下,App权限也就是“设置”App的权限。

那么感染的持久性问题呢?(译者注:persistence这个词在恶意程序分析中出现得比较频繁,通常是指要让这个恶意程序常驻系统中——举个通常的例子,一般的Mirai变种其实就不具备这种persistence,因为一旦IoT设备重启,Mirai也会跟着被清理掉;这里我们倾向于将persistence解释为感染持久性,或令恶意程序持久感染;而在OAT文件篡改的persistence问题上,具体的内容请看下一段)

只要我们修改过的OAT文件还在用,那么这种修改就是有效的。OAT文件仅在系统升级或者App更新之后才会被替换掉。在系统更新之后,boot.art和boot.oat会重新生成,所有App的OAT文件也需要重新编译。某款App更新的话,也是需要重新编译的。请注意,我们的目标不是要维系root访问,刚开始我们就说了这里介绍的方法就是要避开写入到 /system 部分中。至于如何再度root exploit,这里就不作解释了。

替换Boot OAT

框架代码都编译进了单独的boot.oat文件中,这是前提条件。dex2oat工具生成代码,所以我们不需要担心修改二进制程序时会产生混乱。对应的boot.art也会生成。

那么我们要做的就是修改系统框架JAR文件了,用我们自己的代码来替换目标代码,然后用dex2oat来生成新的boot.oat,然后替换掉原始版本。

rootkit的一大目标就是隐藏已安装的恶意应用或进程,这里给出一些修改的例子:

WX20170623-183032@2x.png

WX20170623-183100@2x.png

看一个例子,修改框架方法(method)来隐藏运行进程。我们可以修改ActivityManager类的getRunningAppProcesses()函数。该方法(method)返回RunningAppProcessInfo列表,其中包含有关运行进程的信息(包括name)。比如说,“设置”App就用这个方法(method)来枚举设备上运行中的进程,并在列表中展示出来。下面就是该方法(method)的代码,可在 “/frameworks/base/core/java/android/app/ActivityManager.java” 中找到:

WX20170623-183226@2x.png

根据其package名,我们对其进行修改,将我们的App从列表中移除:

WX20170623-183406@2x.png

在构建这部分代码后生成修改版本的framework.jar。我们不应直接将之作为dex2oat的源JAR,因为和其它文件不匹配可能会导致不可预知的错误。我们从设备中获取原版JAR,然后在其中应用我们所做的修改。

然后我们用Apktool将该JAR译码为smali代码,获取smali代码:

WX20170623-183450@2x.png

WX20170623-183537@2x.png

WX20170623-183614@2x.png

WX20170623-183716@2x.png

然后我们用修改版本来替换framework.jar中的代码。我们从设备中获取JAR然后用Apktool将其译码为smali。然后寻找目标方法(method),原本是这样的:

WX20170623-183857@2x.png

然后用上面我们自己的修改版本来替换smali代码,用Apktool来重新构建JAR。

接下来的一步就是将修改版的JAR放到设备中,用dex2oat对其进行编译了。不过在此之前还有件事要做。ART会检查每个DEX文件的OatDexFile header,查看dex_file_location_data和dex_file_location_checksum,是否与JAR安装位置和对应DEX文件的CRC32匹配。如果匹配失败,ART会重新从设备原有JAR中生成OAT文件。所以,我们就需要来改改dex_file_location_data和dex_file_location_checksum了。

这项工作其实是可以自动化进行的,不过在此我们手动操作。这里假定你已经有了目标方法(method)的修改版smali代码:

1.从 /system/frameworks/ 文件夹获取原版JAR;

2.用Apktool对JAR进行译码,生成smali代码;

3.用我们的修改版来替换目标方法(method);

4.利用Apktool来重新构建JAR;

5.重命名JAR,这样一来在将其放到设备里面之后,和原有 /system 分区的路径长度就一致了。比如说,如果你是修改 “/system/framework/framework.jar”,长度31个字符,那么将其修改为 11framework.jar ,然后再放到 /data/local/temp ,最终也就是 “/data/local/tmp/11framework.jar” ,也就是31个字符长度。这样一来后面修改生成的OAT时,就无需考虑偏移了。

6.获取原有framework.jar中classes.dex文件的CRC32,后面会用到;

7.删除原有的boot.oat;

8.用原有boot.oat中所用命令行来运行dex2oat命令(可以在OAT header的key_value_store字段中获得),用修改版的framework.jar。下面是示例:

WX20170623-183943@2x.png

9.boot.oat生成以后,在OAT DEX File header中修改dex_file_location_data。以framework.jar为例,有两处要改。一个是主DEX,另一个是第二个DEX(classes2.dex),替换 dex_file_location_checksum,可在路径后面找到,改为步骤6中获取到的校验和。

10.重启 Zygote,或者重启设备

WX20170623-184017@2x.png

11.如果一切顺利,已安装的App就会编译为进新的OAT,因为boot.oat已经发生变化,变化也会生效。

替换App OAT

这部分的讨论,是替换到某个特定App的OAT。这种方式产生的影响更小,可能产生问题的概率也更小,因为仅限于某个特定的App。不过一旦App更新,修改就会失效——相较boot.oat的修改,App更新显然更加频繁,所以替换App OAT可能在持久性方面没那么好。

以Android系统中的“设置”应用为目标,可查看运行进程和已安装的应用。原有APK的位置在 “/system/priv-app/Settings/Settings.apk”。源码也可以在AOSP源代码树 “packages/apps/Settings” 中找到。从中寻找使用目标方法(method)的代码,然后修改这部分代码。比如说,可以找一找调用 getRunningAppProcesses() 的代码,修改返回的RunningAppProcesssInfo列表。下面是来自 “packages/apps/Settings/src/com/android/settings/applications/RunningState.java” 的代码:

WX20170623-184041@2x.png

修改调用 getInstalledApplication() 的代码,从已安装应用列表中移除我们的目标App。下面的代码来自 “packages/apps/Settings/scr/com/android/settings/applications/ApplicationState.java”

WX20170623-184135@2x.png

具体的步骤和上面的方法类似,略有些差异:

1.获取原版APK;

2.用Apktool来解码APK,生成smali代码;

3.修改目标方法(method);

4.用Apktool重新构建APK;

5.给APK重命名,令其最终的路径长度和原有APK路径长度一致。比如 “/system/priv-app/Settings/Settings.apk” 长度38个字符;那么重命名APK为1111111111Settings.apk,然后放到 /data/local/temp,最终路径也就是 “/data/local/tmp/1111111111Settings.apk” ,刚好就是38个字符;

6.计算原版APK中classes.dex的CRC32校验和,后面会用到;

7.从该App中删除原有OAT文件;

8.运行dex2oat命令,根据我们修改的APK路径设置-dex-file参数,原有OAT文件路径设定 -oat-file参数。例如:

WX20170623-184211@2x.png

9.OAT文件生成后,用原有APK路径来修改OAT DEX File header中的 dex_file_location_data。用步骤6中计算得到的原有校验和来替换 dex_file_location_checksum;

10.若该App在运行,则停止其进程:

WX20170623-184257@2x.png

11.应用再次运行时,修改就会生效。

其它可能的攻击方式

我们刚开始进行这项研究的时候,想打的主要方法是在修改boot.oat二进制程序或者依附于Zygote直接在内存中修改boot.oat。这种方案是hook生成的原生代码方法(method),将执行转移至我们自己的代码。当时看来这是比较可行的方案,但实际上却难度甚大,而且很不稳定。不过我们认为这种方案值得深入研究,当前我们还在进行研究。

局限性

本文所述的方案存在一些局限性。其一在于对于更低层级的代码,或者不使用系统框架来展示内容的代码,上述隐藏方式是无效的。而且如果设备策略阻止上述步骤,则SELinux自然有可能令上述方案无法施展。不过如果我们能设定SELinux为允许模式(即便为临时设定,或者在重启后就回到强制模式),修改就能生效。最后,局限性还在于这种技术受限于该App本身的权限,不过用恶意程序来搞这一套应该也可以。

终话

在这份研究中,我们演示了通过替换ART生成的原生代码,构建用户态rootkit的可能性。这些技术仍然存在局限性,而且ART还在持续开发中,所以未来上面提到的方案可能会失效。不过这原本就是安全研究需要承担的挑战,所以期待我们持续在这个领域的研究。

当前针对ART公开的安全研究很少,不过我们期望未来会有很多相关内容涌现。我们期望这份研究能够帮助读者理解ART及对安全带来的影响,也期望这项研究能够为其它同人提供思路,了解ART带来相关安全的更多可能性。

* 参考来源:《Hiding behind ART》,本文作者:欧阳洋葱,转载请注明作者与出处

本文原创,作者:小龙,其版权均为华盟网所有。如需转载,请注明出处:https://www.77169.net/html/149939.html

发表回复