2017-04-28 10:58:33
阅读:1094次
点赞(0)
收藏
作者:arnow117
翻译:arnow117
预估稿费:300RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
背景介绍
jemalloc的相关研究
argp与huku在2012年在Phrack上发表的:对jemalloc内存分配器的单独利用(做出了基于FreeBSD上libc的POC)。
argp与huku在2012年BlackHat上发表的:在Firefo中玩坏jemalloc的元数据。
argp在2015年INFILTRATE上的jemalloc漏洞利用方法论。
Android堆漏洞利用的相关研究
Hanan Be'er对CVE-2015-3864这个stagefright中整形溢出导致堆破坏的漏洞利用
Aaron Adams的对这个漏洞的又一次利用
Joshua Drake对于stagefright漏洞利用相关工作 向之前的研究者们致谢!(这也是为什么要翻译这一段之必要)
配合jemalloc使用的插件:Shadow
注,因为本文核心是jemalloc与堆漏洞利用,此章节关于对关于插件shadow的历史部分没有翻译。
shadow是CENSUS开发的的一个基于jemalloc的堆漏洞利用框架,开源在Github(传送门)上。用来搭配调试器提供jemalloc分配器的内部结构信息。可以作为插件在GDB,WINDBG,以及lldb中使用。
这个框架有几个优点:
没有多余的要附加的源文件。
对于Android与Firefox两个平台,使用相同的指令。
简化的调试引擎。
提供堆快照支持。
(gdb)jeparse-f (gdb)jestore/tmp/snapshot1提供单独的脚本,允许进行非运行时的堆排布分析。单独使用时样例:
$pythonshadow.py/tmp/snapshot1jeruns-c listingcurrentrunsonly [arena00(0x0000007f85680180)][bins36] [run0x7f6ef81468][regionsize08][totalregions512][freeregions250] [run0x7f6e480928][regionsize16][totalregions256][freeregions051] [run0x7f6db81888][regionsize32][totalregions128][freeregions114] ...提供对于堆中内存排布的解析脚本。作为Python插件包时的使用样例:
//code importjemalloc heap=jemalloc.jemalloc("/tmp/snapshot1") forchunkinheap.chunks: print"chunk@0x%x"%chunk.addr //run $pythonprint_chunks.py chunk@0x7f6d240000 chunk@0x7f6db00000 chunk@0x7f6db40000 chunk@0x7f6db80000 chunk@0x7f6dbc0000 ...jemalloc
jemalloc的一些特性
jemalloc使用bitmap管理堆分配,而不是通过内存的利用率,这也可能是jemalloc被广泛使用的主要原因。当下FreeBSD libc,Firefox,Android libc,mysql,Redis以及Facebook内部都在用。
设计原则
最小化的元数据开销
基于每个线程进行缓存,避免了同步问题。
避免了连续分配内存的碎片化问题。
简洁高效(所以就可以预判了哦呵呵)
Android中的jemalloc
在Android 6使用的版本实际上是4.0.0,在Android 7 上使用的版本是4.1.0-4-g33184bf69813087bf1885b0993685f9d03320c69
jemalloc在Android源码中的修改通过宏定义开关控制的代码块来实现,同时辅以/* Android change */的注释
#ifdefined(__ANDROID__)/*ANDROIDchange*/ /*…*//*…*/ #endif/*EndANDROIDchange*/在jemalloc的Android.mk中限制了仅使用两个arenas,并且开启了线程缓存(PS:本文的讨论基于64位的架构)
//partofAndroid.mk jemalloc_common_cflags+=\ -DANDROID_MAX_ARENAS=2\ -DJEMALLOC_TCACHE\ -DANDROID_TCACHE_NSLOTS_SMALL_MAX=8\ -DANDROID_TCACHE_NSLOTS_LARGE=16\jemalloc内部结构
概念:region
调用malloc返回给用户的实际内存
在内存中连续分布
不包含元数据
根据大小不同,划分为三种类型:
Small,最大0x14336字节
Large,最大0x38000字节(Android 6上)
Huge,大于0x38000
可以使用shadow中的jebininfo列出当前线程所有的region,或者jesize列出满足给定size的region相关信息
//jebinfo (gdb)jebininfo [bin00][regionsize008][runsize04096][nregs0512] [bin01][regionsize016][runsize04096][nregs0256] [bin02][regionsize032][runsize04096][nregs0128] [bin03][regionsize048][runsize12288][nregs0256] [bin04][regionsize064][runsize04096][nregs0064] [bin05][regionsize080][runsize20480][nregs0256] [bin06][regionsize096][runsize12288][nregs0128] [bin07][regionsize112][runsize28672][nregs0256] //jesize (gdb)jesize24 [bin02][regionsize032][runsize04096][nregs0128]线程申请memory时,与region的对应关系。
概念:run
存放连续的大小相同的region的容器
一系列连续的页集合
内部存放small/large类型的region
没有元数据
查看给定的地址所属的run中的region信息
(gdb)jerun0x7f931c0628 [region000][used][0x0000007f931cc000][0x0000000070957cf8] [region001][used][0x0000007f931cc008][0x0000000070ea78b0] [region002][used][0x0000007f931cc010][0x0000000070ec2868] [region003][used][0x0000007f931cc018][0x0000000070f0322c] ... (gdb)x/4gx0x7f931cc000 0x7f931cc000:0x0000000070957cf80x0000000070ea78b0 0x7f931cc010:0x0000000070ec28680x0000000070f0322c ...线程申请memory时,与run的对应关系。
概念:chunk
存放run的容器
大小固定相同
操作系统返回的内存被划分到chunk中管理
存储着关于自身以及它管理的run的元数据
chunks的结构,与run以及元数据的关系。
chunks中的元数据结构,mapbit[0]与mapmisc[0]指向chunk中的第一个run。
chunks元数据中mapmisc中的bitmap结构管理着run中的region的分配使用。
变化: 不同Android版本下的jemalloc
Chunk的大小
元数据的变化
增加了mapbias与mapbits flags
堆中的jemalloc
root@bullhead/:cat/proc/self/maps|greplibc_malloc 7f81d00000-7f81d80000rw-p0000000000:000[anon:libc_malloc] 7f82600000-7f826c0000rw-p0000000000:000[anon:libc_malloc] 7f827c0000-7f82a80000rw-p0000000000:000[anon:libc_malloc] 7f82dc0000-7f830c0000rw-p0000000000:000[anon:libc_malloc] (gdb)jechunks [shadow][chunk0x0000007f81d00000][arena0x0000007f996800c0] [shadow][chunk0x0000007f81d40000][arena0x0000007f996800c0] [shadow][chunk0x0000007f82600000][arena0x0000007f996800c0] [shadow][chunk0x0000007f82640000][arena0x0000007f996800c0] [shadow][chunk0x0000007f82680000][arena0x0000007f996800c0] [shadow][chunk0x0000007f827c0000][arena0x0000007f996800c0] ...可以看到maps中0x7f81d00000对应的memory,属于两个chunk,分别位于0x7f81d00000以及0x7f81d40000。
jemalloc的内存排布
jemalloc管理下的内存排布
溢出了region的示意图
如果溢出了run的示意图
如果溢出了chunk的示意图,注意到,chunk头部有元数据。
基于jemalloc的堆喷
Hanan Be'er, Aaron Adams, Mark Brand, Joshua Drake都讨论过
region与run都没有元数据
堆喷的时候,chunk的第一个与最后一个页是不可喷的
chunk的地址是可以预测的
chunk中可堆喷的区域大小示意图
chunk地址可预测?
Google ProjectZero的Mark Brand曾经有说过
32位上:大chunk,而地址空间较小
mmap()产生多个chunk,而chunk大小固定。
Andorid进程通常加载很多模块
Android 7的chunk更大
同样适用于申请巨大的内存
可预测的chunk地址意味着
可预测的run地址
可预测的region地址
这些可以让我们做更有目的性的,更加精确的堆喷
jemalloc的内存管理
arena
arena内存申请器
用来缓解线程间申请memory时的竞争问题
每一个arena彼此独立,管理各自的chunk
每个线程在第一次malloc时,建立起与各自的arena的联系,一个线程只指向一个arena
每个进程中,arena的数量由jemalloc配置决定,在Android上硬编码为两个。
在malloc申请内存中,arena与线程缓存的关系。
申请的memory在jemalloc内部实际是通过arena申请的,且在每一个线程中都有一个缓存。
查看进程的arena
//common (gdb)x/2gxarenas 0x7f99680080:0x0000007f997c01800x0000007f996800c0 //usingshadow (gdb)jearenas [jemalloc][arenas02][bins36][runs1408] [arena00(0x0000007f997c0180)][bins36][threads:1,3,5] [arena01(0x0000007f996800c0)][bins36][threads:2,4] arenabin每个arena都有一个bin数组
每一个bin对应着一种small类型,大小固定的region。
同时bin数组还肩负着用树存储未满的run的职责,并选一个作为当前指向的run
查看arena bin,runcur为对应region所属run的地址
(gdb)jebins [arena00(0x7f997c0180)][bins36] [bin00(0x7f997c0688)][sizeclass08][runcur0x7f83080fe8] [bin01(0x7f997c0768)][sizeclass16][runcur0x7f82941168] [bin02(0x7f997c0848)][sizeclass32][runcur0x7f80ac0808] [bin03(0x7f997c0928)][sizeclass48][runcur0x7f81cc14c8] [bin04(0x7f997c0a08)][sizeclass64][runcur0x7f80ac0448] ...查看当前run,以及其中region的信息。
(gdb)jeruns-c [arena00(0x7f997c0180)][bins36] [run0x7f83080fe8][regionsize08][totalregions512][freeregions158] [run0x7f82941168][regionsize16][totalregions256][freeregions218] [run0x7f80ac0808][regionsize32][totalregions128][freeregions041] [run0x7f81cc14c8][regionsize48][totalregions256][freeregions093] [run0x7f80ac0448][regionsize64][totalregions064][freeregions007] ...通过arena申请内存流程
申请size为8字节的memory时,先查bin,发现bin[0]所代表size为8的small region可以装的下,则查找对应存放这个连续region的run,并从中分配一块region返回。
通过arena释放内存流程
free与申请类似,查找到存放region的run,然后释放这个region。
arena中的线程缓存
什么是线程缓存(tcache)
arena与线程缓存的流程关系。
每一个线程维护着一个对small/large内存申请的缓存
对缓存的操作与栈相似
以申请时间为衡量的增长式“垃圾回收”机制
线程缓存栈以及其指向的run中region示意图,tbins[0]中存储着对应size的region缓存栈,每一种size的tbin中存储着其size下对应的缓存栈。
线程缓存在申请内存时候的作用
还是刚才malloc的图,加上了tcache,可以看到,没有直接去通过arena要region,而是先去查对应size的tbin缓存栈avail去了。
在缓存栈中,弹出一个最近被free“回收”到缓存栈上的内存地址做新malloc的返回地址。
按照如此大小一直申请,最终栈会弹空。之后arena再通过元数据向run中要对应size的region,申请的数量是lg_fill_div,将返回的内存地址再压入缓存栈。
线程缓存在释放内存时的作用
释放与申请类似,只不过变成了将释放的地址压入缓存栈。
同样,缓存栈满了后arena也会将对应region还回去,但是每次只还一半。申请时间久的先被归还回去。缓存栈的容量在结构体tcache_bin_info中有定义。
tcache中的数据结构
structtcache_s{ ... tcache_bin_ttbins[]; /*cachedallocation pointers(stacks)*/ }; structtcache_bin_s{ ... unsignedlg_fill_div; unsignedncached; void**avail; }; //tcache_bin_s就是tcache_bin_t 以上这些结构体的内存,是通过arenas[0]分配得到的。每个线程的TSD中也会存着指向这些结构的指针。
内存中的tbin与其avail指针
如何从线程中找到tcache,x0就是线程结构体的地址,其中key_data就是线程特有数据(也叫TSD)的指针,所以这里存放的就包含了tcache的地址。从shadow中可以看到TSD是在size类型为0x80的run中的。
TSD中存放的tcache与arena的示意。从shadow中可以看到tcache是在size类型为0x1c00的run中的。
如果把tcache溢出了?
这些信息在arenas[0]中存放tcache在size类型为0x1c00的run里分配,很难去找对并操作
但是这种情况有可能的
需要创建或者销毁线程
那如果吧TSD溢出了呢?
TSD在size类型为0x80的run里分配,很难去找对并操作
这种情况有可能,但是也难达到
需要创建或者破坏线程相关信息
小结:jemalloc内部结构在堆中的布局
jemalloc中固定的部分有
arena的大小
tcache的大小
arena与线程的关联部分(比如TSD)的大小
结构地址随机化
但是有一点值得注意,线程缓存使得访问相邻的region更加容易
利用shadow搞事情!
基于double free的利用姿势
为什么要用这个呢,是因为之前我们没有在jemalloc里实践过这样的姿势
而且这个姿势在Android和Firefox都有通用的代码模式
可以很通用的使用
在第一次free对象后,控制之后的两次申请
只要申请相同大小就可以进行利用
double free的示例代码
申请到了0x7f8fed1000,看看此时的tcache。
0x7f8fed1000压入tcache
受我们控制的第二次申请,又拿到了0x7f8fed1000
地址还回去,但是指针你留下来。最后我们用这个函数指针跳向我们想去的地方
给函数指针赋值。
任意地址free的利用前提
不是简单的原型,通常是有缺陷的清理逻辑(比如对树中节点的移除)。
jemalloc对于free传入的地址没有很好的检查
Android加入的检查可以被绕过
释放后会把地址压入对应的线程缓存栈
释放时候页索引检查代码段:
chunk=(arena_chunk_t*)CHUNK_ADDR2BASE(ptr); if(likely(chunk!=ptr)){ pageind=((uintptr_t)ptr-(uintptr_t)chunk)>>LG_PAGE; #ifdefined(__ANDROID__) /*Verifytheptrisactuallyinthechunk.*/ if(unlikely(pageind<map_bias||pageind>=chunk_npages)){ __libc_fatal_no_abort(...) return; } #endif /*chunksize_mask=chunksize-1*/ #defineLG_PAGE12 #defineCHUNK_ADDR2BASE(a)((void*)((uintptr_t)(a)&~chunksize_mask))再来看看chunk的排布
chunk中对于mapbits的检查
mapbits=arena_mapbits_get(chunk,pageind); assert(arena_mapbits_allocated_get(chunk,pageind)!=0); #ifdefined(__ANDROID__) /*Verifytheptrhasbeenallocated.*/ if(unlikely((mapbits&CHUNK_MAP_ALLOCATED)==0)){ __libc_fatal(...); } #endif if(likely((mapbits&CHUNK_MAP_LARGE)==0)){ /*Smallallocation.*/ /*...*/ #defineCHUNK_MAP_ALLOCATED((size_t)0x1U) #defineCHUNK_MAP_LARGE((size_t)0x2U)把这两个检查绕过,就可以任意地址进行free了,当然我们就可以传入一个从run中拿到的地址。也就是说,我们可以释放并往tcache里面压栈一个非对齐的region指针,但是有一个字节会被破坏。最后重新申请被free的region就会导致溢出到下一个region,如下图所示。
利用案例
boot.oat 里面有Android框架层的所有编译的native代码,在启动时候随机化加载。
boot.art 装载着一系列栈初始化类信息,以及相关的对象。
加载地址对每一个设备来说地址固定,由第一次启动时决定
包含着指向boot.oat的指针
在boot.art中我们找到一个函数指针0x713b6c40,我们先来分别计算mapbits,以及pagind,可以看到其绕过了这两个检查,注意64bit下的一些常量。
利用流程
1. 把这个在boot.art中指向boot.oat的地址通过free压入缓存栈
2. malloc后从缓存栈中弹出这个地址
3. 把想要控制的PC的值写进新申请的memory里面,覆盖某个当前的函数指针
4. 等风来,调用这个函数指针。
如何找boot.art中的地址
用shadow的jefreecheck找到可以被free的地址
确保这个地址中存储的函数指针会被调用
(gdb)jefreecheck-b0boot.art searchingsystem@framework@boot.art(0x708ce000-0x715c2000) [page0x712cf000] +0x712cf000 +0x712cf028 +0x712cf038 +0x712cf060 +0x712cf070 ...为了举例方便,在这里面我们用gdb直接向malloc得到后的问题地址0x713b6c40写入非法值。可以看到0x713b6c40这个地址存储的是一个函数指针。
free这个地址后,通过malloc再获得这个地址,然后向这个地址所指向的内存写一些值,比如AAAAA,我们便成功的控制了PC。
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:https://census-labs.com/media/shadow-infiltrate-2017.pdf