前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >pwnable.tw刷题之dubblesort

pwnable.tw刷题之dubblesort

作者头像
FB客服
发布2018-02-24 17:11:51
1.5K0
发布2018-02-24 17:11:51
举报
文章被收录于专栏:FreeBufFreeBuf

前言

上一篇中我介绍了phttp://www.freebuf.com/articles/others-articles/134271.htmlwnable.tw中第三题calc的解题思路,在这篇里,我将和大家分享第四题dubblesort的分析过程。

该题在算法上难度不大,能看得懂汇编就基本上可以分析清楚,重点是如何在ASLR、NX等多重保护开启的情况下,利用题目中出现的漏洞来进行漏洞利用,并获取系统shell。该题为我们提供了一个在多重保护下栈溢出的思路,而且还有几个小的技巧值得我们学习。作为一名新手,在这题上我也是绞尽脑汁,最后还是在别人的提示下完成题目,在此将学到的知识分享给大家。

1、题目解析

题目给出了程序dubblesort和libc库文件,说明可能可以通过return to libc的方式进行漏洞利用。从题目来看,貌似是一道和排序(sort)相关的题目。下载并执行程序dubblesort,如下图可以看到,首先需要输入用户名,之后输入想要排序的数字的个数,再依次输入要排序的数字,最后程序会计算并输出排序结果。

使用pwntools的checksec功能对程序的执行保护进行检查,发现包括NX在内的大部分保护都开启了,这对我们来说并不是一个好消息。

2、算法分析

使用IDA对dubblesort程序进行分析,发现程序流程并不复杂。

2.1) main函数分析

如下图所示,在main函数中,首先生成canary并将其入栈,然后调用了timer()函数来计时,超时则会结束程序。在这之后,程序调用read函数来获取用户输入的用户名,缓冲区大小为64字节,也就是64/4=16个栈单元。紧接着,程序调用scanf函数接收用户输入的要排序的数字个数。在以上准备工作完成后,程序就进入while循环,依次接收要排序的数字,并将其保存在nums数组中,该数组是函数创建的局部变量,从下图中可以看出,其起始位置位于栈上esp+0x1c的位置。

2.2) sort函数分析

在接收完用户的所有输入后,程序调用sort函数对用户输入的所有数字按照从小到大的顺序进行排序。

上图为sort函数的代码,算法很简单,是一个典型的冒泡排序,重复count次,每次排序将当前最大的数放在数组的最后,在循环结束后,所有数就从小到大排列了。由于排序并不是本题的重点,因此在这里就不详细介绍了,感兴趣的朋友可以查阅冒泡排序相关知识。在这里我们只用记得,排序后的数字序列仍然保存在原先栈上开辟的这段空间内,只不过数值的顺序变了。

3、漏洞分析

3.1) 栈溢出和canary绕过

从上面的分析可以看出来,在输入待排序数的时候,程序并没有限制要排序数的个数。但是,由于待排序数组位于栈空间内,而当前栈空间的大小是有限的,这就可以导致栈溢出。循环为数组赋值的汇编代码如下:

从上图我们可以看出,待排序数组的起始位置为esp+0x1c。但是不要忘了,我们的main函数开启了canary保护,canary的位置在esp+0x7c的位置(如下图),该位置在esp+0x1c和ebp之间,这让栈溢出无所适从。

这时我们要考虑,有没有什么方法在输入数据时不改变栈上原来数据的内容?我尝试着输入非法字符,结果如下:

这里出现了一个奇怪的现象,当我在第五个数的位置输入“f”这个非法字符时,之后的所有输入自动结束,并且从该位置之后的数据被泄露出来。这个原因我思索了好久,最后发现,这是因为scanf函数接收的数据格式为无符号整型(%u),而程序在检测到stdin中的字符是“f”时,将其视为非法输入,于是本次的scanf函数执行失败,原栈上对应位置的数据也没有被改变。

在下一次循环执行到scanf时,程序又到stdin中取数据,这时,上次输入的“f”由于非法并没有被取走,它还在stdin中存在着,因此scanf的输入又失败了……至此往后的每次循环,scanf都去取stdin中的这个“f”,然后每次都失败,于是从第五个位置往后的所有栈上数据都不会被修改,且在程序最后被泄露出来。

这里可能有朋友要问了,在循环中明明有fflush,为什么无法清空stdin?我在网上查了相关内容,发现对于一些编译器,fflush会失效,不知道这里是不是这个原因。如果有朋友清楚这里的疑问,请一定要帮我解惑!

