内容简介:学习pwn绕不开Linux的堆,找到了有人翻译的shellphish团队在Github上开源的堆漏洞教程。
0x00 前言
学习pwn绕不开 Linux 的堆,找到了有人翻译的shellphish团队在Github上开源的堆漏洞教程。
里面有github地址以及《Glibc内存管理-Ptmalloc2源码分析》的地址,我就不贴了,另外安利一本《程序员的自我修养》
本文是我在学习how2heap遇到的一些坑,做了一些整理,最主要的是因为glibc-2.26之后引入了tcache机制,导致刚开始学习时,发现运行结果和说好的不一样,N脸懵逼。
0x01 准备工作
how2heap的代码要使用不同的版本glibc进行实验,因此提供了glibc_run.sh,使用方法
glibc_run.sh <version> <code>
栗子:
glibc_run.sh 2.25 ./glibc_2.25/unsafe_unlink
不过由于还要使用gdb调试,就不能依赖glibc_run.sh脚本了,看下脚本执行的内容,栗子执行的命令就是 LD_PRELOAD="./glibc_versions/libc-2.25.so" ./glibc_versions/ld-2.25.so ./glibc_2.25/unsafe_unlink
使用不同的libc可以通过环境变量LD_PRELOAD解决,在gdb中可以通过命令 set exec-wrapper env "LD_PRELOAD=./libc-2.25.so"
解决,现在就剩下链接器了,在看雪上看到有人分享了 Python 脚本修改程序使用的ld
0x02 tcache
这里只是初步展示下tcache,一个长度为64的链表数组,每个链表的长度最大是7,链表类似于fastbin,通过fd连起来,不过fastbin中fd指针是指向下一个chunk的首地址,而tcache是指向下一个chunk的fd的地址。
这里以64位为例,tcache是个结构体,里面有count数组和指针数组,长度都是64,根据下标一一对应,结构体的声明是参考的: https://www.anquanke.com/post/id/104760
/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry; /* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */ typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; static __thread tcache_perthread_struct *tcache = NULL;
数组长度是64,存储的chunk大小是从32~1040字节,递增16字节,所以是数组下标从0开始,那么对应chunk大小就是(下标+2)*16
这个缓存链表类似于fastbin,每组链表节点长度为7,对应chunk释放后加入链表时,对应的in_use状态不变(状态位在内存相邻的下一个chunksizi字段上),因此内存地址上连续的两个chunk都释放了,也是不会合并的。
链表是通过fd指针相连的,fd也是指向另一个chunk的fd指针的地址,顺序是先进后出。
进行两个实验,实验一是为了证明数组大小,对应的chunk范围以及chunk是先进后出,实验二是为了证明链表长度最大是7。
实验一:
chunk大小范围是32-1040
因此申请的最小chunk设为16,最大chunk设为1024
a[0]=malloc(16)
a[1]=malloc(16)
a[2]=malloc(1024)
再按顺序释放a[0]、a[2]、a[1],然后再申请一次a[3]=malloc(16)
申请过3个内存之后的heap
现在释放a[0]和a[2],tcache也是申请的一块堆内存上存放的,因此查看申请的第一块内存的数据,tcache的chunk是从0x602000开始的,前16字节是chunk头,之后的64字节是count数组,用来记录对应下标链表的个数,之后的每8个字节是一个链表,总体是链表数组,长度是64。count数组的最低位和最高位分别记数了1,链表里存放的是a[0]和a[2]的fd指针的地址。
现在再释放a[1],tcache链表数组的第一个链表会指向a[1]的fd指针,也就是0x6020a0,而a[1]的fd存储的是a[0]的fd地址,并且count数组对应的值会变为2(对应count数组中的下标为0)
之后再去申请16字节的内存,那么会从tcahce中获取,按照FILO的原则,a[1]对应的chunk会先被选中,因此a[4]与a[1]的值是一样的
实验二:
申请a[0]~a[8]一共9个chunk,依次释放,会发现前7个在tcache中,后两个会在fastbin上。
先释放a[0]~a[6]
查看tcache,现在链表长度是7,fastbins中没有chunk
现在继续释放a[7],tcache是没有变化的,a[7]进入fastbins
继续释放a[8],a[8]也加入了fastbins中
接着看下a[7],a[8]的fd是指向的chunk头还是fd
fastbins中的单链表还是指向的chunk头地址。
最后再申请一个16字节内存,应该是从tcahce中获取
0x03 first_fit
由于how2heap的源代码内还有说明,为了调试看的简单点,做了一些删减
/*first_fit.c*/ #include<stdlib.h> #include<string.h> int main() { char *a = malloc(512); char *b = malloc(256); char *c; printf("1st malloc(512) %pn",a); printf("2nd malloc(256) %pn",b); strcpy(a,"this is A!"); free(a); printf("free an"); c = malloc(500); printf("3rd malloc(500) %pn",c); strcpy(c,"this is C!"); printf("copy string 'this is c' to cn"); printf("c(%p):%sn",c,c); printf("a(%p):%sn",a,a); return 0; }
编译: gcc -g -no-pie -o first_fit first_fit.c
,加入源码信息,关闭PIE。
使用不同版本glibc,通过chaneld脚本修改程序使用的ld,这些是实验前的准备工作,之后不提了。
first_fit是先申请了a,b两块内存,b是为了防止a释放后被top chunk合并,释放了a之后,再去申请c,c的大小小于a,那么c对应内存的首地址与a的一致,如果a存在UAF漏洞,就可以做一些事了(我不知道是什么事),在glibc-2.26之前,a的大小已经是大于fastbin了,所以释放后是进入unsorted bin,那么在申请c的时候,会将a的chunk做切割,然后分配给c,因此c与a的首地址相同,而在glibc-2.26的版本中,a是进入了tcache,只有chunk大小完全匹配,才会将a分配给c。
0x04 fastbin_dup
删减过的代码
#include <stdio.h> #include <stdlib.h> int main() { int *a = malloc(0x20); int *b = malloc(0x20); int *c = malloc(0x20); fprintf(stderr, "1st malloc(8): %pn", a); fprintf(stderr, "2nd malloc(8): %pn", b); fprintf(stderr, "3rd malloc(8): %pn", c); free(a); free(b); free(a); fprintf(stderr, "1st malloc(0x20): %pn", malloc(0x20)); fprintf(stderr, "2nd malloc(0x20): %pn", malloc(0x20)); fprintf(stderr, "3rd malloc(0x20): %pn", malloc(0x20)); }
实验是将同一个chunk释放两次,对于fastbin不能连续调用两次free(a),因为会检测当前链表头部的chunk与释放的chunk是否为同一个,而在glibc-2.26版本中,只是进入了tcache中,没有多大区别。
但是现在可以把free(b)这一句删除,在glibc-2.26之前的版本,会触发检测到double free的错误
在tcache中则没有检测了,可以将同一块内存同时释放两次。
代码执行效果
a对应chunk(0x602250)中fd指针是指向自己的地址的
0x05 fastbin_dup_into_stack
修改过的源码
#include <stdio.h> #include <stdlib.h> int main() { unsigned long long stack_var; fprintf(stderr, "The address we want malloc() to return is %p.n", 8+(char *)&stack_var); int *a = malloc(8); int *b = malloc(8); int *c = malloc(8); free(a); // free(b);//glibc-2.26版本不需要在free(b)了 free(a); unsigned long long *d = malloc(8); fprintf(stderr, "1st malloc(8): %pn", d); fprintf(stderr, "2nd malloc(8): %pn", malloc(8));//这一句glibc-2.26中也可以删除的,不删除也无所谓,不修改fd指针之前,是循环返回a的地址。 // stack_var = 0x20; //glibc-2.26版本不需要满足size字段在对应范围内 *d = (unsigned long long) (((char*)&stack_var) + sizeof(d)); //2.26版本之后这里是+,2.25版本是-,因为stack_var变量是伪造chunk的size字段,2.26中fd指向的是另一个chunk的fd,所以是size字段+8,而2.25是指向chunk头,所以是size字段-8 fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free listn", malloc(8)); fprintf(stderr, "4th malloc(8): %pn", malloc(8)); }
这个算是上一个的延续,通过将一个内存块释放两次,那么申请一个出来,就可以修改其fd指针了,控制之后申请返回的地址为任意地址,fastbin的话还需要满足地址+8(也就是size字段)处的值是在对应fastbin范围内,比如栗子中fastbin的大小是0x20,那么指定的其他地址处,size字段值应该是0x20~0x2f。但是对于tcache来说就简单多了,没有double free检测,没有size字段的检测。
gdb调试
申请过d之后,0x405260还在tcache中,用户使用内存就是chunk的从fd开始的,因此直接修改*d的值即可
fd已经指向了我们希望返回的地址了,先申请一个chunk将d对应的chunk卸下,查看tcache
最后成功返回指定的地址
PS:house_of_lore对于glibc-2.26版本来说没什么区别,因为代码一个是利用fastbin,一个是利用small bin,但是tcache范围较大,就都在tcache内了。
0x06 unsorted bin attack(glibc-2.25)
unsorted bin attack其实就是泄露unsorted bin 的地址,根据unsorted bin的地址计算出malloc_area的地址(main_area=unsorted bin – 0x58),从而推算出libc_base的加载地址,那么就能去获取one_gadget的实际地址,或者是知道system函数地址之类的。因为针对unsorted bin,所以和tcache无关,因此这部分内容其实和标题也无关,算个彩蛋?
这里整理了三个方法,其实就是三种读取到unsorted bin地址的方法。
方法一
how2heap的代码给了一个思路是,将内存释放进入unsorted bin之后,假设这个chunk为p,修改p的bk地址为一个可获取内容的变量地址-2size_t(代码中是一个栈变量),那么再申请一个chunk p大小所对应的的内存,会将p从链表中取出,此时
p->fd = unsorted bin 头部 = FD
p->bk = 栈变量-2 size_t = BK
卸下链表的操作就是
FD->bk = BK (这个是导致unsorted bin之后申请的内存为栈地址)
BK->fd = FD(此时BK->fd 就是BK的地址加上2*size_t,所以就是栈变量地址,那么就是获取了unsorted bin 的地址)
方法二
UAF的方法,当chunk进入unsorted bin,直接输出fd或者bk的值(之前如果是空的话),如果之前unsortedbin有值,那么最好还是输出bk的值保险(合并的情况排除 )
方法三
有两个连续的chunk p和q,其中p的大小是大于fastbin的,那么free之后是会进入unsorted bin的,然后修改p的大小(通过溢出漏洞,如果没有这个方法就没办法用了)为p+q的大小(即理解为p和q是合并为一个chunk了),那么再去申请一个chunk p对应大小的内存,会将假装合并的p+q的chunk分隔出来,将p分配出去,从而修改了chunk q的fd和bk,会指向unsorted bin,那么去读取chunk q的fd或者bk即可。
方法一示例
方法一就是how2heap内的源码,未做修改,直接看下过程。
先申请一个chunk p,再申请个malloc(500)是为了防止free(p)时,p被top chunk合并。
free(p)之后,chunk 进入unsorted bin
然后修改p的bk指针为栈变量地址-2site_t(也可以修改p的fd指针为栈变量地址-3size_t)
然后申请一个chunk p 对应的大小,会将p从链表中卸下,此时栈变量存储的就是unsorted bin的地址了。
方法二示例
UAF,演示代码
#include <stdio.h> #include <stdlib.h> int main(){ unsigned long *p=malloc(400); malloc(500); free(p); printf("p[1] is at %p:%pn",p,(void*)p[1]); }
emmm,没什么说明的了。
方法三示例
通过修改unsortedbin中chunk的size,达到分割chunk来获取unsorted bin的地址
源码
#include <stdio.h> #include <stdlib.h> int main(){ unsigned long *p=malloc(0x100); unsigned long *q=malloc(0x40); free(p); *(p-1) = 0x161; printf("free(p) and p's value is %pn",(void *)p[0]); unsigned long *t = malloc(0x100); printf("t is at %pn",t); printf("p is at %pn",p); printf("q's value is %pn",(void *)q[0]); }
申请两个内存,p和q,对应chunk大小分别是0x110和0x50
此时修改p的size大小(其实只要p的大小+4*size_t就可以了,但是不想分割q了)为p+q的大小和,所以是0x160,再加上最低位表示前一个chunk在使用,所以修改值为0x161,此时查看heap可以看到q好像被合并了
再申请一个0x100的内存,就是再将p分割出来,此时q的fd和bk就是指向unsortedbin了,而此时q是从未释放过的,所以读取q的值算正常操作,因此能获取到unsorted bin的地址。
以上所述就是小编给大家介绍的《how2heap之glibc——2.26版本》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 查看linux内核版本和CentOS版本
- JavaScript版本如何与ECMAScript版本相关联?
- IJPay 0.8 版本发布,兼容低版本 JDK
- fastjson 1.2.55 版本发布,Bug 修复版本
- Apache Ignite 2.9.1 版本发布,小版本更新
- Swoole v4.6.1 版本发布,Bug 修复版本
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
从问题到程序-用Python学编程和计算
裘宗燕 / 机械工业出版社 / 2017-6-1
本书是以Python为编程语言、面向计算机科学教育中的程序设计基础课程与编程初学者的入门教材和自学读物。本书以Python为工具,详细讨论了与编程有关的各方面问题,介绍了从初级到高级的许多重要编程技术。本书特别强调编程中的分析和思考、问题的严格化和逐步分解、语言结构的正确选择、程序结构的良好组织,以及程序的正确和安全。书中通过大量实例及其开发过程,展示了好程序的特征和正确的编程工作方法。此外,书中......一起来看看 《从问题到程序-用Python学编程和计算》 这本书的介绍吧!