之前对Android root相关知识了解的不多,所以打算找几个洞学习一下。这篇是对CVE-2014-7911的相关分析文章的学习笔记。
漏洞成因
0x0 finalize()方法
Java中在进行垃圾回收时,要回收的对象的finalize()
方法会被自动调用。类BinderProxy
在文件/frameworks/base/core/java/android/os/Binder.java
中定义,其finalize()
方法如下:
所以,在回收BinderProxy
对象时,会调用到其native方法destroy()
。该方法的定义在文件/frameworks/base/core/jni/android_util_Binder.cpp
中:
可以看到,指针drl
的方法dexStrong
被执行了,而该指针就是BinderProxy
对象的变量mOrgue
。所以,如果mOrgue
的值被我们控制,那么就可能引发代码执行。
0x1 反序列化
序列化,是把对象转化为字节序列;反序列化,就是把字节序列转化为对象。
java.io.ObjectOutputStream的writeObject(Object obj)
方法,可以把参数obj进行序列化;java.io.ObjectInputStream的readObject()
方法,可以从输入流中读取字节并反序列化为对象。
如果一个类需要进行序列化/反序列化,那么这个类必须实现Serializable
或Externalizable
接口。当实现Serializable
接口的类没有定义readObject
和writeObject
方法时,使用默认的readObject
和writeObject
方法,而且,只有类的非transient
变量才会进行序列化/反序列化。
Java序列化对象时,该对象的类名会保存在序列化得到的字节序列中。随后,在反序列化时,该类名信息会被用于创建对象。而CVE-2014-7911的主要成因,就是篡改了序列化字节中的类名,从而再反序列化时得到了原本不可序列化的BinderProxy
。
为了试验,我们编写一个简单的应用,其中有一个可序列化的类AAAA:
和一个不可序列化的类BBBB:
我们在AVD上,试验将AAAA序列化后,把其中的AAAA改为BBBB,再由此反序列化得到BBBB。
在Google APIs ARM 4.4.2上可以运行成功:
但是在ARM 4.4.2上失败:在方法checkAndGetTcObjectClass
中,提示BBBB不是可序列化的:
而这正是对CVE-2014-7911打了补丁的结果。补丁中新增了一些检查对象的方法,如checkAndGetTcObjectClass
。在实际创建类之前,检查要进行反序列化的是否有效。因此,修改类名来反序列化BinderProxy
,就无法通过检查了,从而修复了漏洞。
0x2 serialVersionUID
注意到,在在可序列化的类AAAA中,包含了值serialVersionUID
。根据文档,每个可序列化的类都有这样一个变量,用于在序列化/反序列化时检查兼容性。如果我们不显示定义这个值,则运行时会自动根据类的信息,计算一个serialVersionUID
。
如果,在AAAA中不显式声明serialVersionUID
,那么在之前的试验中,将其序列化后再反序列化为BBBB时,就会报错:
错误信息显示,BBBB期待的serialVersionUID
值为0,这应该是与BBBB本身无法序列化有关。
我们对比显式设置serialVersionUID
为0后,序列化AAAA得到的字节,发现其包含的信息确实由0x33f9cc28d1a3b64b变为0:
而0x33f9cc28d1a3b64b=3745249040823203403, 正好是错误信息显示的实际的serialVersionUID
:
所以,为了使我们反序列化成功,在类AAAA的定义中应该显式设置serialVersionUID
为0。
漏洞利用
0x0 控制PC
之前提到,我们可以提供特定的mOrgue
,使得方法decStrong
被调用。具体地,方法decStrong
在库libutils.so
中,其汇编代码如下:
这里,r0
为this
指针的值,也就是我们的mOrgue
的值。0x0000D174
处,便是我们控制PC的地方。
为了通过检查,并跳转到我们指定的地址,POC中构造了一套比较精妙的payload:
整个payload分为两段:第一段Relative Addresses Chunk(RAC)是很长的一段,用于跳转到第二段;第二段Gadget Buffer(GB)则是用来放gadget的。上图中:
- SA指Static Address,即我们用来设置mOrgue的一个固定的地址
- GBO指Gadget Buffer Offset,指的是GB在payload中的偏移量,也就是第一段的长度
- SAO即Static Address Offset, 为SA相对于payload的偏移量。
根据payload第一段的构造方式,此时SA处的内容为SA+GBO-SAO,正好为GB的实际地址Gadget Buffer Address(GBA)。所以,只要我们的第一段足够大,使得SA落在其中,则[SA]=GBA。
按照decStrong的执行流程,实际执行效果如下。
由此,我们实现了控制PC
0x1 ROP chain
控制了PC,接下来就是构造ROP链了。试验的对象是AVD 4.4.2 Google APIs ARM image,我们把/system/lib/
下一些常用的库拉下来,并按照POC中的ROP链的思路,搜索ROP gadget。不过在使用ROPgadget时,返回大量的gadget,还是需要人工去看哪些是有用。此外,不知道为什么,有些gadget用ROPgadget没搜到,是手工搜索指令找到的。
最后,ROP链构造如下:
由于decStrong
方法在开始执行时,首先会将r0
的值保存到r5
(见代码0x0000D15C
处),所以,通过chain 0 和chain 1,可以将sp
的值设置到我们的Gadget Buffer中。随后,chain 2将r0
设置为[r0+0x48]
,即r0=GBA-0x48
,我们把要执行的名称放在此处。最后,执行system()
方法,其参数r0
指向的,便是我们提供的要执行的命令。由此完成了整个ROP链,并以system的身份执行命令。
完整的payload结构示意图如下:
0x2 堆喷射
为了使设置的mOrgue能够落入payload中,采取了堆喷射的手段来布置payload。POC中采取的是动态注册Broadcast Receiver,并将payload放入permission字符串中:
注册的Receiver会保存在system_server的堆上。通过大量注册这些Receiver,我们可以在堆上布置大量的payload,以待跳入。
0x3 system_server
为什么注册Receiver,就可以在system_server的堆上布置payload呢?为此,需要简要介绍下system_server。
system_server是一个非常重要的进程,它包括了大量系统服务。我们可以通过ps -t | grep <system_server_pid>
来查看system_server所包含的线程,示例如下:
可以看到,许多重要的服务,如GC, Activity Manager, Windows Manager等,都是system_server的线程。另一方面我们知道,同一进程的不同线程,是拥有各自的栈,但是互相共享text和data,所以堆也是可以共享的。所以,之前研究堆溢出时,可以看到malloc是有线程安全版本的,以保证不同线程分配堆时不会发生冲突。
于是,我们注册Receiver时,最终会调用ActivityManagerService
中的方法registerReceiver
,定义在文件/frameworks/base/services/java/com/android/server/am/ActivityManagerService.java
中。其中会创建新的BroadcastFilter对象,这便是保存在堆上的:
所以通过注册Receiver的方式,我们在Activity Manager、即system_server的堆上布置了payload。
0x4 触发漏洞
当堆上的payload布置好后,我们便需要让system_server触发反序列化了。为此,我们将伪造的序列化字符串发送至system_server。system_server在将其反序列化后,得到伪造的BinderProxy
。随后,在该对象被回收时,触发finalize()
方法,进行访问我们设置的mOrgue
,最后跳到设置好的的堆上,完成代码执行。
参考资料
- http://seclists.org/fulldisclosure/2014/Nov/51
- http://researchcenter.paloaltonetworks.com/2015/01/cve-2014-7911-deep-dive-analysis-android-system-service-vulnerability-exploitation/
- https://github.com/retme7/CVE-2014-7911_poc