2019-2020-2 20174313张博 《网络对抗技术》Exp1 PC平台逆向破解

写在前面

·实践目标

        本次实践的对象是一个名为pwn1的linux可执行文件。该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。该程序同时包含另一个代码片段——getShell,它会返回一个可用Shell,调用命令行供用户使用。正常情况下getShell是不会被运行的。我们实践的目标就是想办法运行这个本不会被运行的代码片段。

·实践内容   

        -手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。

        -利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。

        -注入并运行一段指定的shellcode。

·基础知识

        - 熟悉Linux基本操作。掌握用户切换命令,知道如何获取root权限;掌握文件下载命令;懂得绝对路径、相对路径的使用方法;能查找指定的文件的路径,并通过路径操作进入到相应的文件夹,运行一个指定的文件;掌握管道“|”的含义和用法;掌握输入、输出重定向的知识;遇到陌生的命令,要掌握通过help、man和info等命令获得相应命令的介绍和帮助的方法;会使用gdb、vi;掌握其他一些必备的命令,包括反汇编等。

        -理解缓冲区溢出(BufferOverflow,Bof)的原理。能看得懂汇编、机器指令、EIP、指令地址、ELF文件格式。

        -掌握机器指令与汇编语言的关系,认识并理解常用的汇编语言,包括MOV、SUB、NOP, JNE, JE, JMP, CMP等。汇编语言和机器码是一一对应的,我们在Intel硬件手册里面可以轻松找到这些对应关系。详见https://wenku.baidu.com/view/eeb46ed184254b35eefd34bf.html

        -掌握堆栈的含义、概念和用法,知道函数调用和堆栈的关系,看到一段反汇编代码,要能够理解堆栈在其中的运行变化过程。

 

 ·实验前准备

        -先在主机上下载实验所需的Linux可执行文件,地址是 https://gitee.com/wildlinux/NetSec/attach_files 。这是老师在码云上设立的存放我们班级课程资料的地方。如下图所示找到pwn1.zip,点击右侧蓝字链接下载,下载完成后将压缩包中的pwn1解压出来。

 

        -将pwn1从主机上拖入kali,实现主机与虚拟机之间文件拖拽功能的前提是虚拟机安装了VMware tools,这在安装kali的时候是必备的工具。在kali中为pwn1寻找一个合适的路径——专门用于完成实验一。

