Off-by-One

堆溢出之off by one

介绍

严格来说off-by-one漏洞是一种特殊的溢出漏洞,off by one 指向缓冲区中写入时,写入
的字节数超过了这个缓冲区本身所申请的字节数并且只越界了一个字节。

off by one 漏洞原理

off by one是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,
当然也不排除写入的size就只多了一个字节的情况。其中边界验证不严通常包括:

①使用循环语句向堆块中写入数据时,循环的次数设置错误导致多写入了一个字节
②字符串操作不合适

一般来说,单字节溢出被认为是难以利用的,但是因为Linux的堆管理机制ptmalloc验证的
松散性,基于linux堆的off-by-one漏洞利用起来并不复杂,并且威力强大。此外,需要说明
的一点是off-by-one是可以基于各种缓冲区的,比如栈、bss段等,但在堆上的off by one
是CTF中比较常见的。

off-by-one利用思路

1、溢出字节为可控制字节:通过修改大小造成结构之间出现重叠,从而泄露出其他块数据,
或是覆盖其他块数据。也可使用NULL字节溢出的方法。

2、溢出字节为NULL字节:在size为0x100的时候,溢出NULL字节可以使得prev_in_use位
被清,这样前块会被认为是free块。
(1)这时可以选择使用unlink方法进行处理
(2)另外,这时prev_size域就会启用,就可以伪造prev_size,从而造成块之间发生重叠。此
方法的关键在于unlink的时候没有检查按照prev_size找到的块的大小与prev_size是否一致。

示例1
C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int my_gets(char *ptr,int size)
{
int i;
for(i=0;i<=size;i++)
{
ptr[i]=getchar();
}
return i;
}
int main()
{
void *chunk1,*chunk2;
chunk1=malloc(16);
chunk2=malloc(16);
puts("Get Input:");
my_gets(chunk1,16);
return 0;
}

my_gets函数导致了一个off by one 漏洞,原因是for循环的边界没有控制好导致写入多执行
了一次,这也被称为栅栏错误。

我们使用gdb对程序进行调试,在进行输入前可以看到分配的两个用户区域为16字节的堆块

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <=== chunk2
0x602030: 0x0000000000000000 0x0000000000000000

当我们执行my_gets进行输入之后,可以看到数据发生了溢出覆盖到了下一个堆块的prev_size
域print ‘A’*17

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021 <=== chunk1
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x0000000000000041 0x0000000000000021 <=== chunk2
0x602030: 0x0000000000000000 0x0000000000000000

示例2
第二种常见的导致off-by-one的场景就是字符串操作了,常见的原因就是字符串的结束符
计算有误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(void)
{
char buffer[40]="";
void *chunk1;
chunk1=malloc(24);
puts("Get Input");
gets(buffer);
if(strlen(buffer)==24)
{
strcpy(chunk1,buffer);
}
return 0;

}

程序咋看上去没有任何问题,但是strlen和strcpy的行为不一致却导致了off-by-one的发生。
strlen是我们很熟悉的计算ascii字符串长度的函数,这个函数在计算字符串长度时是不把结束符
‘\x00’计算在内的,但是strcpy在复制字符串时会拷贝结束符’\x00’。这就导致了我们向chunk1
中写入了25个字节,我们使用gdb进行调试可以看到这一点。

1
2
3
0x602000:   0x0000000000000000  0x0000000000000021 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000411 <=== next chunk

我们输入’A’*24后执行strcpy

1
2
3
0x602000:   0x0000000000000000  0x0000000000000021
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x4141414141414141 0x0000000000000400

可以看到next chunk的size域低字节被结束符’\x00’覆盖,这种有属于Off by one 的一个分支
称为NULL byte off by one ,在后面我们会看到off-by-one与NULL byte off-by-one在
利用上的区别。还是有一点就是为什么是低字节被覆盖呢,因为我们通常使用的CPU的字节
序都是小端法的,比如DWORD值在使用小端法的内存中是这样存储的

1
2
DWORD 0x41424344
内存 0x44,0x43,0x42,0x41

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!