题目到此,好像这条路走不通了。我曾经查阅资料,尝试使用其它方式绕过canary,但都证明了不可行。那有没有什么字符可以既让scanf认为它是合法字符,同时又不会修改栈上的数据呢?在多次尝试和不断查阅资料后,我发现“+”和“-”可以达到此目的!因为这两个符号可以定义正数和负数,所以会被识别为合法字符。比如输入“+4”会被识别为4,而“-4”则会将其转为正数输出(%u的原因)。测试如下图:

如上图所示,4294967293即为-3的无符号值,它们的十六进制是一样的(0xFFFFFFFD)。那么我们只输入一个“+”或者“-”就可以达到我们的目的。如下图:

从图中可以看到,在第4个数的位置我输入了“+”,它并未改变栈上数据,且不会影响之后的输入。

至此,我们可以解决canary绕过的问题了。canary距离待输入数据的起始位置为(esp+0x7c)-(esp+0x1c)+4=100字节,100/4=25个栈空间。也就是说,当我们要输入第25个数据时输入“+”或者“-”就可以保持canary不变,从而绕过函数最后的canary检查,实现栈上任意位置的写入。

3.2) libc地址的泄露

那么我们要往栈上写入什么数据呢?前文提到,题目给了libc库文件libc.so.6,这就暗示我们可以通过ret2libc的方式来进行栈溢出。该方法的利用方式是,修改栈上函数返回值地址,将其变为libc库中某函数的地址(如system函数),从而达到获取系统shell等目的。

我们可以修改栈上的main函数返回值为libc中的system函数地址,并在参数对应的位置写入“/bin/sh”字符串的地址,从而使程序跳转到system函数,并执行shell。所期望的溢出后的栈空间如下图:

从图中可以看出,我们要溢出的数据总共35个栈空间,其中第25个栈空间的canary通过输入“+”保持其值不变;第33个栈空间写入system函数的地址;第34个栈空间是system函数的返回地址,由于我们无需考虑system返回后的工作,次数据可任意填写;第35个栈空间需写入“/bin/sh”字符串的地址。现在的主要问题是,如何获取system函数和“/bin/sh”字符串的地址?

首先我们通过gdb调试发现,在ebp+4(main函数返回地址)的位置存放了一个libc中的函数地址__libc_start_main(main函数执行完后返回至该函数),可通过多次执行程序泄露该位置数据来判断libc地址是否随机,即目标系统是否开启ASLR。由上图可知,ebp+4的位置是从nums[0]开始的第33个栈空间,因此我们通过多次输入来泄露该位置的值,过程如下:

从上图可以看出,两次执行程序后,第33个位置的内容改变了,分别为4149671479=0xF756F637和4150175287=0xF75EA637,说明系统的libc基址改变了,开启了ASLR。

我们知道,在ASLR开启的情况下,堆栈地址和libc的地址都是随机的,那么我们如何获取libc中函数的地址呢?通过在输入数字时输入“+”来泄露栈上数据的方法开上去可行,但每次泄露后程序就结束了,下次再执行程序时libc的地址又改变了,无法通过这种泄露来获取当前进程空间的libc地址并进行利用。因此我们要通过其它的手段来在程序执行的过程中泄露libc地址。

经过研究,我发现在输入用户名后程序的返回有点奇怪:

上图可看到,我们输入的用户名是mike,但程序返回的输出并不正常,不仅在mike后换行,而且后面还跟着几个不可见字符,这是为什么?

再来到程序接收用户名输入和输出的过程:

我们发现,程序在用printf函数输出欢迎字符串“Hello….”的时候格式为%s,大家知道,printf在做格式化输出字符串时,是以0×00(null)为结尾来判断字符串结束,可是在我们输入用户名name的时候,程序是用read来接收的,它并不会自动为我们输入的字符串补0。当我们输入“mike”这4个字符并敲回车后,真正传给程序的是“mike\n”这样一个5字节的字符串。

程序在接收这个字符串后将这五个字符保存在栈上的esp+0x3c的位置,但这五个字符之后是否跟着0×00就不得而知了。根据上面的输出我们大致可以猜到,“Hello mike”之后的换行应该是我们输入的回车(“\n”)导致的,而下一行一开始的几个不可见字符,应该是栈上紧跟着换行符后面的数据。也就是说,我们通过输入,无意中泄露了栈上的数据!

这是一个好消息,因为我们可能可以在覆写栈上数据之前泄露出libc的地址。那么name之后的64字节地址空间中是否含有libc中的地址呢?

