逆向工程/常见解决方案
程序员没有很多有效的保护措施来防止大多数溢出漏洞。但是,可以做一些事情。
像Java和C#这样的新语言之所以如此重视它们的“自动边界检查”和“内存管理”功能,主要是因为它们有助于防止堆栈溢出(以牺牲少量性能为代价)。然而,C程序员只能靠自己,需要显式地测试每个数组的边界。这很乏味,但至少黑客不会破坏你的程序,你就不会被解雇。
一些编译器通过在堆栈上内置一个称为金丝雀或Cookie的标志值来提供帮助,通常位于已推入的帧指针和返回地址之上(想想煤矿中用来检测有毒气体积聚的笼子里的鸟,在工人中毒之前)。
push CANARY push ebp mov ebp, esp sub esp, 100
现在,当函数要返回时,我们可以执行以下操作
add esp, 100 mov esp, ebp pop ebp pop ebx ;canary value cmp ebx, CANARY jne _STACK_ERROR_FOUND ret
这样,我们可以检测到堆栈是否被覆盖,因为金丝雀值已经改变。然而,一个可预测的金丝雀值是脆弱的:将该值作为溢出数据的一部分插入堆栈的攻击者可以逃避检测。出于这个原因,大多数金丝雀值是在运行时随机生成的。许多金丝雀值也包含在开头或结尾的两个空字符:字符串复制函数(如strcpy或wcscpy)在遇到和写入空字符后停止复制数据;如果攻击者省略了空字符,则溢出将被捕获。
这种保护方法可以捕获基本溢出,并防止函数返回到修改后的地址并执行任意代码。但是,子程序仍然会被执行——内部状态和变量被破坏——因为溢出只有在返回时才会被检测到。攻击者仍然可以利用这一点:例如,内存指针变量可以被修改为指向任意位置。如果子程序然后使用这个指针写入内存,它可能会覆盖程序地址空间中的任何内容。
许多堆溢出通过覆盖下一个堆块开头的管理数据来变得有效,该数据通常至少包含一个链表。分配或释放覆盖的块会导致数据被写入内存中的任意地址。现在,大多数堆系统都会检查链表指向的数据,以确保它们指向另一个堆块或有效数据。
这种保护方法也存在于 Microsoft Windows 的“结构化异常处理”例程中。在调用异常处理程序(其指针驻留在堆栈上,可以被覆盖)之前,首先会检查它以确保该例程驻留在内存的可执行部分内。如果处理程序例程没有,那么它就不会被调用。
由于标准库字符串函数是堆栈溢出的常见原因,因此出现了许多包含“安全”字符串函数的库来尝试解决这个问题。它们中的大多数在函数参数中要求显式地提供“字符串长度”,并将复制的数据限制为该数量。
程序员显然仍然必须小心,并输入准确的字符串长度值;粗心编程仍然会导致问题。
我们将留给读者作为练习,编写一组安全的字符串函数,这些函数接受长度参数,并执行简单的边界检查以防止溢出。另一个选择是将指向“最大”堆栈位置的指针作为参数,并比较指针以防止溢出。