1

[原创]cve-2015-6620学习总结

glider菜鸟 2018-5-11 21:43 896

cve-2015-6620学习总结

前言

想学习下android漏洞方面的知识,搜了下发现Flanker Edward在知乎上有个回答,提到了binder的经典漏洞cve-2015-6620,所以就从这个漏洞开始学习。作者提供了poc以及文档,这篇笔记主要记录下学习中遇到的问题,以及自己的一些理解。

环境搭建与基础知识

第一次调试android漏洞,搭建环境花了些力气,主要有如下环境,推荐安装pead-arm和shadow这两个gdb插件。

  1. android源码环境:Ubuntu16.04 android_6.0.0_r1
  2. gdb调试环境搭建
  3. peda-arm 安装,调试界面更加方便
  4. shadow 安装,方便调试jemalloc

这是android平台上的binder方面的漏洞,所以涉及一些android底层的知识需要学习下。

  1. binder
  2. 智能指针

漏洞成因

cve-2015-6620包含两个漏洞,编号分别为24123723和24445127。主要分析的是24445127 MediaCodecInfo越界访问,因为这个漏洞可以利用的点更多些。漏洞存在于MediaCodcList服务。该Binder服务提供了一个getCodecInfo的功能,存在漏洞的代码如下:

//http://androidxref.com/6.0.0_r1/xref/frameworks/av/media/libmedia/IMediaCodecList.cpp#54
status_t BnMediaCodecList::onTransact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    switch (code) {

        case GET_CODEC_INFO:
        {
            CHECK_INTERFACE(IMediaCodecList, data, reply);
            size_t index = static_cast<size_t>(data.readInt32());
            const sp<MediaCodecInfo> info = getCodecInfo(index); //调用服务端的实现
            if (info != NULL) {
                reply->writeInt32(OK);
                info->writeToParcel(reply);
            } else {
                reply->writeInt32(-ERANGE);
            }
            return NO_ERROR;
        }
        break;

从Parce中读取从客户端传来的索引index,然后调用在服务端的实现的getCodecInfo。看下在MediaCodecList中实现的getCodecInfo

// http://androidxref.com/6.0.0_r1/xref/frameworks/av/include/media/stagefright/MediaCodecList.h#49
struct MediaCodecList : public BnMediaCodecList { 
    Vector<sp<MediaCodecInfo> > mCodecInfos;
     virtual sp<MediaCodecInfo> getCodecInfo(size_t index) const {
        return mCodecInfos.itemAt(index);   // 未进行任何边界检查
    }
}

可以看到直接调用了vector的itemAt函数,并未进行任何边界检查。而index是我们作为客户端程序可以控制的,这个地方就存在一个越界访问的漏洞。

漏洞利用

根据漏洞的成因,我们现在有这样一个能力:可以越界访问Binder服务所在进程中的一个vector<sp<MediaCodecInfo>>,但是只能读取不能写入。漏洞的作者利用这样一种能力可以实现任意地址读取和pc寄存器的控制,很是神奇。主要分析下pc控制的原理。在分析poc原理前,需要了解相关对象在内存的布局,如下图所示:

pc control poc原理分析

一个越界读可以造成pc的控制,关键在于getCodecInfo的调用: const sp\<MediaCodecInfo\> info = getCodecInfo(index);

//sp 拷贝构造函数
template<typename T>
sp<T>::sp(const sp<T>& other)
: m_ptr(other.m_ptr)
  {
    if (m_ptr) m_ptr->incStrong(this);
  }

上面的代码是用getCodecInfo函数的返回新建了一个info对象,这就会调用info的拷贝构造函数。info的类型为sp,sp的拷贝构造函数如上所示。可以看看getCodecInfo的汇编版本,像这样返回对象的函数,一般会把R0指向返回对象保存的地址。

//libstagefright.so
.text:000A9478 ; android::sp<android::MediaCodecInfo> __usercall android::MediaCodecList::getCodecInfo@<R0>(const android::MediaCodecList *this@<R1>, size_t index@<R2>)
.text:000A9478 return_obj = R0                         ;保存的就是上面info的地址
.text:000A9478 this = R1                               ; const android::MediaCodecList *
.text:000A9478 index = R2                              ; size_t
.text:000A9478                 PUSH.W          {R11,LR}
.text:000A947C                 MOV             R3, R0
.text:000A947E                 LDR             R0, [this,#0x5C]
.text:000A9480                 LDR.W           R0, [R0,index,LSL#2] ; 这里可以越界读取
.text:000A9484                 STR             R0, [R3]; 设置info.m_prt
.text:000A9486                 CMP             R0, #0
.text:000A9488                 ITT NE
.text:000A948A                 MOVNE           this, R3 ; 调用info的拷贝构造函数,因为inline优化直接调用了(info.m_ptr)->incStrong()
.text:000A948C                 BLXNE           _ZNK7android7RefBase9incStrongEPKv ; android::RefBase::incStrong(void const*)
.text:000A9490                 POP.W           {R11,PC}
.text:000A9490 ; End of function android::MediaCodecList::getCodecInfo(uint)

可以看到会将vector的内容读取到R0中,如果R0不为零,会调用incStrong, 代码如下:

//http://androidxref.com/6.0.0_r1/xref/system/core/libutils/RefBase.cpp#322

void RefBase::incStrong(const void* id) const
{
    weakref_impl* const refs = mRefs;
    refs->incWeak(id);

    refs->addStrongRef(id);
    const int32_t c = android_atomic_inc(&refs->mStrong);
    ALOG_ASSERT(c > 0, "incStrong() called on %p after last strong ref", refs);
#if PRINT_REFS
    ALOGD("incStrong of %p from %p: cnt=%d\n", this, id, c);
#endif
    if (c != INITIAL_STRONG_VALUE)  {
        return;
    }

    android_atomic_add(-INITIAL_STRONG_VALUE, &refs->mStrong);
    refs->mBase->onFirstRef(); //这里有虚函数的调用
}

汇编代码版本,可以清楚看到存在虚函数的调用:

// libutils.so
.text:0000E6BE ; void __fastcall android::RefBase::incStrong(const android::RefBase *const this, const void *id)
.text:0000E6BE                 EXPORT _ZNK7android7RefBase9incStrongEPKv     
.text:0000E6BE                                         
.text:0000E6BE this = R0                                         ; const android::RefBase *const
.text:0000E6BE id = R1                                           ; const void *
.text:0000E6BE                 PUSH            {R4,LR}
.text:0000E6C0                 LDR             R4, [this,#4]     ;this存放的就是越界读取的内容
.text:0000E6C2 refs = R4                               ; android::RefBase::weakref_impl *const
.text:0000E6C2                 MOV             this, refs ; this
.text:0000E6C4                 BLX             j__ZN7android7RefBase12weakref_type7incWeakEPKv ; 
.text:0000E6C8                 DMB.W           SY
.text:0000E6CC                 LDREX.W         R3, [refs]
.text:0000E6D0                 ADDS            R2, R3, #1
.text:0000E6D2                 STREX.W         R1, R2, [refs]
.text:0000E6D6                 CMP             R1, #0
.text:0000E6D8                 BNE             loc_E6CC
.text:0000E6DA                 CMP.W           R3, #0x10000000
.text:0000E6DE                 BNE             locret_E700
.text:0000E6E0                 DMB.W           SY
.text:0000E6E4                 LDREX.W         R0, [refs]
.text:0000E6E8                 ADD.W           R12, R0, #0xF0000000
.text:0000E6EC                 STREX.W         R3, R12, [refs]
.text:0000E6F0                 CMP             R3, #0
.text:0000E6F2                 BNE             loc_E6E4
.text:0000E6F4                 LDR             R0, [refs,#8]
.text:0000E6F6                 LDR             refs, [R0]  ; vtable
.text:0000E6F8                 LDR             R2, [R4,#8] ; 可以通过这里控制pc
.text:0000E6FA                 POP.W           {R4,LR}
.text:0000E6FE                 BX              R2

梳理一下就是,越界读取的内容放入R0,然后进行如下操作:

refs = [R0 + 4]
if ([refs] == 0x10000000)
    mbase = [refs + 8]
    vtable = [mbase]
    call [vtable + 8]

也就是说如果我们在内存中伪造了合适的MeidaCodecInfo,并且将指向该伪造的MediaCodecInfo的指针放入vector<sp<MediaCodecInfo>>存储区的后面,这样我们就可以通过越界访问,读取到指向该伪造的MediaCodecInfo的指针,进而控制pc。我们可以在内存中伪造如下的MediaCodecInfo:

//BASEADDR 为假MediaCodecInfo的起始地址
*(BASEADDR) = vtale; //设置MediaCodecInfo vtable 随便填写
*((unsigned int *)BASEADDR + 1) = BASEADDR + 12;     //mRefs, 使他指向BASEADDR + 12
*((unsigned int *)BASEADDR + 3) = 0x10000000;        //mRefs指向此处,即虚假的info->mRefs的起始地址
*((unsigned int *)BASEADDR + 5) = BASEADDR + 0x20;   //info->mRefs->mBase字段,使他指向BASEADDR + 0x20
*((unsigned int*)BASEADDR + 8) = BASEADDR + 0x20 + 4;  //mBase的vtable字段,使他指向BASEADDR + 0x20 + 4
*((unsigned int*)BASEADDR + 11) = 0x61616161;          //vtable + 8, 我们可以在此处放置目标pc

最终poc运行成功,mediaserver运行到我们指定的位置:0x61616161

 

堆喷射的问题

要成功的运行poc实现漏洞利用的目的,要进行两次堆喷射,第一次是将我们伪造的MediaCodecInfo喷射到内存中,第二次是将我们伪造的MediaCodecInfo的地址喷射到vector<sp<MediaCodecInfo>>的存储区的后面,这样可以通过越界读取,来触发漏洞。
但是遇到了如下问题:

 

1.作者的poc中,硬编码了一个地址0xb3003010,就是在作者的测试机器上,作者喷射的伪造的MediaCodecInfo有很大概率会落在这个地址上。作者说之所以是0xb3003010而不是0xb3003000是因为数据前面还有0x10字节的元数据,但是我们知道jemalloc中存放数据的region和run都不包含元数据,那么这个元数据是哪里来的?

 

先看下是如何堆喷射的,作者使用IDrm.provideKeyResponse进行堆喷射的,服务端在拿到response后,最终会调用Session.provideKeyResponse处理:

typedef android::KeyedVector<android::Vector<uint8_t>,
        android::Vector<uint8_t> > KeyMap;

status_t Session::provideKeyResponse(const Vector<uint8_t>& response) {
    String8 responseString(
    reinterpret_cast<const char*>(response.array()), response.size());
    KeyMap keys;

    Mutex::Autolock lock(mMapLock);
    JsonWebKey parser;
    if (parser.extractKeysFromJsonWebKeySet(responseString, &keys)) {
        for (size_t i = 0; i < keys.size(); ++i) {
            const KeyMap::key_type& keyId = keys.keyAt(i);
            const KeyMap::value_type& key = keys.valueAt(i);
            mKeyMap.add(keyId, key);          //在这里会将payload保存到堆
        }
        return android::OK;
    } else {
        return android::ERROR_DRM_UNKNOWN;
    }
}

可以发现喷射的数据最终是保存在android::Vector中的,通过查看源码发现:android::Vector的数据是保存在SharedBuffer中的。0x10字节保存的就是SharedBuffer的私有变量

int32_t        mRefs;
size_t         mSize;
uint32_t       mReserved[2];

2.第一次堆喷射需要将指向伪造的MediaCodecInfo的指针喷射到vector<sp<MediaCodecInfo>>的存储区的后方,但是我在运行作者poc的时候,越界读取很少可以命中,所以就想如何可以提高命中率。

 

作者的方法是:vector的存储区肯定是jemalloc分配的,肯定是落在某个大小的region内,所以作者首先计算出这个大小,然后后面堆喷射时,喷射出大量相同大小的region,这样后面越界读取就有很大概率命中。所以关键是如下步骤:

  • 计算vector<sp<MediaCodecInfo>>的存储区所在region大小
  • 确保堆喷射时,分配的是相同大小的region

通过调试发现vector<sp<MediaCodecInfo>>的存储区所在region大小和作者中poc给的一致,但是在调试时发现喷射的payload并没有落在大小为160的region内,而是在0x100的region内。

 

原因就是payload在mediaserver中是保存在Vector中的,Vector在分配空间时会多分配一些,所以大小为160的payload,最终会放置在大小大于160的region中,通过调试,我把payload大小改为96就可以保证分配在160的region中。

 

修改后,运行poc后,内存布局如下图所示:

 

 

3.作者并未说明是如何找到0xb3003010这个地址的,存在这样一个地址的依据是什么呢?

 

查了一些关于android堆喷射的资料,都提到jemalloc相比之前的dlmalloc更脆弱些,具体表现在如下方面:

  1. 堆地址中的熵较少
    • 很容易猜测到数据位置
  2. 保存数据的region中没有元数据
    • dlmalloc会检查元数据的合法性

上面的堆地址熵较小表现在:32位ARM系统上的ASLR算法的实现很简单,ASLR会将所有的模块随机向下移动几页,范围在0~255页,代码如下,mmap_rnd_bits可以通过/proc/sys/vm/mmap_rnd_bits 来改变。

// kernel/arch/arm/mm/mmap.c

static unsigned long mmap_base(unsigned long rnd)
{
    unsigned long gap = rlimit(RLIMIT_STACK);

    if (gap < MIN_GAP)
        gap = MIN_GAP;
    else if (gap > MAX_GAP)
        gap = MAX_GAP;

    return PAGE_ALIGN(TASK_SIZE - gap - rnd);
}

void arch_pick_mmap_layout(struct mm_struct *mm)
{
    unsigned long random_factor = 0UL;

    if ((current->flags & PF_RANDOMIZE) &&
        !(current->personality & ADDR_NO_RANDOMIZE))
        //随机出向下移动几页
        random_factor = (get_random_long() & ((1UL << mmap_rnd_bits) - 1)) << PAGE_SHIFT;

    if (mmap_is_legacy()) {
        mm->mmap_base = TASK_UNMAPPED_BASE + random_factor;
        mm->get_unmapped_area = arch_get_unmapped_area;
    } else {
        mm->mmap_base = mmap_base(random_factor);
        mm->get_unmapped_area = arch_get_unmapped_area_topdown;
    }
}

可以看到随机移动的范围不大,而进程的各个模块在大范围的是相对固定的,比如在我的机器上,堆分配的空间基本落在0xaf000000 - 0xb6000000 之间,当分配的内存远大于255页时,就基本可以找到一个稳定的地址来放置payload,可以粗略计算下:

 

作者构造的payload在binder服务端,最终是保存在Vector中的,Vector会多分配一些空间。在我的机器上最终分配的空间大小为6144字节,放在大小为0x1800的region内,都是放在大小为0x3000的run内,每个run可以放置两个这样的region,并且run是页对齐的。也就是说分配两次payload就占用了3页大小空间,作者的poc一共分配了0x1200次,理论讲会一共分配了6912页,当然实际中会存在回收后再利用的情况,在我的机器上实际上分配了3000多页,这是远远大于随机移动的0~255页。所以可以找到这样一个相对稳定的地址。

总结

作者并没有给出explicit,但是提到可以用 CVE-2015-1528 exp的思路,后续会继续学习下这个思路,有成果了就补上。小弟刚开始学习漏洞这块,有错误恳请大佬们指正。

学习资料

漏洞作者给的文档以及poc: https://github.com/flankerhqd/mediacodecoob

 

adb+gdb : https://wladimir-tm4pda.github.io/porting/debugging_gdb.html

 

binder学习:https://blog.csdn.net/universus/article/details/6211589

 

jemalloc: https://blog.csdn.net/koozxcv/article/details/50973217

 

shadow jemalloc 调试: https://blog.csdn.net/hl09083253cy/article/details/79147625

 

shadow 安装:https://github.com/CENSUS/shadow

 

heap fengshui: https://bbs.pediy.com/thread-55879.htm

 

ASLR:http://drops.xmd5.com/static/drops/papers-14181.html

最后于 2018-5-11 21:45 被glider菜鸟编辑 ,原因:
最新回复 (9)
sakura零 2018-5-13 15:06
2
赞,你是在什么手机上调的
glider菜鸟 2018-5-13 18:11
3
sakura零 赞,你是在什么手机上调的
谢谢,模拟器上调试的
toToC 2018-5-14 10:12
4
glider菜鸟 2018-5-14 18:21
5
toToC
,谢谢
demoLin 2018-5-15 17:22
6
请问下,poc.cpp怎么编译的?
glider菜鸟 2018-5-16 09:17
7
demoLin 请问下,poc.cpp怎么编译的?
放在源码目录内
LOCAL_PATH  :=  $(call  my-dir)
include  $(CLEAR_VARS)
LOCAL_MODULE  :=  mediapoc
LOCAL_MODULE_TAGS  :=  optional
LOCAL_SRC_FILES  :=  mediapoc.cpp
LOCAL_CFLAGS  +=  -march=armv4
LOCAL_SHARED_LIBRARIES  :=  libbinder  libutils  libmedia  libstagefright_foundation
include  $(BUILD_EXECUTABLE)
最后于 2018-5-16 09:17 被glider菜鸟编辑 ,原因:
demoLin 2018-5-16 11:35
8
glider菜鸟 demoLin 请问下,poc.cpp怎么编译的? 放在源码目录内LOCAL_PATH&nbsp; :=&nbsp; $(call&am ...
你好,再问一下LOCAL_SHARED_LIBRARIES的查询路径是什么?我用ndk-build总是找不到依赖模块
心许雪 3天前
9
mark一下,学习。
Zkeleven 3天前
10
学习
返回