一、直接修改程序机器指令,改变程序执行流程

        Step1:打开kali,在命令行中进入文件所在目录,输入 objdump -d pwn1 | more 进行反汇编,得到如下图所示结果。

         objdump命令是Linux下的反汇编目标文件或者可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息;-d参数表示disassembly——反汇编;pwn1表示要进行反汇编操作的文件名;|表示管道服务,意指将反汇编所得到的内容通过管道输送到一个可执行文件,该文件通过more命令分页显示。

        我们通过输入/getShell可以找到我们需要关注的几个函数,因为它们是挨着的,所以找到getShell即可找到全部三个函数,如下图所示。

        可以看出,在main函数中,地址为80484b5处存放的是一条call指令,执行一条call指令相当于执行了两条指令:PUSH和JMP。将EIP的值压入栈同时,CPU会跳转至指令中的偏移值加上EIP中的值的和所指向地址执行那里的指令。由上图可知,main函数调用foo,对应机器指令为“ e8 d7ffffff”,通过查询Intel硬件手册可以知道,call指令对应的机器指令是e8,那么d7ffffff就是调用foo的关键所在。我们想让main由调用foo改为调用getShell,就要把d7ffffff改为一个指定的值,这个值要能使main调用getShell。确定这个值之前,我们要搞清楚当前d7ffffff这个值为什么可以使main调用foo。

        d7ffffff是补码,表示十进制下的-41,41=0x29。执行call指令时EIP寄存器中存放的内容是下一条指令的地址,即80484ba。80484ba +d7ffffff= 80484ba-0x29=8048491,这正是foo函数的首地址。由此可以发现,80484ba加上一个值,所得的就是CPU会跳转到的地址,我们让这个地址等于getShell的首地址即可。那么要加上的那个数就呼之欲出了,用getShell的首地址减去执行call指令时EIP的值也就是用804847d减去80484ba即可得出这个数的大小,别忘了这个数是call指令的偏移值。这里要注意一点是,结果需要用补码表示,如果结果为正数那就不用变了,如果结果为负数那么要将其除符号位以外各位取反然后末位加一。我们用系统自带的计算器计算,可得如下结果:

 

        如上图所示,HEX是16进制表示,我们在这种环境下计算804847d减去80484ba,所得结果如上。在32位环境下,结果是ffffffc3,我们要把ffffffd7改为ffffffc3,即可实现main函数不再调用foo转而调用getShell。改之前我们要明确一点就是,正常来说一个数是高位在前(左)低位在后(右),然而我们在反汇编内容里则是低位在前高位在后,即小端优先。以字节为单位划分,一个16进制数是4bit,一字节就是两个16进制数,所以我们写起来是两个16进制数在一起算作一个字节,那么就要把call指令里面的偏移值改为c3 ff ff ff。接下来我们研究如何实现上面的更改操作。

        上图所示的步骤,选自码云上的讲义,我们按这个步骤做下去,即可完成修改操作。

        Step2:输入“ctrl+c”退出当前的反汇编分页显示界面。用cp命令备份一下pwn1文件,然后使用vi命令编辑备份文件。

        Step3:由于vi编辑器编辑的对象是文本文件,所以对于一个可执行文件进入编辑模式的话,会出现大片乱码。所以我们要输入 :%!xxd ,以使文件内容以16进制形式显示。效果如下:

        Step3:我们要修改的对象是d7 ff ff ff,要将其改成c3 ff ff ff,简单来说就是把d7改成c3。在vi编辑模式下我们可以轻松更改,关键是要找到这个d7在哪里。我们输入 /e8 d7 既可以定位到这里,如果找到多个查询结果,那么就对比着前面的反汇编结果,看看前后一定长度的字节内容是否匹配,一定要确保改的是正确的地方。

        Step4:把光标移动到d,按一下 r 键,再输入 c ,然后按一下方向键 → ,再按一下 r 键然后输入 3 。完成修改。

        Step5:修改完成后不能立即退出,有一套“退出机制”。首先是把当前的16进制形式转换为原形式::%!xxd -r;然后是存盘退出  :wq  。如此便完成修改了,main函数可以成功调用到getShell函数。

         Step6:验证。我们可以用两种方法来验证是否修改成功了,第一种是反汇编,针对刚刚修改的文件进行反汇编,通过反汇编结果验证。截图如下:

         由上图可知,main函数通过call指令调用了首地址位于80847d的getShell函数,证明刚刚的修改是成功有效的。

         另一种方法就是直接运行了,刚刚修改的文件名叫pwn2,那么就在文件所在的文件夹下输入./pwn2来执行这个文件。./表示当前目录。运行结果如下图所示:

        由上图可知,运行过pwn2以后,即可执行命令行语句了,比如执行ls,就可以显示出当前路径下的所有文件:pwn1(原始文件)和pwn2(攻击过后的文件)。输入exit命令,就退出了shellcode。由此我们看到了攻击效果,程序的功能被改变了。程序本来的功能是什么呢?我们运行一下pwn1就知道了,如上图所示,pwn1运行过后,功能是回显用户输入的字符串,这正是foo函数的效果,也正是程序被攻击前的执行流程。

 二、通过构造输入参数,造成BOF攻击,改变程序执行流

         Step1:在这里,我们首先要发现程序的漏洞,然后针对漏洞实施攻击。这就要求我们对程序的功能有足够的了解。经过研究可以发现,本程序3个函数中foo函数存在BOF漏洞,可以实施缓冲区溢出攻击。

         我们知道foo函数的功能是回显用户输入的字符串,由上图的反汇编结果可知,函数内两条call指令中前面那个读入字符串,后面那个输出输入的字符串。输入字符串按地址递增的顺序自然增长覆盖,系统预留的缓冲区大小只有0x1c,即28字节,地址自然增长填满28字节的区域是正常的,一旦输入字符串过大,超过了28字节,那么接下来4个字节会覆盖到EBP寄存器上,再来4个字节会覆盖到EIP寄存器上,这4个字节,也就是33、34、35和36个字节是我们关注的重点。因为EIP寄存器存放的是下一条要执行的指令的地址,我们若是能控制这个寄存器的值,那么就可以自由控制程序的走向了。正常来说,foo函数会回显输入的字符串,这其中有输入和输出两步,一旦我们通过缓冲区溢出攻击输入了一个长度较大(不少于36字节)的字符串,那么EIP寄存器的值就有可能被修改,程序的走向就被带偏了。我们可以输入一长段字符串,指定第33、34、35、36个字节的内容就是getShell函数的首地址,那么这段字符串一覆盖到堆栈上,程序的走向就被更改了,foo函数不会回显刚刚输入的字符串,而是会跳转到getShell函数的首地址执行该函数。

        另外,除了分析反汇编结果外,我们还可以通过gdb调试工具确定字符串中的哪些字节可以进入到EIP寄存器中。首先我们是为我们的kali下载安装gdb工具,如果没有的话。

        如上图所示,我们通过apt-get install gdb命令即可完成gdb的安装。如果提示权限不足,就在命令前面加上sudo。安装的全过程包括成功的界面截图如上,有上面那些步骤应该就是是安装成功了。

        上图有两个红框,第一个红框以上的部分是gdb调试pwn1的过程,到第一个红框开始才能输入命令。这里我们输入r,表示运行。下面第二个红框里有两行一模一样的内容,这正是pwn1本来的面目,它会调用foo函数回显输入的字符串。我输入了一条测试字符串,由八个1、八个2、八个3、八个4和1、2、3...9、0组成,长度为42字节。可以正常回显,所以就有了第二行的输出,但下面紧接着就给出了segmentation fault,即段错误,因为是发生了缓冲区溢出。那么此时各个寄存器状态如何呢?我们通过info r来查询一下,结果如下图所示:

        由上图可以看出,EIP寄存器对应的内容是0x34333231,这正是16进制下的1、2、3和4。这更加说明,我们上面输入的42字节的数据中,1234被写入了EIP寄存器,即第33、34、35、36个字节是会被写入EIP寄存器的。

        前面通过反汇编可以知道,getShell函数的首地址是0804847d,根据小端优先的规则,再加上216进制数算作1个字节,我们要输入的地址是\x7d\x84\x04\x08。那么攻击字符串就可以构造为11111111222222223333333344444444\x7d\x84\x04\x08\x0a\x0a表示回车。

        Step2:在上述思路的指引下,接下来我们需要确定用什么样的方法注入攻击字符串。我们无法直接通过键盘输入有效的16进制字符,可以通过生成包含这样一个字符串的文件然后经由管道注入到目标文件来实现注入攻击字符串的目的,这需要用到Perl语言。perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input-20174313。这样,我们就通过Perl语言把攻击字符串写入到了input-20174313文件中,它在当前文件夹中生成。生成代码及效果截图如下:

        由上图可知,我们在使用Perl语言生成了存有攻击字符串的目标文件后,使用ls指令显示当前路径下的全部文件,果然显示出了刚刚生成的文件input-20174313。然后用xxd命令验证一下文件里面的内容是否是刚刚写入的攻击字符串。由上图可知文件内容是正确的,地址为00000020的存储单元存储的正是getShell的首地址。

         Step3:我们将input的输入,通过管道符“|”,作为pwn1的输入。输入命令为(cat input-20174313; cat) | ./pwn1。效果如下图所示:

 

        从上图可以看出,经过管道注入后,再运行pwn1文件时,它的功能已经不再是回显输入的字符串了,而是调用了getShell函数,可以输入命令行指令。我先后测试了lspwd,结果都是符合预期的,前面那个是显示出了当前文件夹里面的全部文件,包括input-20174313、pwn1和pwn2;后面那个显示出了当前所处的路径。都是没问题的。

 三、注入Shellcode并执行

        Step1:准备一段shellcode。在本实验中,我们使用的shellcode是指定好的,这就省了很大力气:\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\

       Step2:做好准备工作。为了让本操作没那么复杂,本操作是有一些制条件的,本步骤就是要落实这些限制条件。

 

        上图截自码云上的实验讲义。但在实际操作中,由于种种原因还是不会和上图完全一致,这与用户权限有关。

        还未安装execstack的话,先通过apt-get install execstack命令安装,为避免权限不够,我还在命令前面加上了sudo。安装过程如下图所示:

        有如上过程的话,十有八九就是成功安装了。

        接下来通过execstack -s pwn4313设置堆栈可执行。通过execstack -q pwn4313查询文件的堆栈是否可用。如下图所示,出现X的话就表示可用了。

        下面要关闭地址随机化。我们通过 more /proc/sys/kernel/randomize_va_space来查询地址随机化的状态。我们用more来查看的这个文件有3个值,分别是0、1、2,每个值都有不同的含义。当且仅当它的值为2时才表明关闭了地址随机化。下图所示的3段有效指令中,第一段是查询randomize_va_space的值,发现不是0,然后就通过第二段指令将0覆盖进去,最后通过第三段指令重新查看一下该文件的值,验证一下第二段指令的修改是否有效。结果是0,这是正确的、符合预期的!

        如下图所示,当我输入第二段指令时,可以看,出连续两次被提示权限不够,即使我已经加了sudo。后来查阅资料才知道,还要加上sh -c才可以。利用 "sh -c" 命令,它可以让 bash 将一个字串作为完整的命令来执行,这样就可以将 sudo 的影响范围扩展到整条命令。详见 https://blog.csdn.net/tangtang_yue/article/details/78030658

        关于randomize_va_space文件的解释,参见 https://www.cnblogs.com/scrat/p/3505930.html

        Step3:使用输出重定向将perl生成的字符串存储到文件input_shellcode-4313中,效果如下图所示:

        上面这一大段字符串也是事先规定好的,copy过来直接用就可以。

        Step4:下面我们通过(cat input_shellcode-4313;cat) | ./pwn4313注入攻击缓冲区buffer。效果如下图所示:

        Step5:使用gdb调试进程。

        保持当前的命令行终端不变,另外打开一个命令行,通过ps -ef | grep pwn4313查询pwn4313的进程号,结果如下图所示:

        我们开启了两个终端,要查询的是第一个终端也就是pts/0里面的pwn4313的进程号,可以看出这是4734。接下来我们要使用gdb工具来调试该进程。

         如上图所示,我们首先输入gdb来开启调试工具。然后等到图中一个红框所示的位置以后可以输入内容,我们就输入attach加上刚刚查询到的进程号,在此我输入了attach 4734。

        然后输入disassemble foo,针对foo函数进行反汇编,得到如下结果:

        从上图可知,ret的地址为0x080484ae,所以我们就在这个地址设置断点,指令为break *0x080484ae。效果如下图所示:

        此时系统会提示自己说,Breakpoint 1 at 0x80484ae,这表明断点设置完毕。

        现在,我们在第一个命令行终端按下一次回车键,然后回到第二个终端按下c键继续运行。

 

        经历过上面的操作后,我们在第二个终端输入info r esp查看栈顶指针所在的位置及其存放的数据。

        由上图可知,esp寄存器放的是存0xffffd30c,紧接着我们用x/16x 0xffffd30c查看该地址的及其临近地址所存放内容。很幸运我们找到了0x01020304,这也正是返回地址的位置。shellcode就挨着,加上4就是了,所以shellcode的地址是 0xffffd310

        Step6:修改shellcode的值,完成攻击。

        如上图所示,我们可以先退出gdb调试工具,然后向刚才生成的input_shellcode-4313文件中覆盖修改后的Perl代码。注意地址0xffffd310在写入文件时要遵循小端优先的原则,这已经多次强调过,要按\x10\xd3\xff\xff......的形式写入,每一个16进制数表示一个字节。

        如果不放心,我们还可以用xxd命令来验证一下刚刚的文件input_shellcode-4313有没有被正确写入,如下图所示:

        毫无疑问,这准确无误的。

        Step7:最后,我们可以来收获成功的果实了!如下图所示,通过管道将input_shellcode-4313的输入作为pwn4313的输入。这样以后就出现了我们期待已久的、针对getShell函数的调用了!如下图所示:

        在上图中,输完管道命令后下面第一行的ls是无效的,因为此时程序尚未执行完毕。当出现过第二行的"AAAA..."这一大堆字符后,就可以成功调用命令行了。在此我先后输入了ls、pwd、exit这三个有效命令进行测试,结果都是符合预期的。