通过gdb调试,我们发现在name后的第7个栈单元保存着一个疑似libc中的地址0xf7fb1000:

那么此时的libc基址是多少呢?该地址又是否是libc上的地址呢?我首先通过 info sharedlibrary命令来获取libc的地址:

从图中可以看出,libc.so.6的地址空间为0xf7e16750到0xf7f4204d,好像并不包括我们上面可泄露的地址0xf7fb1000。我又用vmmap命令(info proc mappings命令亦可)查看libc在内存中的加载情况:

可以看出,libc的地址范围变为0xf7dff000到0xf7fb2000,和info sharedlibrary命令获取的libc地址不同(这里libc-2.23.so和libc.so.6是同一个文件,libc.so.6是libc-2.23.so链接)。这个问题同样困扰了我好久,通过查阅资料,我找到了两个命令的不同之处:info sharedlibrary显示的地址范围为libc-2.23.so文件的.text段在内存中的地址范围,而vmmap显示的为libc-2.23.so文件加载到内存中的全部地址空间。在这里可以这样验证:

首先通过hexdump命令验证了0xf7dff000确实为libc-2.23.so加载在内存中的起始地址(可清楚地看到ELF头部标志)。之后通过readelf -S命令查看libc-2.23.so文件的.text段偏移(0×17750),将其加上起始地址0xf7dff000即为0xf7e16750。验证成功。这个小实验和本题关系不大,但是能告诉大家如何在gdb调试时更加清楚地查看libc基址。

回到问题开始,0xf7fb1000这个地址确实在libc加载在内存中的地址范围内(0xf7dff000到0xf7fb2000),它的偏移是0xf7fb1000-0xf7dff000=0x1b2000,那么我们就可以泄露这个地址并减去它相对于libc基地址的偏移来动态获取libc的基址。可是事情并没有这么简单,在我写好exp后,怎么执行都无法获取shell,最后发现是这个偏移出了问题。因为我自己的libc库和目标系统的libc库不一样,偏移也就不同!那么真正的偏移是多少呢?

我们再用readelf命令来看看0x1b2000这个偏移在我的libc中的位置:

从上图可以看出,该偏移是.got.plt节相对于libc基址的偏移,那么我们再来看看题目中给的目标系统的libc文件的节情况:

可以看出,.got.plt节的偏移为0x1b0000,并不等于我们之前得到的0x1b2000。而system函数的偏移和“/bin/sh”字符串在libc中的偏移我们可以通过readelf -s命令和二进制编辑器HxD得到:

这样一来,我们就可以得到libc基址、system函数地址以及“/bin/sh”字符串的地址:

addr_libc=addr_leak-0x1b0000

addr_system=addr_libc+0x3a940

addr_shell=addr_libc+0x158e8b

4、漏洞分析

有了以上的分析,漏洞利用的实现就简单多了。

首先我们要泄露name后第6个栈单元上的数据(.got.plt节地址)。由分析可知,该单元距离name的初始地址为24字节,因此我们至少要发送24字节的冗余数据。经测试后发现,该栈单元的数据的第一个字节(即.got.plt节地址的最后一个字节,因为小端序)总为0×00,因此若要泄露该数据,需要多发送一个字节覆盖掉0×00,否则printf会将0×00之后的数据截断。可以发送’A'*24+’\n’来泄露出该数据的后三个字节,再加上’\x00′即可。

之后就可以根据泄露的地址推算出system函数和“/bin/sh”字符串在内存中的地址。需要注意的是,程序在执行过程中会将所有数据排序,因此我们需要在输入数据时注意数据的大小,这并不难,具体做法是将canary之前的数据都置0,canary和返回地址之间(包括返回地址)的数据都写入system函数的地址(canary随机数大部分时间都小于system地址,除非人品不好),而最后两个栈单元都写入“/bin/sh”字符串的地址即可。配置好的栈结构如下:

后话

经过一段时间的学习,我深刻地意识到自己根基不牢且知识量匮乏,目前接触的这些皮毛只是二进制世界里极小的一部分,仍有太多好玩的东西等待着我们去学习和挖掘。

由于pwnable.tw要求不允许在公开渠道公布高分题目的解题思路,虽然这只是一道200分的题,但是POC和flag我就不留啦,祝大家解题愉快!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-05-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FreeBuf 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 2.1) main函数分析
  • 2.2) sort函数分析
  • 3.1) 栈溢出和canary绕过
  • 3.2) libc地址的泄露
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档