上一篇文章CVE-2014-7911 Android提权漏洞(一)中对CVE-2014-7911这个漏洞的原理和PoC进行了学习,通过学习这个漏洞感觉还是很有收获,因为之前只是做过CTF比赛中的题目,没太接触过实际的漏洞分析,所以通过这次学习不仅将之前学到的知识用到了实践,还对真实世界的漏洞有了初步了解。
这篇文章继续对这个漏洞的利用进行学习,最终目标是利用这个漏洞以system权限执行代码。
前情回顾,这个漏洞是利用Java反射和Binder进程间通信机制,向system_server
传入一不可序列化的恶意对象,由于java.io.ObjectInputStream
未对该输入的对象实例是否实际可序列化做校验,因此当该对象实例被ObjectInputStream反序列化时,将发生类型混淆,对象的Field被当作由native代码处理的指针,使攻击者获得控制权。
Dalvik-heap Spray
为了能可靠稳定地跳转到攻击者的提权代码,需要使用堆喷射技术,在system_server
内存空间的Dalvik-heap
中预先布置大量的Spray Buffer
,其中放置提权代码以及大量指向该提权代码的地址。这里需要解决两个问题:
- 向system_server的Dalvik-heap空间传入可控字符串
- 构造特殊布局的Spray Buffer,使代码能稳定执行
1.向Dalvik-heap传入字符串system_server
向Android系统提供绝大多数的系统服务,通过这些服务的一些特定方法可以向system_server
传入字符串,同时system_server
把这些字符串存储在Dalvik-heap
中,在GC之前都不会销毁。
如android.content.Context
中的registerReceiver()
1 | public Intent registerReceiver (BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler) |
其中,broadcastPermission
为String类型,调用该方法后,String Buffer将常驻system_server
进程空间。调用链如下:
1 | ContextWrapper.registerReceiver |
可以看出从应用程序的Context就能通过binder IPC跨进程调用system_server
的ActivityManagerService.registerReceiver()
。ActivityManagerService
常驻system_server
进程空间,其registerReceiver()
实现如下:
代码清单 /frameworks/base/services/java/com/android/server/am/ActivityManagerService.java
1 | public Intent registerReceiver(IApplicationThread caller, String callerPackage, IIntentReceiver receiver, IntentFilter filter, String permission, int userId) { |
通过上述代码中的new
就能在system_server
进程的Dalvik-heap
中分配String Buffer了。
2.构造特殊布局的Spray Buffer
攻击者可控的mOrgue
需要指向一个可读的内存区域,让其指向传入registerReceiver()
的broadcastPermission
参数所属的地址区域并在broadcastPermission
中布置ROP Gadget即可。但是system_server
在其Dalvik-heap
中分配String Buffer
的地址是未知的,因此mOrgue
未必能命中Dalvik-heap
中的String Buffer
。为了提高命中率,需要在Dalvik-heap
中分配大量的String Buffer
,这就是Heap Spray
(堆喷射)。反复调用registerReceiver()
分配大量的String Buffer
即可完成Heap Spray
,但是因为String Buffer
地址未知,这就需要构造一种特殊的堆喷射布局:
1 | STATIC_ADDRESS = mOrgue |
之前的思路是在
Relative Address Chunk
中都放入GADGET_BUFFER
(即Gadget Buffer
的地址),但是这个地址在每个堆块中都不一样,而且也无法在跨进程传入system_server
之前知道,因此不能这样简单构造。
如上图所示,这里的布局是在Relative Address Chunk
中,按地址增长的方向依次放入递减的地址值,这样给定一个STATIC_ADDRESS
,只要其落在某个Relative Address Chunk
的地址范围,就一定有[STATIC_ADDRESS] = GADGET_BUFFER
,推导如下:
1 | 已知: |
而且上述布局还满足[STATIC_ADDRESS + 4N] = GADGET_BUFFER - 4N
(这个条件在后面布置Gadget时会用到),因为STATIC_ADDRESS
每加4,对应地址里的值就会减4,如:
1 | [STATIC_ADDRESS + 4] = STATIC_ADDRESS + Gadget_offset - 4N - 4 = GADGET_BUFFER - 4 |
为了提高STATIC_ADDRESS
落入Relative Address Chunk
的可能性,需要满足:
1.每一个堆块的Relative Address Chunk比Gadget Buffer大很多;
2.分配大量的这样的堆块
按照这样布局,再看汇编代码,布置Gadget Buffer:
1 | ldr r4, [r0, #4] # r0 = STATIC_ADDRESS--->r4 = [STATIC_ADDRESS + 4] = GADGET_BUFFER - 4 |
调用android_atomic_dec函数之后
1 | cmp r0, #1 # r0 = [GADGET_BUFFER-4] |
为了进入blx r2
这条分支,r0
必须等于1,也就是[GADGET_BUFFER - 4] = 1
;
为了blx r2
跳转到GADGET_BUFFER
里的内容执行(即r2 = [GADGET_BUFFER]
),需要令[[GADGET_BUFFER + 4]] + 12 = GADGET_BUFFER
,只要令[GADGET_BUFFER + 4] = STATIC_ADDRESS + 12
即可。
ROP Chain
由于Android使用了DEP
,因此Dalvik-heap
内存里的数据不能执行,这就必须使用ROP技术,使PC跳转到一系列合法的指令序列(Gadget)。这里要用Gadget调用system函数执行代码。
使用ROPGadget
,在zygote
加载的基础模块(如libc.so、libwebviewchromium.so、libdvm.so)上进行搜索,把ARM code
当做Thumb code
来搜索,可以增加更多的候选指令序列。
为了调用system函数,需要控制r0
寄存器,指向我们预先布置的命令行字符串作为参数。这里需要使用Stack Pivot
技术,将栈顶指针SP
指向控制的Dalvik-heap
堆中的数据,这将为控制PC
寄存器、以及在栈上布置数据带来便利。利用
1 | ROPgadget --thumb --binary libwebviewchromium.so |
可找到如下Gadget
Gadget1
为Stack Pivot做准备
libwebviewchromium.so
1 | 70a93c: 682f ldr r7, [r5, #0] # r5 = STATIC_ADDRESS, r7 = [STATIC_ADDRESS] = GADGET_BUFFER |
因此,GADGET_BUFFER+8
这个地址需要指向第二个Gadget
Gadget2
Stack Pivot
libdvm.so
1 | 664c4: f107 0708 add.w r7, r7, #8 # r7 = r7 + 8 = GADGET_BUFFER + 8 |
可以看到,将SP
指向堆中可控的数据后,后面就可以控制PC
。这里,我们提前将system函数的地址写入[GADGET_BUFFER+12]
。为什么要通过Gadget1的过渡才能来到Gadget2,事实上这是不得已而为之,使用ROPGadget搜遍/system/lib
下的基础模块grep "mov sp, r"
,只发现mov sp, r7
,因此只能采取这种过渡的方式。
接下来,在GADGET_BUFFER+20
这个地址填入Gadget3的地址。
Gadget3
libwebviewchromium.so
1 | 30c4b8: 4668 mov r0, sp # r0 = GADGET_BUFFER + 24 |
因此,提前将system函数的参数放入r0指向的GADGET_BUFFER+24即可,最终将以system_server的权限执行任意代码。
最终的chunk布局如图。
1 | +---------------------------------+<------低地址(堆底地址) |
最后,构造ROP Chain还需要考虑一个细节,ARM有两种模式Thumb和ARM模式,我们使用的Gadgets均为Thumb模式,因此其地址的最低位均需要加1。
ASLR
Android自4.1始开始启用ASLR
(地址随机化),任何程序自身的的地址空间在每一次运行时都将发生变化。但在Android中,攻击程序、system_server
皆由zygote
进程fork而来,因此攻击程序与system_server共享同样的基础模块和Dalvik-heap。只要在使用Dalvik-heap Spray和构建ROP Gadget时,只使用libc、libdvm这些基础模块,就无需考虑地址随机化的问题。通过对攻击程序自身/proc/<pid>/maps
文件的解析,就可以得知所加载基础模块的基址。如图,
根据上述Gadgets构建的POC ,执行完毕后,将以system用户的权限在/data目录下生成一个pwned.txt
文件。
修复
见Google的diff,涉及与反序列化相关的 ObjectInputStream.java、ObjectStreamClass.java、ObjectStreamConstants.java、SerializationTest.java等文件。主要加了三种检查:
- 检查反序列化的类是否仍然满足序列化的需求;
- 检查反序列化的类的类型是否与stream中所持有的类型信息 (enum, serializable, externalizable)一致;
- 在某些情形下,延迟类的静态初始化,直到对序列化流的内容检查完成。
漏洞的原理和利用都学习完了,但是还是觉得差的很多,主要原因是没有对利用过程进行调试,后面要学习以下Android源码的调试,对漏洞利用有一个更深刻的理解。
reference
再论CVE-2014-7911安卓序列化漏洞 by 小荷才露尖尖角