8.1 Bug 的分类

当 hacker 或者逆向工程师分析软件漏洞的时候,都会设法找到能控制程序执行的 bug。 Fuzzer 就提供了一种自动化的方式帮助我们找出 bug,然后获得系统的控制权,提升权限, 并且偷取程序访问的信息,不论目标程序是在系统独立运行的进程,还是只运行脚本的网络 程序。在这里我们关注的是独立运行的进程,以及其泄漏的信息。

8.1.1 缓冲区溢出

缓冲区溢出是最常见的软件漏洞。所有的正常的内存管理函数,字符处理代码甚至编程语言本身都可能产生缓冲区溢出漏洞,致使软件出错。

简而言之,缓冲区溢出就是由于,把个过多的数据存储在一个过小的内存空间里,所引发的软件错误。对于其原理的可以用个很形象的比喻来说明。如果一个水桶,只能装一加仑 的水,我们只导入几滴水或者半加仑的水,甚至是一加仑,把水桶填满了,都不会出问题, 一切都正常如初。如果我们导入两加仑的水,那水就会溢出到地板上,到时候,你就得收拾 这堆烂摊子了。用在软件上也是一样的,如果太多的水(数据),倒入到一个桶(buffer)内, 水就会溢出到作为的地表(memory)上。当一个攻击者能够控制多余的数据对内存区域的 覆盖,就拿那个得到代码的执行权限,进一步获取到系统信息或者做别的事。有两种主要的 缓冲区溢出:基于栈的和基于堆的。两种溢出的表现不同,但产生的结果相同:攻击者最终 控制代码的执行。

栈溢出的特点就是通过溢出覆盖栈,来控制程序的执行流程:比如改变函数的指针,变量的值,异常处理程序,或者覆盖函数的返回地址,可以得到代码的执行权限。栈溢出的时 候,会抛出一个访问违例;这样我们就能够在 fuzzing 的时候非常方便的跟踪到它们。

应用程序在运行的时候会动态的申请一块内存区域,这些区域就是堆。堆是一块一块连在一起的,负责存储元数据。当攻击者将数据覆盖到自己申请的堆以外的别的堆的时候,堆 溢出就发生了。接着攻击者能通过覆盖数据改变任何存储在堆上的数据:变量,函数指针, 安全令牌,以及各种重要的数据。堆溢出很难被立即的跟踪到,因为被影响的内存快,一般 不会被程序立即的访问,需要等到程序结束之前的某个时间内才有可能访问到,当然也有可 能一直不访问。在 fuzzing 的时候我们就必须一直等到一个访问违例产生了才会知道,这个 堆溢出是否在成功了。

MICROSOFT GLOBAL FLAGS

这项技术是专门为软件开发者(or exploit writer)专门设计的。Gflags(Global flags 全局标 志)是一系列的诊断和调试设置,能够让你非常精确的跟踪,记录,和调试软件。在 2000, xp 和 2003 上都能够使用这项技术。

这项技术最有趣的地方在于堆页面的校对。当我们在一个进程上打开这个选项的时候,校对器会动态的更重内存的操作,包括内存的申请和释放。不过真正令人高兴的特点是,它 能够在堆溢出发生的时候立刻产生一个调试中断,这样调试器就会停在产生错误的指令上。 这样在下面的调试中遇到了堆相关的 bug 的时候我们就能方便的查找到源头在哪。

我们能够使用gflags.exe来编辑Gflags标志帮助我们跟踪堆溢出。 http://www.microsoft.com/downloads/details.aspx?FamilyId=49AE8576-9BB9-4126-9761-BA80 这个软件是 Microsoft "免费"提供的,请"放心"安装。

Immunity 也提供了一个 Gflags 库,并且将它和 PyCommand 结合在了一起。更多的信 息访问 http://debugger.immunityinc.com/.

为了通过 fuzzing 溢出目标程序,我们会简单的传递给程序大量的数据,然后跟踪程序, 找出利用了我们传入数据的代码,然后祈祷它没有合格验证数据长度的,my god!

接下来看看另一个常见的应用程序漏洞,Integer Overflows 整数溢出。

8.1.2 Integer Overflows

