在2014年,Jann Horn发现一个安卓的提权漏洞,该漏洞允许恶意应用从普通应用权限提权到system用户执行命令,漏洞信息与POC见(1]。漏洞的成因源于在安卓系统(<5.0)中,java.io.ObjectInputStream并未校验输入的java对象是否是实际可序列化的。攻击者因此可以构建一个不可序列化的java对象实例,恶意构建其成员变量,当该对象实例被ObjectInputStream反序列化时,将发生类型混淆,对象的Field被视为由本地代码处理的指针,使攻击者获得控制权。这就是CVE-2014-7911。在了解它的详细信息之前我们先了解以下基础知识:
Android IPC机制
Android应用之间使用沙盒进行隔离用以保障安全性。不过,它们之间可以通过Intent消息机制进行交互。交互时复杂数据类型的传递必须由发送方进行序列化,然后由接收方反序列化。为了方便开发者,Java为序列化提供了内置支持。
Java序列化
序列化Serializable的作用是将数据对象存入字节流当中,在需要时重新生成对象。Android中Intent中传递对象进行序列化有两种方法,一种是Bundle.putSerializable(Key,Object),另一种是Bundle.putParcelable(Key, Object)。前者是实现了Serializable接口,而后者是实现了Parcelable接口。Serializable使用IO读写存储在硬盘上,而Parcelable是直接在内存中读写,很明显内存的读写速度通常大于IO读写,所以在Android中通常优先选择Parcelable。但是Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable在外界有变化的情况下不能很好的保证数据的持续性。所以在这种情况下会使用Serializable。
implements Serializable接口的的作用就是给对象打了一个标记,系统会自动将其序列化。
例如,序列化(和其它)的对象可以通过以下形式添加到Intent的extras中:
1 SerializableTypeobj = … 2 3 Bundle b = intent.getExtras(); 4 5 b.putString(“foo”, “some string”); 6 7 b.putSerializable(“bar”, obj);
接收数据的代码如下:
1 Bundle b = intent.getExtras(); 2 3 String foo = (String)b.getString(“foo”); 4 5 SerializableTypeobj = (SerializableType)b.getSerializable(“bar”);
java垃圾回收机制(GC)
我们主要介绍一下java垃圾回收机制中的finalize()方法。
finalize()方法是所有实体对象都具有的方法,因为这个是Object类定义的,常被误认为是垃圾回收的方法或者叫做析构函数,其实并非如此。finalize在JVM内存回收前会被调用(但并非绝对),而即使不调用它,JVM回收机制通过后面所述的一些算法就可以定位哪些是垃圾内存,那么这个拿来干什么用呢?
finalize()其实是要做一些特殊的内存回收操作,如果对JAVA研究稍微多一点,大家会发现JAVA中有一种JNI的机制,即:Java native interface,这种属于JAVA本地接口调用,即调用本地的其它语言信息,JAVA虚拟机底层调用也是这样实现的,这部分调用中可能存在一些对C、C++语言的操作,在C和C++内部通过new、malloc、realloc等关键词创建的对象垃圾回收机制是无能为力的,因为这不是它要管理的范围,而平时这些对象可能被JAVA对应的实体所调用,那么需要在对应JAVA对象放弃时(并不代表回收,只是程序中不使用它了)去调用对应的C、C++提供的本地接口去释放这段内存信息,它们的释放同样需要通过free或delete去释放,所以我们一般情况下不要滥用finalize(),可能你会联想到另一类某些特殊引用对象的释放,如层数引用太多,JAVA虚拟机有些时候不知道这一线的对象是否都可能被回收,那么,你可以自己将finalize()重写,并将内置对象的句柄先释放掉,这样也是没有问题的,不过一般不要滥用。
了解了这些知识后,我们说下CVE-2014-7911的细节,大概是这样的:system_server是一个拥有system权限的关键系统进程,任意应用可向其发送可序列化对象,虽然system_server并不会主动调用该对象的方法,但是在系统GC该对象时会调用到其finalize方法。Jann Horn发现android.os.BinderProxy这个类在finalize方法里调用了一个native指针,而这个指针可被攻击者控制指向任意地址,因此在GC时有机会造成代码执行。
但android.os.BinderProxy是一个不能被序列化的类,因此理论上我们并不能通过发送它的实例来控制将要攻击的目标进程代码执行,但由于java.io.InputStream没有检查接收到的对象是否可序列化,因此我们可以通过特殊方法构造一个被序列化了的伪android.os.BinderProxy来达到目的。值得注意的是,尽管Android采用了ASLR机制,但为了获得正确的地址,可以基于这样一个事实:system_server和攻击者app都是从同一个进程-Zygote fork而来,具有相同的基址,攻击者通过分析自己进程的map文件(位于/proc/maps)就可以知道system_server以及所加载模块的内存布局。Google发布的补丁为java.io.InputStream添加了序列化检查来阻止攻击者发送android.os.BinderProxy,但通过这个漏洞我们可以发现一个可序列化的类只要满足以下条件就能代替android.os.BinderProxy实现代码执行的目的:
(1)实现了finalize方法;
(2)在finalize方法里调用了一个native指针;
(3)该native指针是攻击者可控的(没有被声明为transient和static);
(4)implements了Serializible接口(可序列化);
(5)没有重载readObject和readResolve方法来阻止我们控制native指针。
IBM Security在安卓原生框架中找到了一个这样的类OpenSSLX509Certificate(CVE-2015-3525),在Google Play Services APK中就使用了OpenSSLX509Certificate类。另外还发现了6 个存在漏洞的第三方SDK:
- Jumio (CVE-2015-2000)
- MetaIO (CVE-2015-2001)
- PJSIP PJSUA2 (CVE-2015-2003)
- GraceNote GNSDK (CVE-2015-2004)
- MyScript (CVE-2015-2020)
- esriArcGis (CVE-2015-2002)
那么这些SDK是否存在某些相似之处呢?经过分析发现前5个都使用了SWIG,这是一种连接C/C++与Java等各种高级语言的互操作性工具。在某些开发者提供的配置中,SWIG可以产生以下漏洞代码:
因为Bar能够被序列化,所以Foo可以被序列化,因此,攻击者可以控制swigCPtr(和swigCMemOwn),而swigCPtr被用于native代码中。因而这是一个高度符合条件可被利用的场景,特别是当原始的C++类有虚拟析构函数的时候。
通过分析这个漏洞我们可以知道,CVE-2014-7911的补丁只是紧缩了攻击界面,而没有真正解决问题。Google随后发布了CVE-2015-3525的补丁,将存在于OpenSSLX509Certificate类中的native指针声明为transient,使该指针不可被序列化。然而,即使安卓的原生世界修复了所有危险的可序列化类,如果第三方SDK中存在这样的类依然可被用于序列化攻击。IBM Security建议将Bundle的解析内部资源修改为懒行为,并修改readObject、readResolve等读取方法来阻止攻击者控制危险变量。
参考文章:
https://github.com/retme7/CVE-2014-7911_poc
http://www.freebuf.com/news/74676.html