最近在学习 Python,为以后工作做好准备;又恰好个人对黑客技术很好奇,就想在学习 Python 同时也学习下黑客技术,于是就有了这篇文章。由于是第一次学习黑客技术,好多好多都不懂,但也到处是金矿,等待我去挖取。
缓冲区溢出可分为栈溢出和堆溢出。平常我们所讲的以及本文所提到的缓冲区溢出特指的是栈溢出
。
注:本文缓冲区溢出攻击介绍部分摘录自博文缓冲区溢出攻击。
缓冲区溢出攻击介绍
关于缓冲区溢出攻击,博文缓冲区溢出攻击有一个很详细明了的阐述;另外也考虑到自己不一定能比该博文作者写的更好,所以关于缓冲区溢出介绍及攻击部分直接摘录自该博文,但会尽量保持原文排版风格。
缓冲区溢出(Buffer Overflow)是计算机安全领域内既经典而又古老的话题。随着计算机系统安全性的加强,传统的缓冲区溢出攻击方式可能变得不再奏效,相应的介绍缓冲区溢出原理的资料也变得“大众化”起来。其中看雪的《 0day 安全:软件漏洞分析技术》一书将缓冲区溢出攻击的原理阐述得简洁明了。本文参考该书对缓冲区溢出原理的讲解,并结合实际的代码实例进行验证。不过即便如此,完成一个简单的溢出代码也需要解决很多书中无法涉及的问题,尤其是面对较新的具有安全特性的编译器——比如 MS 的 Visual Studio2010。接下来,我们结合具体代码,按照对缓冲区溢出原理的循序渐进地理解方式去挖掘缓冲区溢出背后的底层机制。
一、代码 <=> 数据
顾名思义,缓冲区溢出的含义是为缓冲区提供了多于其存储容量的数据,就像往杯子里倒入了过量的水一样。通常情况下,缓冲区溢出的数据只会破坏程序数据,造成意外终止。但是如果有人精心构造溢出数据的内容,那么就有可能获得系统的控制权!如果说用户(也可能是黑客)提供了水——缓冲区溢出攻击的数据,那么系统提供了溢出的容器——缓冲区。
缓冲区在系统中的表现形式是多样的,高级语言定义的变量、数组、结构体等在运行时可以说都是保存在缓冲区内的,因此所谓缓冲区可以更抽象地理解为一段可读写的内存区域,缓冲区攻击的最终目的就是希望系统能执行这块可读写内存中已经被蓄意设定好的恶意代码。按照冯·诺依曼存储程序原理,程序代码是作为二进制数据存储在内存的,同样程序的数据也在内存中,因此直接从内存的二进制形式上是无法区分哪些是数据哪些是代码的,这也为缓冲区溢出攻击提供了可能。
图 1:进程地址空间分布
图 1 是进程地址空间分布的简单表示。代码存储了用户程序的所有可执行代码,在程序正常执行的情况下,程序计数器(PC 指针)只会在代码段和操作系统地址空间(内核态)内寻址。数据段内存储了用户程序的全局变量,文字池等。栈空间存储了用户程序的函数栈帧(包括参数、局部数据等),实现函数调用机制,它的数据增长方向是低地址方向。堆空间存储了程序运行时动态申请的内存数据等,数据增长方向是高地址方向。除了代码段和受操作系统保护的数据区域,其他的内存区域都可能作为缓冲区,因此缓冲区溢出的位置可能在数据段,也可能在堆、栈段。如果程序的代码有软件漏洞,恶意程序会“教唆”程序计数器从上述缓冲区内取指,执行恶意程序提供的数据代码!本文分析并实现栈溢出攻击方式。
二、函数栈帧
栈的主要功能是实现函数的调用。因此在介绍栈溢出原理之前,需要弄清函数调用时栈空间发生了怎样的变化。每次函数调用时,系统会把函数的返回地址(函数调用指令后紧跟指令的地址),一些关键的寄存器值保存在栈内,函数的实际参数和局部变量(包括数据、结构体、对象等)也会保存在栈内。这些数据统称为函数调用的栈帧,而且是每次函数调用都会有个独立的栈帧,这也为递归函数的实现提供了可能。
图 2:函数栈帧
如图所示,我们定义了一个简单的函数 function,它接受一个整形参数,做一次乘法操作并返回。当调用 function(0) 时,arg 参数记录了值 0 入栈,并将 call function 指令下一条指令的地址 0x00bd16f0 保存到栈内,然后跳转到 function 函数内部执行。每个函数定义都会有函数头和函数尾代码,如图绿框表示。因为函数内需要用 ebp 保存函数栈帧基址,因此先保存 ebp 原来的值到栈内,然后将栈指针 esp 内容保存到 ebp。函数返回前需要做相反的操作——将 esp 指针恢复,并弹出 ebp。这样,函数内正常情况下无论怎样使用栈,都不会使栈失去平衡。
sub esp,44h 指令为局部变量开辟了栈空间,比如 ret 变量的位置。理论上,function 只需要再开辟 4 字节空间保存 ret 即可,但是编译器开辟了更多的空间(这个问题很诡异,你觉得呢?)。函数调用结束返回后,函数栈帧恢复到保存参数 0 时的状态,为了保持栈帧平衡,需要恢复 esp 的内容,使用 add esp,4 将压入的参数弹出。
之所以会有缓冲区溢出的可能,主要是因为栈空间内保存了函数的返回地址。该地址保存了函数调用结束后后续执行的指令的位置,对于计算机安全来说,该信息是很敏感的。如果有人恶意修改了这个返回地址,并使该返回地址指向了一个新的代码位置,程序便能从其它位置继续执行。
三、栈溢出基本原理
上边给出的代码是无法进行溢出操作的,因为用户没有“插足”的机会。但是实际上很多程序都会接受用户的外界输入,尤其是当函数内的一个数组缓冲区接受用户输入的时候,一旦程序代码未对输入的长度进行合法性检查的话,缓冲区溢出便有可能触发!比如下边的一个简单的函数。
1 | void fun(unsigned char *data) |
这个函数没有做什么有“意义”的事情(这里主要是为了简化问题),但是它是一个典型的栈溢出代码。在使用不安全的 strcpy 库函数时,系统会盲目地将 data 的全部数据拷贝到 buffer 指向的内存区域。buffer 的长度是有限的,一旦 data 的数据长度超过 BUF_LEN,便会产生缓冲区溢出。
图 3:缓冲区溢出
由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。当把 data 的数据拷贝到 buffer 内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:
1、淹没了其他的局部变量。如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。这种方式可以用于破解简单的软件验证。
2、淹没了 ebp 的值。修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。
3、淹没了返回地址。这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!
4、淹没参数变量。修改函数的参数变量也可能改变当前函数的执行结果和流程。
5、淹没上级函数的栈帧,情况与上述 4 点类似,只不过影响的是上级函数的执行。当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!)。
如果在 data 本身的数据内就保存了一系列的指令的二进制代码,一旦栈溢出修改了函数的返回地址,并将该地址指向这段二进制代码的其实位置,那么就完成了基本的溢出攻击行为。
图 4:基本栈溢出攻击
通过计算返回地址内存区域相对于 buffer 的偏移,并在对应位置构造新的地址指向 buffer 内部二进制代码的其实位置,便能执行用户的自定义代码!这段既是代码又是数据的二进制数据被称为 shellcode,因为攻击者希望通过这段代码打开系统的 shell,以执行任意的操作系统命令——比如下载病毒,安装木马,开放端口,格式化磁盘等恶意操作。
四、栈溢出攻击
上述过程虽然理论上能完成栈溢出攻击行为,但是实际上很难实现。操作系统每次加载可执行文件到进程空间的位置都是无法预测的,因此栈的位置实际是不固定的,通过硬编码覆盖新返回地址的方式并不可靠。为了能准确定位 shellcode 的地址,需要借助一些额外的操作,其中最经典的是借助跳板的栈溢出方式。
根据前边所述,函数执行后,栈指针 esp 会恢复到压入参数时的状态,在图 4 中即 data 参数的地址。如果我们在函数的返回地址填入一个地址,该地址指向的内存保存了一条特殊的指令 jmp esp
——跳板。那么函数返回后,会执行该指令并跳转到 esp 所在的位置——即 data 的位置。我们可以将缓冲区再多溢出一部分,淹没 data 这样的函数参数,并在这里放上我们想要执行的代码!这样,不管程序被加载到哪个位置,最终都会回来执行栈内的代码。
图 5:借助跳板的栈溢出攻击
借助于跳板的确可以很好的解决栈帧移位(栈加载地址不固定)的问题,但是跳板指令从哪找呢?“幸运”的是,在 Windows 操作系统加载的大量 dll 中,包含了许多这样的指令
,比如 kernel32.dll,ntdll.dll,这两个动态链接库是 Windows 程序默认加载的。如果是图形化界面的 Windows 程序还会加载 user32.dll,它也包含了大量的跳板指令!而且更“神奇”的是 Windows操作系统加载 dll 时候一般都是固定地址,因此这些 dll 内的跳板指令的地址一般都是固定的
。我们可以离线搜索出跳板执行在 dll 内的偏移,并加上 dll 的加载地址,便得到一个适用的跳板指令地址!
1 | // 查询dll内第一个jmp esp指令的位置 |
这里简化了搜索算法,输出第一个跳板指令的地址,读者可以选取其他更合适位置。LoadLibraryA 库函数返回值就是 dll 的加载地址,然后加上搜索到的跳板指令偏移 pos 便是最终地址。jmp esp 指令的二进制表示为 0xffe4,因此搜索算法就是搜索 dll 内这样的字节数据即可。
虽然如此,上述的攻击方式还不够好。因为在 esp 后继续追加 shellcode 代码会将上级函数的栈帧淹没,这样做并没有什么好处,甚至可能会带来运行时问题。既然被溢出的函数栈帧内提供了缓冲区,我们还是把核心的 shellcode 放在缓冲区内,而在 esp 之后放上跳转指令转移到原本的缓冲区位置。由于这样做使代码的位置在 esp 指针之前,如果 shellcode 中使用了 push 指令便会让 esp 指令与 shellcode 代码越来越近,甚至淹没自身的代码。这显然不是我们想要的结果,因此我们可以强制抬高 esp 指针,使它在 shellcode 之前(低地址位置),这样就能在 shellcode 内正常使用 push 指令了。
图 6:调整 shellcode 与栈指针
调整代码的内容很简单:
1 | add esp,-X |
第一条指令抬高了栈指针到 shellcode 之前。X 代表 shellcode 起始地址与 esp 的偏移。如果 shellcode 从缓冲区起始位置开始,那么就是 buffer 的地址偏移。这里不使用 sub esp,X 指令主要是避免 X 的高位字节为 0 的问题,很多情况下缓冲区溢出是针对字符串缓冲区的,如果出现字节 0 会导致缓冲区截断,从而导致溢出失败。
第二条指令就是跳转到 shellcode 的起始位置继续执行。(又是 jmp esp!)
通过上述方式便能获得一个较为稳定的栈溢出攻击。
缓冲区攻击实战
环境说明
这次选择的攻击环境简要列写如下:
- 攻击对象:FreeFloatFTPServer
- 操作系统:Windows XP SP2
- 工具:Immunity Debugger(Github)、Metasploit Framwork(Github)、mona、Infigo FTPStress
检测 FreeFloatFTPServer 是否存在漏洞
法一:利用 Infigo FTPStress 软件
Infigo FTPStress
Infigo FTPStress 软件可用于检测 FTP 服务器是否存在漏洞。该软件的主界面如下:
接下来点击 Config 对该软件进行简单配置:
攻击数据
攻击数据长度:默认即可
攻击选项
攻击 USER 命令
这次选择攻击的是 USER 命令。结果如下图所示:
红色警示,说明 FreeFloatFTPServer 的 USER 命令存在缓冲溢出漏洞。
攻击 MKD 命令
红色警示,说明 FreeFloatFTPServer 的 MKD 命令存在缓冲溢出漏洞。
下文的攻击针对该软件的 MKD 命令。
法二:代码检测
Python 代码样例如下:
1 | 'Detect buffer overflow script' |
检测结果如下:
确定函数返回地址的存放位置
从之前的介绍中,我们知道,要成功实现缓冲溢出攻击,一个很重要的环节就是确定函数返回地址存放位置,以便我们在该位置存放指向 ShellCode 的地址值。
确定攻击载体大概长度
从之前的实验来看,攻击载体 exploit 的长度不到 1000 个字符就可以让 FreeFloatFTPServer 停止工作。在这里把 1000 个字符长作为 exploit 的长度。
首先,我们在 Immunity Debugger 中打开 FreeFloatFTPServer EXE 程序,并运行该程序,如下图所示:
接下来,执行攻击程序:
1 | 'Exploit script' |
最后,会看到如下结果:
确定函数返回地址的存放位置
对于确定函数返回地址的存放位置,我们有一个很好的工具 mona 可以使用。mona 是 Immunity Debugger 的一个插件,需要我们自己添加到 Immunity Debugger 的 PyCommands 目录下。
第一,生成特征字符串:
注:也可以用 Metasploit framwork 中的 pattern_create.rb 命令生成,结果一样。对于该框架的环境搭建花费了太多时间,而且最后是通过安装集成了该框架的 Kali Linux 才解决的。建议直接安装 Kali Linux 以便于使用。
第二,我们将生成的特征字符串(存放在 …/pattern.txt 文件)替换原 exploit 的内容:
1 | exploit = '特征字符串' |
第三,在 Immunity Debugger 中重启 FreeFloatFTPServer,并开启攻击脚本,并得到如下结果:
第四,执行上图中的 !mona pattern_offset addr 命令,其中 addr 的值是此时 EIP 的值 0x33694132,执行结果说明了函数返回地址所在位置偏移为 248:
最后,为了确保求出来的偏移位置正确,我们将 exploit 的值修改为:
1 | exploit = 'A' * 248 + 'B' * 4 + 'C' * 748 |
并进行新一轮的攻击,得到如下结果:
攻击
现在的工作就是把真正的攻击代码 ShellCode 放到 exploit 中,不过在此之前我们还要做一些工作。
查找 jmp esp 指令
因为我们无法在源程序中加入代码,所以我们需要借助于系统文件中所包含的指令。启动 FreeFloatFTPServer,查找 push esb 指令:
从其中我们选出地址为 0x7c941eed 的在 ntdll.dll 中的 push esp 指令,并重新构造 exploit:
1 | exploit = 'A' * 248 + '\xed\1e\94\x7c' * 4 + 'C' * 748 # 注意 i386 CPU 是小端存储数据的 |
对于验证 ‘\xed\1e\94\x7c’ 是否被正确存放到函数返回地址处,请参考前文。
另外,我们可以在 FreeFloatFTPServer 启动后设定断点,看程序是否执行到 jmp esp 指令时停下:
发起攻击
利用网络 ShellCode
在这里,我选者了通过攻击 FreeFloatFTPServer 从而打开系统计算器的 ShellCode。和 exploit 组装在一起的代码如下:
1 | shellcode = ( |
直接双击打开 FreeFloatFTPServer (注意不是在 Immunity Debugger 中打开),执行攻击程序,会出现如下结果:
这证明了攻击已经成功!!!
下边完整的攻击程序:
1 | 'Exploit script' |
命令生成 ShellCode
另外,ShellCode 也可以通过 Metasploit Framework 中的 msfvenom
命令(包括了 msfpayload 和 msfencode 命令)来生成。msfvenom 命令参数如下图所示:
生成打开系统计算器的 ShellCode 的命令参数样例 1:
生成打开系统计算器的 ShellCode 的命令参数样例 2:
写代码生成 ShellCode
绕过 DEP 机制
- How do ASLR and DEP work?
- ASLR/DEP绕过技术概览
- Return-Oriented-Programming
- exploit-writing-tutorial-part-10-chaining-dep-with-rop-the-rubikstm-cube
- ROP 技术绕过 DEP 初学习
- Defeating DEP with ROP 中文:利用ROP绕过DEP(Defeating DEP with ROP)调试笔记
- Return-Oriented Programming (ROP) Exploit Example
- corelan tutorial 10 exercise solution
- Rop绕过DEP和ASRL流程实例介绍
- Easy RM to MP3 Converter Buffer Overflow Exploit on Windows 7
参考资料
缓冲区溢出攻击
- 缓冲区溢出攻击
- Exploiting “Vulnerable Server” for Windows 7
- 实例讲解如何利用 Python 编写 Exploit
- 缓冲区溢出攻击专题
- 针对 FreeFloatFTPServer 的攻击
- 针对 WarFTPD 1.65 的攻击
安全相关资料
网站
资料
- Windows Exploit Development Series
- Exploit Writing Tutorial Series(翻译)
- 书籍
- Python 灰帽子
- Python 黑帽子