heap off-by-one
简介
严格来说off-by-one
漏洞是一种特殊的溢出漏洞, off-by-one
指程序向缓冲区写入时, 写入的字节数超过了这个缓冲区本身申请的字节数并且只越界了一个字节
原理
off-by-one
是指单字节缓冲区溢出, 这种漏洞往往与
边界验证不严和字符串操作有关
使用循环语句向堆块中写入数据时, 循环次数设错, 导致多写入一个字节。该错误也被称为栅栏错误
wikipedia: 栅栏错误(有时也称为电线杆错误或者灯柱错误)是差一错误的一种。如以下问题:
建造一条直栅栏(即不围圈), 长 30 米、每条栅栏柱间相隔 3 米, 需要多少条栅栏柱?
最容易想到的答案 10 是错的。这个栅栏有 10 个间隔, 11 条栅栏柱。
char buff[10];
for (int i = 0; i <= 10; i++) {
buff[i] = getchar();
}或者是错误使用 read 函数的返回值, 导致多写入一个字节
char buff[10];
buff[read(0, buff, 10)] = 0;- read 函数返回值返回实际读到的字节数
字符串操作不规范
不能排除写入的 size 正好就只多一个字节
一般来说, 单字节溢出被认为是难以利用的, 但是因为 Linux 的堆管理机制 ptmalloc
验证的松散性, 基于 Linux 堆的 off-by-one
漏洞利用起来并不复杂, 并且威力强大。
off-by-one 是可以基于各种缓冲区的, 比如栈、bss 段
等等, 但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。这里仅讨论堆上的 off-by-one 情况
利用思路
- 溢出字节为可控任意字节: 通过修改大小导致堆结构之间出现重叠, 从而泄露其他块数据, 或覆盖其他块数据。也可以使用 NULL 字节溢出方法
- 溢出字节为 NULL 字节:在 size 为 0x100 的时候, 溢出 NULL 字节可以使得
prev_in_use
位被清零, 这样前块就会被认为是 free 块- 可以使用 unlink attack 方法处理
- 通过伪造
prev_size
造成块间重叠, 此方法的关键在于 unlink 的时候没有检测按照prev_size
找到的块的大小与prev_size
是否一致
在最新版本代码中, 已加入针对 2 中的后一种方法的 check, 但是在 2.28 前并没有该 check
/* consolidate backward */ |
示例1
|
用 gdb 对程序调试, 在进行输入前可以看到分配的两个用户区域为 16 字节的堆块
pwndbg> x/16gx 0x555555559290 |
当我们执行my_gets
进行输入"a" * 17
之后, 可以看到数据发生了溢出覆盖到了下一个堆块的 prev_size
域
pwndbg> x/16gx 0x555555559290 |
查看chunkinfo
, 很明显看到chunk2-> pre_size
位被设置为了 0x61
pwndbg> chunkinfo 0x5555555592b0 |
示例2
第二种常见的导致 off-by-one 的场景就是字符串操作了, 常见的原因是字符串的结束符计算有误
|
程序乍看上去没有任何问题(不考虑栈溢出), 可能很多人在实际的代码中也是这样写的。 但是 strlen
和 strcpy
的行为不一致却导致了 off-by-one
的发生。 strlen
是我们很熟悉的计算 ascii 字符串长度的函数, 这个函数在计算字符串长度时是不把结束符 '\x00'
计算在内的, 但是 strcpy
在复制字符串时会拷贝结束符 '\x00'
。这就导致了我们向 chunk1 中写入了 25 个字节, 我们使用 gdb
进行调试可以看到这一点。
pwndbg> x/16gx 0x555555559290 |
在我们输入’A’*24 后执行strcpy
pwndbg> x/16gx 0x555555559290 |
可以看到 next chunk
的 size 域低字节被结束符 '\x00'
覆盖, 这种又属于off-by-one
的一个分支称为NULL byte off-by-one
, 我们在后面会看到 off-by-one
与 NULL byte off-by-one
在利用上的区别。 还是有一点就是为什么是低字节被覆盖呢, 因为我们通常使用的 CPU 的字节序都是小端法的, 比如一个 DWORD 值在使用小端法的内存中是这样储存的
DWORD 0x41424344 |