pwnable.tw新手向write up(一)
pwnable.tw新手向write up(二) 3×17-x64静态编译程序的fini_array劫持
pwnable.tw新手向write up(三) dubblesort-多重保护下的栈溢出
如果你没做过堆类型的题目,或者对chunk,fast bin这些内容还不怎么清楚的话,可以先看看这个:《glibc内存管理ptmalloc源代码分析.pdf》,作者是华庭,具体文件我会上传到附件的.
chunk的实际大小与申请的内存大小并不相同
我们所有使用malloc或者其他方式申请的内存空间,ptmalloc都使用chunk来表示,释放掉的内存也不是直接归还给操作系统,而是依旧用chunk来管理.所以chunk在使用中和释放后是两种不同的结构.首先是使用中:
前两个字节为chunk header,第三个字节开始,也就是mem指针指向的地方开始,就是我们申请的内存(大小等于我们申请的大小),malloc函数返回值就是mem指针的值.接着看看被释放的chunk:
user data被替换为了两个指针,fd指向前一个空间的chunk,bk指向后一个,如果是large bin的话还会多两个指针,不过这里就不提了.
现在我们已经知道了chunk的构造,那么如何通过申请内存的大小来计算chunk的实际大小呢?以32位系统为例,一个free掉的chunk有四个字段,分别是prev_size,size,fd,bk,也就是16个字节,所以一个chunk的大小最小为16字节.再看看使用中的chunk,因为是使用状态,所以下一个chunk的prev_size就是无效的,所以这个地方也可以被当前chunk使用.
这样就可以得知,chunk的in_use_size=(用户请求大小+8-4) align 8B.稍微解释一下,从左到右,用户请求大小就是我们申请的大小,+8是因为需要两个字段来保存chunk header,-4就是因为当前chunk可以使用下一个chunk的prev_size,节省了4字节的大小,对齐8B则是ptmalloc的要求.
所以,最终的分配空间大小chunk_size = max(in_use_size,16).下文中及exp中用到的大小都是chunk的最终分配大小.
Fast Bins 简介
glibc采用单向链表对其中的每个 bin 进行组织,并且每个bin采取LIFO策略,也就是和栈一样,最近释放的chunk会被更早的分配.不同大小的chunk也不会被链接在一起,fastbin支持从16字节开始的10个相应大小的bin.
glibc中的首次适应(First Fit)算法:空间分区以地址递增的次序链接,分配内存时顺序查找,找到大小能满足要求的第一个空闲分区.
如果分配内存时存在一个大于或等于所需大小的空间chunk,glibc就会选择这个chunk.举个例子:
Use After Free
当一个内存块在被释放之后,执行他的指针应该置0,不然就会变成危险的悬挂指针,使得这个内存块被再次申请出去之后(比如刚刚说的First Fit),我们还可以对内容进行操作,比如我们将数据进行构造之后,再次利用这个不合法的悬挂指针,就可能造成任意地址读写.
看一下防护,只开了NX和Canary
ida分析一下程序结构,比较短小.
main函数没什么好说的,常规的堆题目,打印一个菜单,分别提供创建,删除和打印笔记的功能,先进入add_func查看一下:
chunk_count限制使用add函数的次数.申请note的时候,程序先申请8字节内存,并且把地址保存在chunk_ptr[]中.这8个字节用来保存note结构,前四个字节保存一个函数指针,姑且命名为puts_func,这个函数将传入指针的下一个偏移地址的内容打印出来
后四个字保存一个指针,指向大小为size的内存,用来保存note的content.这样可能不是很直观,我用c语言展示一下note的结构:
puts_func_ptr指向puts_func这个函数,content_ptr则指向note的content,用来保存我们的输入.如果还是不直观的话,可以看看图:
这样应该很清晰了,假设我们创建一个大小为10字节的note,程序会先申请八个字节来保存note结构,接着再申请0x10字节作为real_content保存我们的输入.
delete_func函数先读取一个下标index,然后对相应的note进行free.这两个free比较晕,第一个free释放我们的rea_content,第二个free释放note结构,但是释放之后并没有对指针进行置0,造成了两个悬挂指针,我们可以对释放后的指针继续进行操作,所以这个地方造成了UAF.
show_func函数先读取一个下标index,接着调用*chunk_ptr[index]处的函数,也就是我们刚刚说到的puts_func,参数则是chunk_ptr[index],又因为puts_func实际打印的是传入参数的下一个地址,也就是note->content的内容,那么就会打印出我们输入的content.
利用思路
唯一的漏洞是delete_func函数里面存在UAF,哪怕一个note已经被我们删除,我们依然可以调用show_func函数来调用puts_func_ptr指向的函数,如果我们可以修改puts_func_ptr的值,那么我们就可以劫持程序的控制流了.具体如何修改我写到exp里边了,这里你先假设你已经可以修改了.
但是这个程序并没有后门函数,并且还给了libc.so文件,所以我们需要先泄露libc加载的地址,然后计算得到system函数和'/bin/sh'的地址.因为我们现在已经可以修改note中的两个指针,所以只要将content_ptr修改为got['puts']的地址就可以泄露libc基地址了
通过libc基地址可以计算出system函数的地址,然后继续修改一个note的puts_func_ptr就可以调用system函数了.这里对于system函数的参数设置还有一个坑,回顾一下show_func函数的内容:
此时chunk_ptr[index]保存的是note指针,chunk_ptr[index]也就是*note->puts_func_ptr,这会调用system函数,和我们预想的一样,但是参数同样是note指针,puts_func自身进行了四个字节的偏移,所以才能顺利打印出content的内容,system函数并不会进行偏移,自然也就无法执行system('/bin/sh').
我们可以考虑构造note指针本身,它本身作为参数,前四个字节无法被系统识别,只要在后四个字节加入一个';',就可以自由执行命令了,举个例子:
回到题目,我们的system函数地址无法被识别,但是分号后边如果加一个sh就可以顺利执行了
exp
PS:如果你的IDA和我反汇编出来代码不一样,那是因为我进行了变量重命名和函数类型修改,还创建了一个note结构体,不过这个影响应该不大.
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-8-12 20:23
被0x2l编辑
,原因: 修改