off-by-one漏洞原理
off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括
- 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节。
- 字符串操作不合适
一般来说,单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制 ptmalloc 验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。 此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况。
off-by-one利用思路
- 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
- 溢出字节为 NULL 字节:在 size 为 0x100 的时候,溢出 NULL 字节可以使得
prev_in_use
位被清,这样前块会被认为是 free 块。(1) 这时可以选择使用 unlink 方法(见 unlink 部分)进行处理。(2) 另外,这时prev_size
域就会启用,就可以伪造prev_size
,从而造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照prev_size
找到的块的大小与prev_size
是否一致。
实例1:Asis CTF 2016 b00ks
静态分析
程序基本情况如下,为64位程序,开启了RELRO、NX、PIE,程序提供了创建、删除、编辑、打印图书的功能。
IDA看一下,首先是create,创建book时,name和description在堆上分配。首先使用malloc分配name buffer,大小不超过32;之后,分配description buffer, 大小自定义;最后分配book结构体,用于保存book的信息。
分析图中可知第一个+6其实就是4*6=24 =0x18,后面以此类推(qword=8个字节)。其中unk_202024
存储的其实就是bookID
get_bookID
获取的其实是在array_ptr(其实就是多个book_struct)中为0的索引(应该是为了方便后面放数据)
点进my_read
函数看一下,可以看到当输入数据的长度正好为size时,会向ptr中越界写入一个字节\x00
。
以上分析总结如下图
漏洞利用
当申请内存小于128k就会使用brk,大于128k的时候就会使用mmap,mmap开辟出的块与libc基址的偏移是固定的,因此只要拿到mmap开辟出的chunk的地址,就能通过一个“固定的偏移”得到libc
看一下主函数一开始的一个函数
从上图看到my_read函数会读32个字节的数据到author_name(可以看到当输入数据的长度正好为32时,会向author_name中越界写入一个字节\x00
。)
printf函数用%s打印author_name,如果没有遇到\x00
会一直输出,甚至输出array_ptr的内容。因此,如果名字长达32字节,就能够泄露出第一个book结构的地址。通过打印 author name 就可以获得 array_ptr 中第一项的值。
为了实现泄漏,首先在 author name 中需要输入 32 个字节来使得结束符被覆盖掉。之后我们创建 book1 ,这个 book1 的指针会覆盖 author name 中最后的 NULL 字节,使得该指针与 author name 直接连接,这样输出 author name 则可以获取到一个堆指针。
程序中同样提供了change_name 函数, 用于修改 author name ,所以通过 该函数可以写入 author name ,利用 off-by-one 覆盖 array_ptr 第一个项的低字节。
覆盖掉 book1 指针的低字节(bookId)后,这个指针会指向 book1 的 description ,由于程序提供了 edit 功能可以任意修改 description 中的内容。我们可以提前在 description 中布置数据伪造成一个 book2结构,使得其中的book1_description_ptr指向book2_description_ptr;这样通过先后修改book1_description和book2_description,从而实现任意地址写任意内容的功能。
我们已经获得了任意地址读写的能力,但是这个题目开启 PIE 并且没有泄漏 libc 基地址的方法,在分配第二个 book 时,使用一个很大的尺寸,使得堆以 mmap 模式进行拓展。我们知道堆有两种拓展方式一种是 brk 会直接拓展原来的堆,另一种是 mmap 会单独映射一块内存。
在这里我们申请一个超大的块,来使用 mmap 扩展内存。因为 mmap 分配的内存与 libc 之前存在固定的偏移因此可以推算出 libc 的基地址。
payload
第一个框就是我们输入的32个a(author_name),第二个框就是book1_struct_ptr,第三个框就是book2_struct_ptr。两者相差0x30,详细查看一下其中的内容,可以看到保存的分别是Id,name_ptr,des_ptr,des_size。
由于该程序启用了FULL RELRO
保护措施,无法对GOT
进行改写,但是可以改写__free_hook
或__malloc_hook
。
将__free_hook
指向的内容修改为system
的地址,在调用free
函数时,由于__free_hook
里面的内容不为NULL
,从而执行指向的指令。
__free_hook
参考如下:
执行edit修改的是book_des_ptr和book_des_size的内容
因为book1的des指向book2的des处,将该处改为__free_hook
地址,那么写book2的des时就会往__free_hook
处写入
1 | from pwn import * |
参考链接:bilibili这个讲的很清楚