整数溢出是一种非常有趣的漏洞,包括程序如何处理(编译器标准的)有符号整数和 exploit 如何利用这个整数。一个有符号整数,由 2 个字节组成,表示的范围从 -32767 到 32767。当我们从尝试向存储一个整数的地方写入超过其大小的数字的时候,整数溢出就触 发了。因为存如的数字太大,处理器会自动的将高位多出来的字节丢弃。初看起来,好像没 什么值得利用的。下面让我们看一个设计好的例子:

MOV EAX, [ESP + 0x8] 
LEA EDI, [EAX + 0x24] 
PUSH EDI
CALL msvcrt.malloc

第一条指令将栈内的数据[esp+0x8]传给 EAX,第二条指令,将 EAX 加上 0x24 这个地 址存储在 EDI 中。之后我们将这个唯一的参数(申请的内存的大小)传入函数 malloc。一切看 起来都很正常,真的是这样吗?假设在栈中的数据是一个有符号整数,而且非常大,几乎接 近了有符号整数的最大值( 32767),然后传递给 EAX,EAX 加上 0x24,整数溢出,最后我 们得到一个非常小的值。看一看表 8-1,看看这一切是如何发生的,假定在堆上的参数是我 们能够控制的,我们给它设置成一个非常大的值 0xFFFFFFF5。

Stack Parameter => 0xFFFFFFF5 
Arithmetic Operation => 0xFFFFFFF5 + 0x24
Arithmetic Result => 0x100000019 (larger than 32 bits) 
Processor Truncates => 0x00000019

Listing 8-1:在控制下的整数操作

如何一切顺利,malloc 将只申请 0x19 个字节大小的空间,这块内存比程序本身要申请 的空间小很多。如果程序将一大块的数据写入这块区域,缓冲区溢出就发生了。在 fuzzing 的时候,我们得从整形最大值和最小值两个方面入手,测试执行溢出,接下来就是设法进一 步控制溢出,使溢出变得更完美。

下面让我们快速的看一看另一种常见漏洞,格式化字符串漏洞 Format String Attacks。

8.1.3 Format String Attacks

格式化字符串攻击,顾名思义,攻击者通过将设计好的字符串传入特定字符串格式化函 数,使其产生溢出,列如 C 语言的 printf。让我们先看看 printf 的原型:

int printf( const char * format, ... );

第一个参数是一个完整需要被格式化的字符串,我们可以附加额外的参数,表示数据将 以什么形式被输出。举个例子:

int test = 10000;
printf("We have written %d lines of code so far.", test); 
Output:
We have written 10000 lines of code so far.

%d 是一个模式说明符,格式指定符指定了特定的输出格式(变量 test 以数字的形式输 出)。如果一个程序员不小心睡着了(压榨 严重的压榨),写出了下面的代码:

char* test = "%x"; 
printf(test); 
Output: 5a88c3188

这看起来和上面的很不同。我们传递了一个模式说明符给 printf 函数,但是没有传递需 要打印的变量。printf 会分析我们传递给它的参数,并且假设栈中的下一个参数就是需要打 印的参数,但是其实这个是毫无效果的一个数据。在这个例子中是 0x5a88c3188,也许是存 在栈上的数据,也有可能跟是一个指向内存的指针。有两个指示符很有趣,一个是 %s,另 一个是 %n。 %s 指示符告诉字符串函数,把内存当作字符串来扫描,直到遇到一个 NULL 字符,代表字符串结束了。这对于读取一大块连续的数据或者读取特定地址的数据都十分有 用,当然你也可以用它来 crash 程序。%n 指示符(惟一一个)允许向内存写入内存,而不 仅仅是格式化字符串。这就允许,攻击者覆盖函数的返回地址,或者改写一个以存在的函数 指针,以获得代码的执行权限。在 fuzzing 的似乎后,我们只要在测试用例中加入这些特定 格式说明符,然后传递给一个被错误使用了的字符串处理函数。

现在我们已经对不同的 bug 类型有了个个大概的了解,是时候开始创造第一个 fuzzer 了。接下来我们会简单的实现一个 file fuzzer,它先将正常的文件变形之后拿去给程序处理。 这次继续使用我们久违的老朋友 PyDbg。Come on!!!