内容: 本文主要讲解有关Buffer Overflow的原理, 以及结合实战范例介绍Linux和Solaris下的漏洞利用.
本文并不介绍如何编写shell code.
要求: 读者要有一点C和汇编语言基础.
目标: 希望本文能够尽量做到通熟易懂,使得稍有计算机基础知识的朋友看后能够亲自动手写自己的Exploit
如果你觉得自己对这些都懂了, 就请不要再往下看了.
第一部份 概述篇
1. Buffer overflow是如何产生的?
所谓Buffer overflow, 中文译为缓冲区溢出. 顾名思意, 就是说所用的缓冲区太小了, 以至装不下
那么多的东西, 多出来的东西跑出来了. 就好象是水缸装不了那么多的水, 硬倒太多会溢出来一样
那么, 在编程过程中为什么要用到buffer(缓冲区)呢? 简单的回答就是做为数据处理的中转站.
2. UNIX下C语言函数调用的机制及缓冲区溢出的利用.
1) 进程在内存中的影像.
我们假设现在有一个程序, 它的函数调用顺序如下.
main(...) - func_1(...) - func_2(...) - func_3(...)
即: 主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3
当程序被操作系统调入内存运行, 其相对应的进程在内存中的影像如下图所示.
(内存高址)
+--------------------------------------+
| ...... | ... 省略了一些我们不需要关心的区
+--------------------------------------+
| env strings (环境变量字串) | +--------------------------------------+ | argv strings (命令行字串) | +--------------------------------------+ | env pointers (环境变量指针) | SHELL的环境变量和命令行参数保存区
+--------------------------------------+ /
| argv pointers (命令行参数指针) | /
+--------------------------------------+ /
| argc (命令行参数个数) | /
+--------------------------------------+
| main 函数的栈帧 | +--------------------------------------+ | func_1 函数的栈帧 | +--------------------------------------+ | func_2 函数的栈帧 | +--------------------------------------+ | func_3 函数的栈帧 | Stack (栈)
+......................................+ /
| | /
...... /
| | /
+......................................+ /
| Heap (堆) | /
+--------------------------------------+
| Uninitialised (BSS) data | 非初始化数据(BSS)区
+--------------------------------------+
| Initialised data | 初始化数据区
+--------------------------------------+
| Text | 文本区
+--------------------------------------+
(内存低址)
这里需要说明的是:
i) 随着函数调用层数的增加, 函数栈帧是一块块地向内存低地址方向延伸的。随着进程中函数调用层数的减少, 即各函数调用的返回, 栈帧会一块块地被遗弃而向内存的高址方向回缩。各函数的栈帧大小随着函数的性质的不同而不等, 由函数的局部变量的数目决定。
ii) 进程对内存的动态申请是发生在Heap(堆)里的。 也就是说, 随着系统动态分配给进程的内存数量的增加, Heap(堆)有可能向高址或低址延伸, 依赖于不同CPU的实现。 但一般来说是向内存的高地址方向增长的。
iii) 在BSS数据或者Stack(栈)的增长耗尽了系统分配给进程的自由内存的情况下,进程将会被阻塞, 重新被操作系统用更大的内存模块来调度运行。(虽然和exploit没有关系, 但是知道一下还是有好处的)
iv) 函数的栈帧里包含了函数的参数(至于被调用函数的参数是放在调用函数的栈帧还是被调用函数栈帧, 则依赖于不同系统的实现),它的局部变量以及恢复调用该函数的函数的栈帧(也就是前一个栈帧)所需要的数据, 其中包含了调用函数的下一条执行指令的地址。
v) 非初始化数据(BSS)区用于存放程序的静态变量, 这部分内存都是被初始化为零的。初始化数据区用于存放可执行文件里的初始化数据。这两个区统称为数据区。
vi) Text(文本区)是个只读区, 任何尝试对该区的写操作会导致段违法出错。 文本区是被多个运行该可执行文件的进程所共享的。 文本区存放了程序的代码。
2) 函数的栈帧.
函数调用时所建立的栈帧包含了下面的信息:
i) 函数的返回地址. 返回地址是存放在调用函数的栈帧还是被调用函数的栈帧里,取决于不同系统的实现.
ii) 调用函数的栈帧信息, 即栈顶和栈底.
iii) 为函数的局部变量分配的空间
iv) 为被调用函数的参数分配的空间--取决于不同系统的实现.
3) 缓冲区溢出的利用.
从函数的栈帧结构可以看出:
由于函数的局部变量的内存分配是发生在栈帧里的, 所以如果我们在某一个函数里定义了缓冲区变量, 则这个缓冲区变量所占用的内存空间是在该函数被调用时所建立的栈帧里.
由于对缓冲区的潜在操作(比如字串的复制)都是从内存低址到高址的, 而内存中所保存的函数调用返回地址往往就在该缓冲区的上方(高地址)――这是由于栈的特性决定的, 这就为复盖函数的返回地址提供了条件。 当我们有机会用大于目标缓冲区大小的内容来向缓冲区进行填充时, 就有可以改写函数保存在函数栈帧中的返回地址, 从而使程序的执行流程随着我们的意图而转移。 换句话来说, 进程接受了我们的控制。 我们可以让进程改变原来的执行流程, 去执行我们准备好的代码。
这是冯.诺曼计算机体系结构的缺陷.
下面是缓冲区溢出利用的示意图:
i) 函数对字串缓冲区的操作, 方向一般都是从内存低址向高址的.
如: strcpy(s, "AAA.....";
s s+1 s+2 s+3 ...
+---+---+---+--------+---+...+
(内存低址) | A | A | A | ...... | A |...| (内存高址)
+---+---+---+--------+---+...+
ii) 函数返回地址的复盖
/ | ...... | (内存高址)
/ +--------------------+
调用函数栈帧 | 0x41414141 |
\ +--------------------+
\ | 0x41414141 | 调用函数的返回地址
\+--------------------+
/| ...... |
/ +--------------------+ s+8
/ | 0x41414141 |
/ +--------------------+ s+4
被调用函数栈帧 | 0x41414141 |
\ +--------------------+ s
\ | 0x41414141 |
\ +--------------------+
\| ...... |
+....................+
| ...... | (内存低址)
注: 字符A的十六进制ASCII码值为0x41.
iii) 从上图可以看出: 如果我们用的是进程可以访问的某个地址而不是0x41414141来改写调用函数的返回地址, 而这个地址正好是我们准备好的代码的入口, 那么进程将会执行我们的代码。 否则, 如果用的是进程无法访问的段的地址, 将会导致进程崩馈――Segment Fault Core dumped (段出错内核转储); 如果该地址处有无效的机器指令数据, 将会导致非法指令(Illigal Instruction)错误, 等等。
4) 缓冲区在Heap(堆)区或BBS区的情况
i) 如果缓冲区的内存空间是在函数里通过动态申请得到的(如: 用malloc()函数申请), 那么在函数的栈帧中只是分配了存放指向Heap(堆)中相应申请到的内存空间的指针。 这种情况下, 溢出是发生在(Heap)堆中的, 想要复盖相应的函数返回地址, 看来几乎是不可能的。 这种情况的利用可能性要看具体情形, 但不是不可能的。
ii) 如果缓冲区在函数中定义为静态(static), 则缓冲区内存空间的位置在非初始化(BBS)区,和在Heap(堆)中的情况差不多, 利用是可能的。 但还有一种特姝情况, 就是可以利用它来复盖函数指针, 让进程后来调用相应的函数变成调用我们所指定的代码。
3. 从缓冲区溢出的利用可以得到什么?
从上文我们看到, 缓冲区溢出的利用可以使我们能够改写相关内存的内容及函数的返回地址, 从而改变代码的执行流程, 让进程去执行我们准备好的代码。
但是, 进程是以我们当前登录的用户身份来运行的. 能够执行我们准备好的代码又怎样呢? 我们还是无法突破系统对当前用户的权限设置, 无法干超越权限的事.
换句话来说, 要想利用缓冲区溢出得到更高的权限, 我们还得利用系统的一些特性.
对于UNIX来讲, 有两个特性可以利用.
i) SUID及SGID程序
UNIX是允许其他用户可以以某个可执行文件的文件拥有者的用户ID或用户组ID的身份来执行该文件的,这是通过设置该可执行文件的文件属性为SUID或SGID来实现的。也就是说如果某个可执行文件被设了SUID或SGID, 那么当系统中其他用户执行该文件时就相当于以该文件属主的用户或用户组身份来执行该文件。如果某个可执行文件的属主是root, 而这个文件被设了SUID, 那么如果该可执行文件存在可利用的缓冲区溢出漏洞, 我们就可以利用它来以root的身份执行我们准备好的代码。 没有比让它为我们产生一个具有超级用户root身份的SHELL更吸引人了, 是不是?
ii) 各种端口守护(服务)进程
UNIX中有不少守护(服务)进程是以root的身份运行的, 如果这些程序存在可利用的缓冲区溢出,那么我们就可以让它们以当前运行的用户身份――root去执行我们准备被好的代码。由于守护进程已经以root的身份在运行, 我们并不需要相对应的