前言
想学习下 Android漏洞方面的知识,搜了下发现Flanker Edward在知乎上有个回答,提到了binder的经典漏洞cve-2015-6620,所以就从这个漏洞开始学习。作者提供了poc以及文档,这篇笔记主要记录下学习中遇到的问题,以及自己的一些理解。
环境搭建与基础知识
第一次调试android漏洞,搭建环境花了些力气,主要有如下环境,推荐安装pead-arm和shadow这两个gdb插件。
android源码环境:Ubuntu16.04 android_6.0.0_r1
gdb调试环境搭建
peda-arm 安装,调试界面更加方便
shadow 安装,方便调试jemalloc
这是android平台上的binder方面的漏洞,所以涉及一些android底层的知识需要学习下。
binder
智能指针
漏洞成因
cve-2015-6620包含两个漏洞,编号分别为 24123723 和 24445127。主要分析的是 24445127 MediaCodecInfo 越界访问,因为这个漏洞可以利用的点更多些。漏洞存在于MediaCodcList服务。该Binder服务提供了一个getCodecInfo 的功能,存在漏洞的代码如下:
//http://androidxref.com/6.0.0_r1/xref/frameworks/av/media/libmedia/IMediaCodecList.cpp#54status_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(data.readInt32());
const sp 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#49struct MediaCodecList : public BnMediaCodecList {
Vector > mCodecInfos;
virtual sp getCodecInfo(size_t index) const {
return mCodecInfos.itemAt(index); // 未进行任何边界检查 }
}
可以看到直接调用了vector的itemAt函数,并未进行任何边界检查。而index是我们作为客户端程序可以控制的,这个地方就存在一个越界访问的漏洞。
漏洞利用
根据漏洞的成因,我们现在有这样一个能力:可以越界访问 Binder 服务所在进程中的一个vector>,但是只能读取不能写入。漏洞的作者利用这样一种能力可以实现任意地址读取和pc寄存器的控制,很是神奇。主要分析下pc控制的原理。在分析poc原理前,需要了解相关对象在内存的布局,如下图所示:
pc control poc原理分析
一个越界读可以造成pc的控制,关键在于getCodecInfo的调用:const sp\ info = getCodecInfo(index);
//sp 拷贝构造函数template
sp::sp(const sp& 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 __usercall android::MediaCodecList::getCodecInfo@(const android::MediaCodecList *this@, size_t index@)
.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#322void 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> 存储区的后面,这样我们就可以通过越界访问,读取到指向该伪造的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>的存储区的后面,这样可以通过越界读取,来触发漏洞。
但是遇到了如下问题:
1.作者的poc中,硬编码了一个地址 0xb3003010,就是在作者的测试机器上,作者喷射的伪造的 MediaCodecInfo 有很大概率会落在这个地址上。
作者说之所以是 0xb3003010 而不是 0xb3003000 是因为数据前面还有0x10字节的元数据,但是我们知道 jemalloc 中存放数据的region和run都不包含元数据,那么这个元数据是哪里来的?
先看下是如何堆喷射的,作者使用 IDrm.provideKeyResponse 进行堆喷射的,服务端在拿到response后,最终会调用 Session.provideKeyResponse 处理:
typedef android::KeyedVector,
android::Vector > KeyMap;
status_t Session::provideKeyResponse(const Vector& response) {
String8 responseString(
reinterpret_cast(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> 的存储区的后方,但是我在运行作者poc的时候,越界读取很少可以命中,所以就想如何可以提高命中率。
作者的方法是:vector的存储区肯定是jemalloc分配的,肯定是落在某个大小的region内,所以作者首先计算出这个大小,然后后面堆喷射时,喷射出大量相同大小的region,这样后面越界读取就有很大概率命中。所以关键是如下步骤:
计算 vector> 的存储区所在region大小
确保堆喷射时,分配的是相同大小的region
通过调试发现 vector> 的存储区所在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更脆弱些,具体表现在如下方面:
堆地址中的熵较少
很容易猜测到数据位置
保存数据的region中没有元数据
dlmalloc会检查元数据的合法性
上面的堆地址熵较小表现在:32位ARM系统上的ASLR算法的实现很简单,ASLR会将所有的模块随机向下移动几页,范围在0~255页,代码如下,mmap_rnd_bits可以通过/proc/sys/vm/mmap_rnd_bits 来改变。
// kernel/arch/arm/mm/mmap.cstatic 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
原文链接:[原创]cve-2015-6620学习总结-『Android安全』-看雪安全论坛
本文由看雪论坛 glider菜鸟 原创
转载请注明来自看雪社区