使用 Splint 检测缓冲区溢出漏洞

Splint 是一个比较常用的静态分析工具,缓冲区溢出漏洞,或者说缓冲区溢出攻击,也是信息安全领域的基础知识,本文以此对用静态分析工具进行漏洞检测做一个最基本的例子。

Splint

Splint 是一个开源的静态代码检测工具,用于检查 C 语言程序安全弱点和编码错误。从官网下载源码:http://www.splint.org/downloads/splint-3.1.2.src.tgz

tar xvzf splint-3.1.2.src.tgz
cd splint-3.1.2
./configure
make
sudo make install

然后,即可在终端使用。

缓冲区溢出基础

缓冲区溢出是经典的安全漏洞,想理解它的话,需要对程序运行时系统堆栈的运行原理有基本的了解。简单地讲,一个程序能实现函数调用,主要是依赖堆栈的存在。学过计算机原理的都应该知道,计算机执行程序的方法就是 CPU 从内存中一条一条地取出指令,然后执行。而究竟去什么地方取指令,取决于 EIP 寄存器,EIP 里面记录了下一条指令的存放地址,如果想实现函数调用,或者说实现一个跳转,那么需要修改 EIP 的值,问题在于,函数执行完了之后怎么回到原来的调用处继续往下执行呢?这个过程必然需要堆栈帮忙,保存跳转前的各种状态,方便函数执行完毕后,返回主程序时,把上下文恢复成原来的情况。

程序中的每个函数都有自己的栈帧,关于栈帧不多解释了,可以参考《Linux C 程序设计大全》或者网络博客等等任何有关栈帧和内存管理的文章。

考虑下面一段程序:


int seeding (int a, int b)
{
    char buf[32];
    return 0;
}
int main()
{
    ret = seeding(2, 5);
    return 0;
}

在主函数中调用了一个 seeding 函数,那么主函数里面的逻辑如果用汇编来表示,大体上就是:


push 5
push 2
push addr_of_return
call seeding

而在 seeding 里面,则会 push ebp, mov esp, ebp,保护现场,进入自己的栈帧,等函数执行完毕,会把 ebp 恢复,之后根据 addr_of_return 的值修改 EIP,返回到主函数中。

画图不太方便, 借用网络的一张图进行示意,函数调用过程堆栈的情况大体是这样。




不用管右边,只看左边。压栈是从上往下压的,data上边的区域可以认为是主函数使用的,data 可以认为是存放了 a 和 b 两个参数,即 5 和 2,返回地址存放了主函数里面 return 语句的指令地址, ebp 存放了主函数的 ebpebp esp是什么意思这里不解释,只说重点)。缓冲区就是buf[32],因为局部变量也是存放在栈里面的,所以如果往buf里面赋值的话,内容就存放在图上的缓冲区中。

正常情况下,seeding函数返回的时候,会把图上的ebp值出栈,这样寄存器ebp就恢复到了seeding调用前的状态,之后把返回地址放到EIP中,这样,下一条指令自然就会执行主函数的 return 0语句,那么函数调用就成功结束,返回到了主函数中。

下面问题来了,C 语言是不检查数组边界的,会根据你的输入一直往后面写,所以如果你往buf[32]里面存了 48 个字符,超过了数组的容量,那么由于缓冲区上面的空间是ebp和返回地址,所以超出的部分会覆盖掉这些地方原来的值。特别的,如果精心设计超出部分的值,让我们设计好的数字恰好覆盖掉返回地址,那么程序返回的时候,就不是返回到原来的主函数,而是跳转到我们想要执行的恶意代码了。

这就是缓冲区溢出漏洞,也可以说是缓冲区溢出攻击,由于一般的做法是通过故意让缓冲区溢出,来打开一个 shell,所以这段用于溢出原缓冲区的代码又称为 shellcode。

构造带有缓冲区溢出漏洞的程序

为了做实验,我们写一个带溢出漏洞的程序。顺便将上面的原理在实践中演示一下。

首先是一个正常的程序,我们展示一下程序中调用栈的情况。源代码是:


// filename: overflow.c
#include &ltstdio.h&gt
#include &ltstring.h&gt

int func(char in[])
{
	char buf[10];

	strcpy(buf, in);
	return 0;
}

int main()
{
	char *in = "myinput";
	int ret;

	ret = func(in);

	return 0;
}

编译这段程序,注意要加上禁止堆栈保护选项:

gcc -g -fno-stack-protector overflow.c -o overflow

