引用
作者:刘朋,NICP国家安全重点实验室成员,CSDN博客专家,2016年博客之星。“白帽子”,如今已经向多家手机厂商,包括Google小米、魅族等提交高危漏洞。另外也主导开发了两款App,其中主导开发“妙趣剪纸”已登录苹果体验店。
声明:本文为
《程序员》原创文章,未经允许不得转载,更多精彩文章请订阅
2017 年《程序员》
【CSDN 有奖征稿啦】技术之路,共同进步,有优质移动开发、VR/AR/MR、物联网原创文章欢迎发送邮件至
mobilehub@csdn.net。
Android系统服务在为用户提供便利的同时,也存在着一些风险。在使用系统服务的过程中,异常的外部数据,有可能会导致系统服务崩溃,甚至是远程代码执行,内存破坏等严重后果。Android系统服务的安全问题需要重视。
Android系统服务即由Android提供的各种服务,比如WiFi、多媒体、短信等,几乎所有的Android应用都要使用到系统服务。但系统服务也并非绝对的安全,例如一个应用获取到了系统服务中的短信服务,那么就可能会查看用户的短信息,用户隐私就有可能暴露。
在工作中我们发现主要的漏洞和攻击包括特权提升攻击、恶意软件攻击、重打包、组件劫持攻击等类型。尽管安全研究人员已经针对Android上层App的漏洞挖掘做了大量的工作,但是针对Android系统服务的漏洞挖掘一直被安全人员所普遍忽视。
通过Binder机制可以对Android的系统服务漏洞进行深入的挖掘。本文基于Android的Binder机制编写了一套漏洞挖掘框架。下面我们首先介绍一下基础知识。
基础知识
Android的Binder机制
Binder其实也不是Android提出来的一套新的进程间通信机制,它是基于OpenBinder来实现的。Binder是一种进程间通信机制,它是一种类似于COM和CORBA分布式组件架构,提供远程过程调用(RPC)功能。
那么何为Binder?
- 直观来说,Binder是Android中的一个类,它继承了IBinder接口;
- 从IPC角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;
- 从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager等)和相应ManagerService的桥梁;
- 从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当你Bind Service的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
在Android系统的Binder机制中,由系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间,如图1所示。Binder就是一种把这四个组件粘合在一起的粘结剂,其中核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Service Manager和Binder驱动已经在Android平台中实现好,开发者只要按照规范实现自己的Client和Server组件就可以了。
图1 Binder架构图
为什么使用Binder
Android中有大量的CS(Client-Server)应用方式,这就要求Android内部提供IPC方法,而Linux所支持的进程通信方式有两个问题:性能和安全性。
目前Linux支持的IPC包括传统的管道,System V IPC(消息队列/共享内存/信号量),以及socket,但只有socket支持Client-Server的通信方式,由于socket是一套通用的网络通信方式,其传输效率低下有很大的开销,比如socket的连接建立过程和中断连接过程都是有一定开销的。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。
在安全性方面,Android作为一个开放式,拥有众多开发者的的平台,应用程序的来源广泛,确保智能终端的安全是非常重要的。终端用户不希望从网上下载的程序在不知情的情况下偷窥隐私数据,连接无线网络,长期操作底层设备导致电池很快耗尽等。传统IPC没有任何安全措施,完全依赖上层协议来确保。首先传统IPC的接收方无法获得对方进程可靠的UID/PID(用户ID/进程ID),从而无法鉴别对方身份。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志。使用传统IPC只能由用户在数据包里填入UID/PID,但这样不可靠,容易被恶意程序利用。可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。比如命名管道的名称,system V的键值,socket的IP地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。
基于以上原因,Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。
Aidl机制
AIDL(Android Interface Definition Language)是一种IDL 语言,用于生成可以在Android设备上两个进程之间进行进程间通信(interprocess communication,IPC)的代码。如果在一个进程中(例如Activity)要调用另一个进程中(例如Service)对象的操作,就可以使用AIDL生成可序列化的参数。
AIDL IPC机制是面向接口的,像COM或CORBA一样,但是更加轻量级。它是使用代理类在客户端和服务端传递数据。只有你允许客户端从不同的应用程序为了进程间的通信而去访问你的service,以及想在你的service处理多线程。如果不需要进行不同应用程序间的并发通信(IPC),或者你想进行IPC,但不需要处理多线程的。使用AIDL前,必须要理解如何绑定service。
AIDL IPC机制是面向接口的,像COM或Corba一样,但是更加轻量级。它是使用代理类在客户端和实现端传递数据。
Fuzz技术
模糊测试定义为“通过向应用提供非预期的输入并监控输出中的异常来发现软件中的故障(faults)的方法”。典型而言,模糊测试利用自动化或是半自动化的方法重复地向应用提供输入。显然,上述定义相当宽泛,但这个定义阐明了模糊测试的基本概念。
用于模糊测试的模糊测试器(fuzzer)分为两类:一类是基于变异(mutation-based)的模糊测试器,这一类测试器通过对已有的数据样本进行变异来创建测试用例;而另一类是基于生成(generation-based)的模糊测试器,该类测试器为被测系统使用的协议或是文件格式建模,基于模型生成输入并据此创建测试用例。这两种模糊测试器各有其优缺点模糊测试各阶段采用何种模糊测试方法取决于众多因素。没有所谓的一定正确的模糊测试方法,决定采用何种模糊测试方法完全依赖于被测应用、测试者拥有的技能,以及被进行模糊测试的数据的格式。但是不论对什么应用进行模糊测试,不论采用何种模糊测试方法,模糊测试执行过程都包含相同的几个基本阶段。
确定测试目标
只有有了明确的测试目标后,我们才能决定使用的模糊测试工具或方法。如果要在安全审计中对一个完全由内部开发的应用进行模糊测试,测试目标的选择必须小心谨慎。但如果是要在第三方应用中找到安全漏洞,测试目标的选择就更加灵活。要决定第三方应用模糊测试的测试目标,首先需要参考该第三方应用的供应商历史上曾出现过的安全漏洞。在一些典型的安全漏洞聚合网站如 SecurityFocus 18和Secunia 19上可以查找到主要软件供应商历史上曾出现过的安全漏洞。如果某个供应商的历史记录很差,很可能意味着这个供应商的代码实践(code practice)能力很差,他们的产品有仍有很大可能存在未被发现的安全漏洞。除应用程序外,应用包含的特定文件或库也可以是测试目标。
如果需要选择应用包含的特定文件或者库作为测试目标,你可以把注意力放在多个应用程序之间共享的那些二进制代码上。因为如果这些共享的二进制代码中存在安全漏洞,将会有非常多的用户受到影响,因而风险也更大。
确定输入向量
几乎所有可被利用的安全漏洞都是因为应用没有对用户的输入进行校验或是进行必要的非法输入处理。是否能找到所有的输入向量(input vector)是模糊测试能否成功的关键。如果不能准确地找到输入向量,或是不能找到预期的输入值,模糊测试的作用就会受到很大的局限。有些输入向量是显而易见的,有些则不然。寻找输入向量的原则是:从客户端向目标应用发送的任何东西,包括头(headers)、文件名(file name)、环境变量(environment variables),注册表键(registry keys),以及其他信息,都应该被看做是输入向量。所有这些输入向量都可能是潜在的模糊测试变量。
生成模糊测试数据
一旦识别出输入向量,就可以依据输入向量产生模糊测试数据了。究竟是使用预先确定的值、使用基于存在的数据通过变异生成的值、还是使用动态生成的值依赖于被测应用及其使用的数据格式。但是,无论选择哪种方式,都应该使用自动化过程来生成数据。
执行模糊测试数据
紧接上一个步骤,正是在里“模糊测试”变成了动词。在该步骤中,一般会向被测目标发送数据包、打开文件、或是执行被测应用。同上个步骤一样,这个步骤必须是自动化的。否则,我们就不算是真正在开展模糊测试。
监视异常
一个重要但经常容易被忽略的步骤是对异常和错误进行监控。设想我们在进行一次模糊测试,我们向被测的Web服务器发送了10000个数据包,最终导致了服务器崩溃。但服务器崩溃后,我们却怎么也找不到导致服务器崩溃的数据包了。如果这种事真的发生了,我们只能说这个测试毫无价值。模糊测试需要根据被测应用和所决定采用的模糊测试类型来设置各种形式的监视。
判定发现的漏洞是否可能被利用
如果在模糊测试中发现了一个错误,依据审计的目的,可能需要判定这个被发现的错误是否是一个可被利用的安全漏洞。这种判定过程是典型的手工过程,需要操作者具有特定的安全知识。这个步骤不一定要由模糊测试的执行者来进行,也可以交给其他人。
漏洞挖掘思路
Fuzz在协议和接口安全测试中比较简单粗暴,试错成本低。Fuzzing是一种基于缺陷注入的自动软件测试技术。通过编写Fuzzer工具向目标程序提供某种形式的输入并观察其响应来发现问题,这种输入可以是完全随机的或精心构造的。Fuzzing测试通常以大小相关的部分、字符串、标志字符串开始或结束的二进制块等为重点,使用边界值附近的值对目标进行测试。
Fuzz的切入点和目标
切入点
为了更好地挖掘漏洞,选择Fuzz接口需要满足这几个要求:
- 这个接口是开放的,可以被低权限进程调用;
- 接口距离Fuzz目标(系统服务)比较接近,中间路径最好透传,这样比较容易分析异常;
- 从简原则。
根据上面的分析,BpBinder中的transact函数就是一个很好的Fuzz接口,但这个函数在底层无法直接调用。
底层transact方法介绍,在c层中,BBinder::transact中会调用onTransact,这个onTransact才是真正处理业务的。需要注意的是,因为我们的binder实体在本质上都是继承于BBinder的,而且我们一般都会重载onTransact()函数,所以onTransact()实际上调用的是具体binder实体的onTransact()成员函数。也就是说,onTransact的具体实现一般在上层的binder实体,而不在BBinder。BBinder没有实现一个默认的onTransact()成员函数,所以在远程通信时,BBinder::transact()调用的onTransact()其实是Bnxxx或者BnInterface的某个子类的onTransact()成员函数,举个例子,BnMediaRecorder中实现了一个onTransact函数,通过switch-case,根据不同code进行分发处理。
我们从BpBinder往上层找,很容易发现,Java层IBinder的transact函数最终调用到BpBinder,且参数是原封不动的“透传”到底层,考虑到Java层的可视化和扩展性,可以选择IBinder的公有方法transact作为Fuzz接口。
接下来是transact的四个参数介绍。我们可以构造这四个参数进行测试。
- code是int类型,指定了服务方法号;
- data是parcel类型,是发送的数据,满足binder协议规则,下面会有详述;
- reply也是parcel类型,是通信结束后返回的数据;
- flag是标记位,0为普通RPC,需要等待,调用发起后处于阻塞状态直到接收到返回,1为one-way RPC,表示“不需要等待回复的”事务,一般为无返回值的单向调用。
目标
Binder其实是提供了一种进程间通信(IPC)的功能。这些系统服务,通过Binder协议抽象出一个个的“接口”,供其他进程调用,是一个重要的潜在的攻击面。如果没有做好权限控制,会让低权限的第三方应用/病毒/木马利用,后果不堪设想。
系统服务具有高权限,是我们需要重点关注的对象,而低权限进程(农民)可以利用binder call去调用系统服务,从低权限到高权限,存在一个跨安全域的数据流,这里就是一个典型的攻击界面。所以,我们选择系统服务作为Fuzz的目标。
系统服务的分类:
- Binder体系的java服务(有Stub接口,也就是AIDL封装);
- Binder体系的Native服务;
- socket体系的init服务(通常见于init.rc);
- 其他服务。
Fuzz引擎
Fuzz引擎实际是构造transact(int code,Parcel data,Parcel reply,int Flags)函数的四个参数,然后调用Ibinder.transact()来调用系统服务。
如何获取Ibinder对象
我们要取到对端的IBinder对象,才可以调用这个服务。系统其实有一些隐藏API可以利用。先通过反射出ServiceManager(hide属性)中的listServices获取所有运行的服务名称。获取到String类型的服务名称后,再反射getService获取对应的服务IBinder对象。
code如何生成
code也称为TransactionID,标定了服务端方法号。每个服务对外定义的方法都会分配方法号,而且是有规律的,第一个服务方法code使用1,第二个是2,第三个使用3,依次类推,如果有N个方法,就分别分配1-N个连续的服务号。
对于Java服务,必定有Stub类,可以通过反射出mInterfaceToken+“$Stub”类中所有成员属性,其中以“TRANSACTION_”开头的int型就是该方法对应的。
data如何构造
data由“RPC header+参数1+参数2+….”来构成的。但我们不需要自己去构造RPC header,直接调用wr i teInter faceToken函数,传入interface name就可以了。interface name是接口名称,只要取得IBinder对象,就可以直接getInterfaceDescriptor来获取interface name,也就是接口方法的描述符。对于Java层服务的方法,可以通过反射获取method对象,然后用getParameterTypes获取所有的类型。
Fuzz系统和逻辑设计
Fuzz系统和逻辑设计一共分为四个部分:
- 测试数据产生器就是用上述方法产生transact需要用到的的四个参数;
- Fuzz引擎用于执行具体的transact调用过程,调用ibinder的transact()函数;
- 监视器用于监控Fuzz结果和异常;
- 日志模块用于记录Fuzz结果,通过对异常日志的分析可以发现漏洞。
操作过程
漏洞挖掘工具以一个App的形式出现,如图2所示,一共有16种测试类型,可以不输入任何的数据,直接点击测试,选中相应的按钮就会出发相应的测试程序,会遍历所有的系统服务以及相应系统服务的函数。每个测试都会新开一个线程,测试过程中,程序可能会出现ANR,这时候选择等待就可以了,或者选择在服务的管理界面关闭BinderFuzzer的后台服务,然后重新运行、测试。在测试的过程中,要盯着AndroidStudio的logcat输出框,查看有没有异常发生,等上面的步骤都操作完毕,查看导出到电脑的log文件,运行脚本,搜索关于崩溃和异常的关键词,进一步分析日志来查找漏洞。
图2 16种测试类型
Android漏洞
目前的主要Android漏洞主要包括App反编译重打包、组件劫持漏洞、密码泄露、第三方库漏洞、WebView漏洞、系统服务漏洞。大部分漏洞都是因为缺乏安全意识,比如系统服务很多漏洞,就是Google的Android开发人员没有做好参数检查和防御性编程。
实践过程中我挖到的第一个是关于WallPaper系统服务一个漏洞,这个系统服务是管理壁纸,由于缺乏完善的参数检查,可以通过setWallpaper()等函数实现一些工具,比如偷换壁纸以及造成System UI进程崩溃等。
在对第三方定制系统挖掘漏洞的过程中,发现小米的一个定制服务存在漏洞,这个系统服务是KeyStore,负责小米的秘钥管理,是一个安全相关的系统服务,如果组合其他的漏洞进行攻击,可以直接对安全中心发起攻击,后果不堪设想。目前小米已经确认危险漏洞,并且会得到及时修复。
漏洞挖掘的结果
通过对Android系统服务的漏洞挖掘,目前一共发现了32个漏洞,其中在AOSP版本的虚拟机上发现了20个,在第三方厂商定制的系统服务中发现了12个,漏洞已经提交Google、小米、魅族等厂商,并且得到了高危漏洞的确认。这些漏洞主要导致重启,从而可以构造拒绝服务攻击(DDoS),还有一些会导致显示进程崩溃等干扰性破坏。
个人觉得Android系统在安全方面的提升主要体现在以下几个方面:
- 建立更加完善的Android漏洞,提交相应完善制度,加快补丁发布;完善Android文件的加密,同时在硬件上完善,比如TrustZone;
- 通过更加细粒度的授权机制,来保护用户的安全和隐私;
- 缩小Android的碎片化;
- 提高开发者的审核门槛,应用市场加强恶意应用的检查。
随着Android的版本升级和对漏洞的不断完善,Android系统正在变得越来越安全。