四、实验感想与收获

        本次实验核心思想是缓冲区溢出攻击,围绕这个核心开展了三个操作,它们代表了三种不同的攻击思路。具体操作起来并不复杂,因为在码云上有十分详尽的实验讲义,并且针对讲义内容,老师录制了四部教学视频,带领同学们仔仔细细地把实验内是容过了一遍。这不仅仅是在教同学们如何去操作,更是在教同学们如何去思考,以问题为导向步步为营走下去直至攻击目标最终实现。操作很简单,但重中之重是思考的过程。这需要我们对函数调用与堆栈的关系足够了解,拿到一段反汇编内容要能够清晰地认识到每一条指令是干什么的,彼此之间有什么关联。很关键的几条指令一定不能模棱两可而要彻底明白,比如call指令,还有push、pop、mov、sub、leave、ret、jmp等。对于堆栈结构更是得十分清楚,ebp、esp、eip寄存器分别承担着何种职责。

        总之,本实验并不难在操作,而是难在理解上。即使能把实验完整做下来,也未必能够深入理解实验的内涵、思想。我也知道自己虽然完成了实验,做出了结果,但是对整个实验流程还没有一个很深的把握,这有待我在今后的学习中继续加强!

        最后,对于我们通常所说的漏洞的话,我认为它是编程人员在编程过程中因为没有十分详尽备至科学合理的考虑而产生的会对整个代码体系产生威胁的安全隐患,漏洞可以非常可怕,威胁到国家安全和利益;也可以微不足道,造不成什么实质损害。但出现了漏洞也不能过分责怪编程人员。因为他们编写的内容体系往往都过于庞大,一个团队即使再怎么分工,一个人可能也得关注到海量的代码。人脑不是万能的,能够考虑到的东西不会覆盖到所有应该考虑到的东西,出现思维上的“漏网之鱼”是不可避免的。有漏洞不可怕,亡羊补牢为时不晚。记得以前电脑上的360隔三岔五地就会提示安装漏洞补丁,这就是一个动态弥补发展的过程!人不可能一下子就从难以计数的代码中找出漏洞所在,有时即便找出来了也可能无法找到合理的应对之策。不过但凡有一丝可能,技术人员都是可以想方设法弥补漏洞的。毕竟没有网络安全就没有国家安全!针对本实验的缓冲区溢出攻击,也是有很多规避方法的,避免缓冲区溢出漏洞即可。包括GCC堆栈保护技术、设置堆栈不可执行、启用地址随机化、加强代码质量检查等。

posted @ 2020-03-13 00:56  20174313张博  阅读(157)  评论(1编辑  收藏