编译后,使用 gdb 在 func里面的return 处下断点,观察内存、esp以及栈帧情况。结果如图所示。




  • 由于是 64 位系统,所以寄存器都是 rbp,rsp,实际就是上文的 ebp,esp。
  • 由于是 64 位系统,所以地址有点长。
  • 这里只关注第二张图,后文都说的是第二张图。

可以看到,倒数第四行,rsp(堆栈栈顶) 指向了 dee0。在正数第三行开始,我们查看了 dee0 开始到 df10 的内存数据,对照 ASCII 码表可以看到,堆栈最上面的元素(def0 那行)就是程序里面的 myinput,紧接着就是 ebp(df00 那行的前两个数据),而 df00 行的第三个数据 0x0040056f,就是返回地址,与图片倒数第一行给出的栈帧情况相符,倒数第一行表明,函数结束后,应该返回 main 的 40056f 处,和堆栈中写入的数据是一致的。所以实验结果与上节原理的解释是相符的,堆栈里面先是缓冲区,缓冲区上面是 ebp,再上面是返回地址。

下面我们改动程序,让缓冲区溢出,覆盖掉返回地址。改动后的程序是这样:


// filename: exploit.c
#include &ltstdio.h&gt
#include &ltstring.h&gt

int exploit()
{
	printf("我是幼苗我自豪\n");
}

int func(char in[])
{
	char buf[16];

	strcpy(buf, in);
	return 0;
}

int main()
{
	char *in = "1234567890123456\x20\xdf\xff\xff\xff\x7f\xcc\xcc\x7d\x05\x40\x00";
	// char *in = "123456789012345"; 正常版本
	int ret;

	ret = func(in);
	return 0;
}

首先,程序里面多了一个 exploit 函数,这是我们想通过缓冲区溢出攻击来执行的恶意代码。可以注意到,func 里面 buf 的空间是 16,而在主函数中,给出的输入 in,前面的数字正好有 16 个,恰好用完 buf 的空间,后面以 16 进制方式硬编码的输入,就是我们想要通过溢出来覆盖掉 ebp 和返回地址的数据。

我们先使用 main 里面的正常版本输入来编译这个程序,看一下堆栈的情况。




和前文一样,第四行表明 rsp 是 ded0,dee0 那行数据就是输入 in 存储的字符,可以和 ASCII 码表对照来看。def0 行的前两个数据是 ebp,第三个数据就是返回地址,和倒数第一行的栈帧数据相符。

然后,更改前文源码变成溢出版本,将正常版本注释掉,编译并反汇编,查看 exploit 的函数地址。





可以看到,exploit 的调用地址是 40057d,这个数字已经填写在前文源码中 in 的最后几个字节上了, in 的作用就是让 buf 溢出,然后让这几个字节恰好覆盖在堆栈里返回地址的位置上。

用 gdb 看一下带漏洞的这个程序,结果是:





不难发现,倒数第九行 def0 行就是正常的缓冲区数据,df00 行的前两个数据是 ebp,然而 ebp 已经被我用溢出的方法非法改掉,对照前文的正常版本可以看出,第二个数据应该是 0x00007fff,我在输入 in 里面,改成了 0xcccc7fff。

当然,最重要的是,第三个数据返回地址,已经被溢出为 40057d,变成了 exploit 的调用地址,而并非原来的主函数返回地址,同时可以看到,由于非法把返回地址改写,最后几行 gdb 探测栈帧出现了异常,已经无法显示正常的栈帧。

最后,执行这个带有溢出攻击的程序,结果是:




在源码中,并没有调用 exploit 的语句,然而我们却通过利用缓冲区溢出漏洞,让这个函数执行了。

使用 Splint 分析漏洞程序

带漏洞的程序已经编出来了,最后,用静态分析工具 Splint 来分析一下它。假定我们完全不知道 exploit 这个程序的情况,就是别人给我的,那么,cd 到程序目录:

splint +bounds exploit.c

分析结果是:




它分析出 3 条警告,第一条说 exploit 函数里面没写 return 语句,第二条就是我们需要的,它展示出 strcpy 是不安全的,可能会出现 buffer 越界的隐患。(就是带有缓冲区溢出漏洞)

这就是用静态工具分析出漏洞的一个简单例子。但它究竟是用什么技术分析出来的,那我就说不清了。

文章所展示的层次,就是实验层次的会用,这工具的背后,就是各种语法分析,语义分析等等,那些学术原理。那些原理用来指导编写出这个工具。


comments powered by Disqus