简介

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

原理

off-by-one 是指单字节缓冲区溢出, 这种漏洞往往与

  1. 边界验证不严和字符串操作有关

    1. 使用循环语句向堆块中写入数据时, 循环次数设错, 导致多写入一个字节。该错误也被称为栅栏错误

      wikipedia: 栅栏错误(有时也称为电线杆错误或者灯柱错误)是差一错误的一种。如以下问题:

      建造一条直栅栏(即不围圈), 长 30 米、每条栅栏柱间相隔 3 米, 需要多少条栅栏柱?

      最容易想到的答案 10 是错的。这个栅栏有 10 个间隔, 11 条栅栏柱。

      char buff[10];
      for (int i = 0; i <= 10; i++) {
      buff[i] = getchar();
      }
    2. 或者是错误使用 read 函数的返回值, 导致多写入一个字节

      char buff[10];
      buff[read(0, buff, 10)] = 0;
      • read 函数返回值返回实际读到的字节数
    3. 字符串操作不规范

  2. 不能排除写入的 size 正好就只多一个字节

一般来说, 单字节溢出被认为是难以利用的, 但是因为 Linux 的堆管理机制 ptmalloc 验证的松散性, 基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂, 并且威力强大。

off-by-one 是可以基于各种缓冲区的, 比如栈、bss 段等等, 但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。这里仅讨论堆上的 off-by-one 情况

利用思路

  1. 溢出字节为可控任意字节: 通过修改大小导致堆结构之间出现重叠, 从而泄露其他块数据, 或覆盖其他块数据。也可以使用 NULL 字节溢出方法
  2. 溢出字节为 NULL 字节:在 size 为 0x100 的时候, 溢出 NULL 字节可以使得 prev_in_use位被清零, 这样前块就会被认为是 free 块
    1. 可以使用 unlink attack 方法处理
    2. 通过伪造 prev_size造成块间重叠, 此方法的关键在于 unlink 的时候没有检测按照 prev_size找到的块的大小与prev_size是否一致

在最新版本代码中, 已加入针对 2 中的后一种方法的 check, 但是在 2.28 前并没有该 check

/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
/* 后两行代码在最新版本中加入, 则 2 的第二种方法无法使用, 但是 2.28 及之前都没有问题 */
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

示例1

#include <stdio.h>
#include <stdlib.h>

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;
}

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

pwndbg> x/16gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x5555555592a0: 0x0000000000000000 0x0000000000000000
0x5555555592b0: 0x0000000000000000 0x0000000000000021 <=== chunk2

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

pwndbg> x/16gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0x6161616161616161 0x6161616161616161 <=== chunk1
0x5555555592b0: 0x0000000000000061 0x0000000000000021
0x5555555592c0: 0x0000000000000000 0x0000000000000000 <=== chunk2

查看chunkinfo, 很明显看到chunk2-> pre_size 位被设置为了 0x61

pwndbg> chunkinfo 0x5555555592b0
==================================
Chunk info
==================================
Status : Used
Freeable : True
prev_size : 0x61
size : 0x20
prev_inused : 1
is_mmap : 0
non_mainarea : 0
pwndbg> chunkinfo 0x555555559290
==================================
Chunk info
==================================
Status : Used
Freeable : True
prev_size : 0x0
size : 0x20
prev_inused : 1
is_mmap : 0
non_mainarea : 0

示例2

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
char buffer[40] = "";
void *chunk1;
chunk1 = malloc(24);
puts("Get Input");
gets(buffer);
if (strlen(buff) == 24) {
strcpy(chunk1, buffer);
}
return 0;
}

程序乍看上去没有任何问题(不考虑栈溢出), 可能很多人在实际的代码中也是这样写的。 但是 strlenstrcpy 的行为不一致却导致了 off-by-one 的发生。 strlen 是我们很熟悉的计算 ascii 字符串长度的函数, 这个函数在计算字符串长度时是不把结束符 '\x00' 计算在内的, 但是 strcpy 在复制字符串时会拷贝结束符 '\x00' 。这就导致了我们向 chunk1 中写入了 25 个字节, 我们使用 gdb进行调试可以看到这一点。

pwndbg> x/16gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x5555555592a0: 0x0000000000000000 0x0000000000000000
0x5555555592b0: 0x0000000000000000 0x0000000000000411 <=== next chunk

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

pwndbg> x/16gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x5555555592a0: 0x4141414141414141 0x4141414141414141
0x5555555592b0: 0x4141414141414141 0x0000000000000400 <=== next chunk

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

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

例题

Asis CTF 2016 b00ks

plaidctf 2015 plaiddb

参考

CTF-wiki 堆中的 Off-By-One