0×00 序
好久没有写文章了,最近在学习pwn,这次就分享pwnable.tw上的一道pwn的解题思路。这篇文章主要目的并不是以做这道题为目的,而是以这个题为主线,我主要想讲的是通过这道题,我们能联想或者学会一些其他的东西,所以叫做从一道pwn题说起。如果有什么不对的地方,欢迎大家指出。
0×01 题目分析
题目本身难度并不大,正好适合刚接触堆的选手练习,快速掌握堆利用的知识和技巧,下面我们开始分析,首先运行程序,如下图所示,这是一道菜单题目,可以添加,删除和打印节点,这应该是一个堆上的pwn题。
然后可以使用IDA分析程序流程,首先看一下主函数,我对其中的一些函数做了重名命,这样可以对后面的分析更简单,更方便,让你更清楚程序的流程。
主函数分析
主函数的主要流程很清晰,读入你输入的选项,然后执行对应的函数,我们需要主要分析一下各个功能选项(如add,delete等)的详细流程。
Add函数分析
在分析Add函数时,这里我分析出来一个结构体,创建一些结构体对程序的数据结构进行描述,对你分析程序有很大的帮助。
00000000 st struc ; (sizeof=0x8, mappedto_5) 00000000 func dd ? 00000004 msg_ptr dd ? 00000008 st ends
Add函数首先会判断num的值已经申请的结构体数量,如果小于等于5就遍历存储结构体指针的数组,找到一个为空的项,然后申请一个8字节大小的堆块,然后将sub_804862B赋值给func也就是结构体的第一个成员,然后在读入一个size申请一个size大小的堆块,将其赋值给msg_ptr指针,然后读入一个字符串到msg_ptr (新申请的堆块),最后将结构体数量加1
Delete函数分析
相对于Add,delete函数的流程就简单的多了,读入要删除index,然后判断index是否合法,然后判断这个index对应的结构体指针数组的是否存在,如果存在就释放对应的结构体的msg_ptr和结构体指针
print函数分析
print函数也很简单和delete的类似,不同的是最后调用了func指针指向的函数,根据Add函数中的赋值,这个函数应该是sub_804862B,下面我们看一下这个函数
这个函数也很简单,就是输入msg_ptr指向堆块的内容。
0×02 漏洞分析
整个程序的流程上面已经分析清楚,主要的漏洞点,就在delete函数中,在释放指针之后没有将其赋值为空,这样会引起UAF和double free 漏洞。可能对新手来说,对堆上的漏洞很陌生,我这里简单的介绍一下,如果想要详细了解可以阅读文章最后的参考资料,要搞懂这些首先需要对堆的结构有一定了解,这些网上有很多文章。
UAF(use after free)释放重用漏洞,漏洞原理,释放后的指针没有赋值为空,在其他地方再次申请到这块内存并改变其的内容,而再次使用到之前释放后的指针,就会造成程序的结果变得不正确。如果这个释放的指针中有函数指针等重要数据,同时在其他的地方修改成精心构造的数据,就可能泄露数据,甚至劫持控制流。
double free 双重释放漏洞,漏洞原理,对释放的堆块再次进行释放,当然连续释放一块堆块,libc中有检查,这个是报double free的错,但是中间释放一个其他堆块,程序不会报错崩溃,这样就将double free转化成uaf,因为第一块和第三块指向同一个地址公用一块内存,同样可以构造特殊的数据完成利用。
下面我们首先明确一下自己的目标,通过漏洞拿到shell,完成这个目标我们需要什么条件呢
system地址 劫持控制流/bin/sh等字符串 堆结构
在讲我们怎么获取这些信息之前,我先来将一些堆结构基础知识,如果您已经掌握,请跳过此处。在linux 的内存管理中,主要是通过bins数组和链表来管理各个堆块的,首先他们分为fast bin ,small bin ,large bin, unsorted bin,这里我主要讲一下fast bin和small bin,因为篇幅有限,这里没一个部分都可以拿出来单独将一篇,其中的细节也很多,如果自己有能力或者有兴趣,强烈建议大家去阅读glibc的源代码,这会对你堆利用或者发现新的利用姿势有很大帮助,下面先介绍一下堆块的结构(x86平台)
其中fd和bk只有释放的堆块才有效,同时NMP是三个标志位,P代表这前一个堆块是否被释放,pre_size也是前一个堆块的大小(这里的前一块只得是连续的前一块),堆块头的大小也就是8字节。
Fast bin
在glibc内存管理中,fastbinsY 这是一个链表数组, 这数组的大小是10,数组的每一项都是一个单项链表(只使用其中的fd),每次堆块都是从尾部添加和摘除(后进先出),每一个链表里的堆块大小一样,相邻两个链表相差8字节,堆块大小从16到80,用户申请大小小于等于64字节,这都会分配到fastbin,fastbin的堆块不会发生堆块合并,它的P位一直是1。
Small bin
smallbin 属于bins中的一部分,一共有62个,和fastbin一样是个链表数组,数组中的每一条链表都是双向链表,堆块的大小小于512字节,连续free的堆块会发生堆块合并。
本题堆分析
根据上面的介绍,我们对堆的结构有个大概的了解,下面我们对这个题目的堆进行一个简单的分析和调试,根据上面静态分析,我们了解到程序首先会申请一个8字节大小的堆块来存储函数指针和字符串指针,根据上面的知识我们知道这个堆块是属于fastbin的,同时程序还会分配一个我们自定义大小的堆块。首先我们使用pwntools写一个脚本,我们要add一个大小为0×50的note,(pwntools是一个很好的工具,可以帮助我们快速写出exp,堆上的操作很多都是重复的建议写成函数,我们的gdb也可以装peda,pwndbg,gef等插件来帮助我们来调试,我这里装了pwndbg),运行脚本,这里加了gdb.attach(p),我们可以很方便的程序,我们附加程序后,在malloc函数的下一个指令下断点,这时eax寄存器里的值就是返回的分配堆地址,具体如下图。
程序断在0x0804869f,这是我们下的断点上,我们使用x/100wx $eax-8 可以查看程序当前分配后的堆情况包括堆块头的信息(8是堆块头的大小,系统分配给程序的地址是从堆块头之后的),具体如图。
然后我在0×08048731,下断点,然后继续调试,依旧使用x/100wx $eax-8 查看堆信息,这里除了大小不一样和上面的差不多,我就不具体分析,我们记录一下当前的堆的起始地址0x898f000,然后再add函数返回的地方0x080487D3下断点,然后使用x/100wx 0x898f000 查看堆的状态,具体如下图。
下面我们修改脚本申请和释放几个堆块分析一个堆块释放过程,熟悉了堆的结构我们可以使用pwndbg插件的一些特殊调试命令加快我们的调试速度,比如pwndbg给我们提供heap命令可以方便的查看堆块的分配情况,bins命令可以快速查看bins的状态,当然还有一些其他方便的功能。
脚本主要代码 add(0x50,"A"*10) add(0x50,"B"*10) add(0x50,"C"*10) gdb.attach(p) delete(0) delete(1) gdb.attach(p)
运行脚本,使用heap命令可以看到当前申请的堆块,具体如下图所示。
然后继续运行,程序会断在释放前两个note(note之程序中的结构)之后,这时候我们可以使用bins命令查看一个堆块的释放情况,具体如下图。
结合着两个图,我们可看到,首先我们释放的是note0(0x9fd1000),然后释放的是note1(0x9fd1068),我们可以观察到fastbins的情况,这两个都是申请8字节大小,整个堆块的大小是16=0×10,所以这两块会连在一起,根据释放的顺序,是从链表的尾部插入(这里的头和尾是相反的)的,而其他两个堆块是先放到unsortbin里暂存,以提高分配速度。这里思考一些如果我们再 add(0×8,”X”),这时程序会分配到哪里的堆块。
0×03漏洞利用 system地址获取
通过上面的分析和调试,我相信大家对堆也有了一定的了解,下面我们回到之前的几个问题,首先是泄露system地址,这个题目给了libc文件,所以我们知道leak出libc地址或者是leak一个一直函数的真实地址(比如free,malloc等等都可以),这里我讲两种方式类获取这个地址。
方法一:通过leak函数got表的地址获取libc基址
首先说这个方法,因为ELF的动态链接,在got.plt段会存储真正的函数地址(在这个函数被调用之后,程序的加载过程同样很复杂,这里我们可以去阅读《程序员的自我修养:链接、装载与库》,相信会对你有很大的帮助),还记得上面说到的问题吗?如果我们再 add(0×8,”X”)的时候,这里叫note2吧,具体我们可以先看下面这张图。
note2分配的地址就是(note1的头结构体地址)0x9fd1068,而他的字符串也是8大小,它分配的地址就是(note0的头结构体的地址)0x9fd1000,也就是我们可以通过输入来控制,note0头结构体的值,让他完成我的leak功能,最上面的函数分析,我们知道print函数的功能就是打印字符串的值,现在我们可以控制字符串的值了,相当于我们可以控制他打印的值了,那么我们可以这样如下操作。
add(0x8,p32(0x804862B)+p32(0x0804A018)) 0x0804A018这是free函数真实函数的地址,在IDA中got.plt段可以找到
根据堆的信息我们可以发现我们成功的修改了note0的字符串指针,把他修改成了之前free函数的真实地址的位置,我们在调用print(0),就可以将free的函数地址获取,然后通过下面的公式就可以获取到system函数的地址(两个函数的相对偏移是固定的)。
libc_base = free_addr - libc.symbols['free'] system_addr = libc_base + libc.symbols['system'] 方法二:使用main_arena获取libc基址
上面我们已经介绍了一种leak地址的方式,下面我们介绍另一种方式获取地址的方式,那就是通过main_arena来获取,在fastbin为空时,unsortbin的fd和bk指向自身main_arena,而main_arena存储在libc.so.6文件的.data段,通过这个偏移我们就可以获取libc的基址,这里我讲一下怎么找到main_arena的地址,首先使用IDA打开libc文件,然后搜索函数malloc_trim(),具体如下图所示。
为什么是这个呢,我们可以对照一下malloc.c的源代码,源代码如下图。
我们可以如下构造脚本
add(0x50,"A"*10) //申请一个不是fastbin的内存 add(0x50,"B"*10) //防止发生堆块合并 delete(0) add(0x50,"") //note0的头已经被破坏了它的(func位置)也就是fd位置会为0,所以我们要再申请同样大小的,才能正确的调用print gdb.attach(p)
根据上面的调试的结果,我们可以计算libc_base和system_addr,具体如下
libc_base = leak_addr - (main_arena+48) system_addr = libc_base + libc.symbols['system'] 劫持控制流和/bin/sh字符串
至此我们获取了system的地址了,我们看一下怎么劫持控制流,这个还是比较明显的,上面获取地址的时候,我们已经可以修改func指针了,我们可以把它写成我们获取的system地址。
((void (__cdecl *)(st *))ptr[v1]->func)(ptr[v1]);
我们再来仔细分析一下print这个函数,主要就是上面这一行代码,调用这个函数同时,将这个头结构体的地址当作参数传递给函数,但是我们这个结构体开头的是这个函数地址,system执行到这里会报错,找不这个指令,我们跳过这个4个字节的函数指针,我们加一个 “;” 就可以写下一条指令了,但是这里会有一个问题,那就是我们空间有限,除了前四个字节之外还有四个字节,在去除”;”就剩三个字节,想写入”/bin/sh”这是不可能的,这里有两个技巧可以解决这个问题,具体如下:
system("$0"); system("sh");
这两种方法都可以启动shell,最后exp构造如下:
add(0x8,p32(system_addr)+p32(";$0\x00")) 或 add(0x8,p32(system_addr)+p32(";sh\x00"))
0×04 总结
经过这道题目,我们对堆更加的了解,一道简单的题目也可以让我们学到很多,同时体会到调试的重要性,亲自动手去调试和只想不动手学到的知识和深度是不同的,只有亲自动手才能加深自己的印象和理解的也会更深,强烈建议大家跟着我的流程调试一下,只看这个文章,不动手是不够的。
来自freebuf