标签:技术分享 alt fast images 判断 理论 set 数据段 png
漏洞原理
二次释放
如何在第二次释放前修改函数地址
fastbin的特性
修改函数指针流程
如何获得进程的加载基址
格式化字符串漏洞
确定printf函数在代码段中偏移
printf函数输出想要的地址
如何获得system函数的地址
寻找一个被fheap进程调用并且和system函数处于同一个so库的函数A
通过读取函数A在got.plt中相应位置的值获得函数A的地址
通过读取dynsym段中的信息计算system相对于函数A在so库中的偏移
实际运行效果
小结
参考资料
程序自己实现了一套管理字符串的体系,但是在释放的时候用指针是否为空来判断该索引代表地方是否存放有字符串,如果指针不空,表示可以释放。但是释放完后,没有将指针置空,因此导致可以二次释放,多次释放。
图1 漏洞触发原理
根据[参考资料一]的介绍,fastbin维护的chunk分九个档次,大小从16字节到80字节,每8个字节一个档次。那我们要求的0x20(32)个字节,属于48字节的档次(因为每个chunk还要加上16字节的管理区),所以我们申请0x20空间后释放的chunk被归到fastbin[5]这个链表中了。
图2 创建四个字符串之后的内存布局
在heap中分配的8个块是从上到下在heap中依次排列的。之所以中间没有连续排列,是因为每个块前面都有16个字节的chunk管理区没有画出来。
图3 删除四个字符创后内存布局
由于glibc中释放内存是将16到80个空间的chunk直接添加到fastbin数组中的链表里面,所以能在fastbin[5]代表的链表中找到我们释放的内存块。而fastbin中的链表的进出规则是先进后出,所以最先释放的str0相关的块在链表尾,而最后释放的str3相关的块在链表头的位置。详情见图3。
在释放完字符串后,再创建一个0x80大小的字符串,由于str_manage的大小就是0x20,所以str_manage存放空间还是从fastbin中分配,此时的str0_manage就和原来的str3_manage重合了,但是由于其需要存放字符的空间是0x80,所以这次存放数据的chunk块就没有从fastbin中获取了,而是接着上述8个块之后再分配的。这样,我们再创建的字符串str1_manage就和原来的string3重合,string1就和原来的str2_manage重合了,那我们此时写入的字符串str1就会覆盖原来的str2_manage结构,那么我们再次释放str2时候,依然会将str2_info_ptr指向地址的后8个字节解释为函数指针并且调用它。我们写入想执行的函数,delete str2就会执行我们的函数。
图4 修改release_ptr方式
此时我们已经可以让程序执行我们需要的函数了。但是我们要执行的函数地址是多少呢?首先说明,release_ptr处填的值是fheap程序的函数,也就是这个值是在代码段中的。我们先看看进程运行起来后的内存分布图。
图5 fheap运行后的内存区域分布
可以看到,此时堆还没有分配,而libc库加载到0x7FFFF7A0E000处,但是我们能保证libc库每次都加载到相同的地址吗。不能,那现在我们有什么信息呢?我们有源程序,我们知道代码段中每条指令相对于进程基址的偏移,如果我们只修改release_ptr处的低位字节,那么我们可以调到代码段中任意一条指令处。
那么我们让程序执行到哪里比较有用呢?或者说用什么方式能获得程序的加载基址,以便后续工作开展呢?[参考资料二]的作者使用格式化字符串方式获取程序基址。
[参考资料三]总有很详细的介绍,此处不再赘述,简单说一下原理。我们在使用printf时候,往往会按照正常形式先传递一个格式化字符串,再传递我们要打印的数据作为参数。但是,如果我们只传递格式化字符串,而不传递参数呢?见下图。
图6 格式化字符串漏洞示意
会发生什么事呢?printf会按照格式字符解释arg1之后的数据。
此处,我们仅仅是读取了栈上的数据,至于如何读任意地址的数据,还是见[参考资料三],原理是一样的。只不过不适合我们这个实例。
由于位置无关代码(PIC)技术的使用,printf这种实现在程序外的动态库中的函数都被编译器在本地.plt段添加了一个代理函数,具体原理见[参考资料四]。所以我们要找到print在本地的代理函数。见下图。
图7 printf_plt
我们将0x9D0填充到release_ptr的低两个字节,就可以让程序控制流到printf_plt了。从而可以完成读取任意地址处的数据。
那我们怎么获取进程加载基址呢?回想一下,当程序控制传递到release函数时候,此时是在delete 函数中,而delete函数是在main中被调用,所以栈中肯定有返回main中的地址。此时我们需要的仅仅是动态调试一次,看看执行release函数之前栈中保存返回main函数地址的位置。
图8 执行release函数之前堆栈中的信息
可以发现,删除之前我们输入的确认字符串”yes.aaaabbbbbbbbcccccccc”已经放在栈中了。剩下的就是计算要获取的ret_mian_addr在栈中的位置。可以看到ret_mian_addr在栈中0x6F8处,而栈顶在0x5D0处。相当于偏移了多少个参数呢?偏移了(0x6F8-0x5D0)/8=37个参数。
但是,这是在64位机器上的程序,不要忘了,调用函数的规则还包括前六个参数保存在六个寄存器中。所以总数还要加上6,即37+6=43。
那么,我们要打印43个参数才能得到我们要的数据吗?还好,[参考资料三]中已经介绍了一种方法,打印指定位置的值,即:”%参数位置$格式”。例如,我们要以地址的格式打印第43个参数,可以这样写:“%43$p”,确实很方便。
这样,我们能获取ret_main_addr。
那怎么用这个地址呢?这个地址处于main函数的范围,我们在ida里面可以看到这个地址距离进程起始地址偏移为0xCF2。
所以进程加载基址就是ret_main_addr-0xCF2。
我们要打开一个shell,就要获得system函数的地址。system函数是在libc库中。但是这个库每次加载的地址可不是唯一的,我们如何获得libc库中函数的地址呢?
要是我们能够通过进程本身,得到libc库中任意变量或者函数的地址,我们就能通过查看libc符号表知道所有变量或者函数的地址了。
也许read这个函数可以联系进程和库(此处我也是学习者,积累技巧)。
那么怎么获取read函数的地址呢?位置无关代码(PIC)技术的使用,使得我们能够实现需求。PIC技术要通过编译器在elf文件中加入GOT表来实现。见[参考资料四],写的真好,还有实例。GOT表中存放的是什么呢?就是程序调用的动态库中函数的地址。对于read而言,放的就是read函数的实际地址。那么read在GOT表中什么位置呢?由于read是函数,所以它的位置在got.plt中的某一个位置offset,而在加载时候,offset处填充的是read函数在本地的代理read_plt函数的第二条指令的地址。不是read在libc中实际地址,所以它会被动态加载器在合适(见[参考资料四]讲解合适的意思)的时候再次修订。那么在重定位信息中,必然有read在got.plt的地址。我们查看重定位信息。
见下图。
图9 read函数的重定位信息
可以看到,在相对进程加载基址0x202058处会被动态加载器填上read函数的实际地址。我们获得了进程的加载基址,那就能知道代码段和数据段中的任意数据。
此时我们已经知道了read函数的地址,那么怎么获取system函数的地址呢?read和system的距离肯定是一定,知道了相对距离,就可以知道system的地址。
在动态库的.dynsym段中记录了每个可以被其他程序或者库调用的的符号地址,这个地址被写成相对于逻辑地址0的偏移。就是用来方便动态加载器修改GOT表的。我们看看read和system在.dynsym中的信息。
图10 read函数在dynsym中的信息
图11 system函数在dynsym中的信息
由这两个信息,可以计算出system相对于read的偏移是-0xb12e0。
有了system地址,我们通过创建字符串的方式修改release函数指针,而字符串其实地址会被传递过去,这样,创建的字符串开始部分可以写成”/bin/sh”,当做system开启shell的参数。当我们再次delete str2的时候,就会执行system函数,进而开始一个shell。
图12 成功开启shell
和别人的差距挺大的,花费了15个小时才明白[参考资料二]中作者的意图。之前只是拥有正向的知识和理解,我感觉分析了这一题,不仅将以往学习的理论知识运用了一下,也好像打开了另一种思想世界的大门。
本次涉及到知识点:
[1] glibc内存管理介绍:
[2] fheap漏洞利用程序:
http://bobao.360.cn/ctf/detail/179.html
[3] 格式化字符串漏洞:
[4] 《深度探索Linux操作系统:系统构建和原理解析》王柏生
hctf2016 fheap学习(FlappyPig队伍的解法)
标签:技术分享 alt fast images 判断 理论 set 数据段 png
原文地址:http://www.cnblogs.com/shangye/p/6156350.html