跳转到内容

逆向工程/常见解决方案

来自维基教科书,开放世界中的开放书籍

保护机制

[编辑 | 编辑源代码]

程序员没有很多有效的保护措施来防止大多数溢出漏洞。但是,可以做一些事情。

边界检查

[编辑 | 编辑源代码]

像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

这样,我们可以检测到堆栈是否被覆盖,因为金丝雀值已经改变。然而,一个可预测的金丝雀值是脆弱的:将该值作为溢出数据的一部分插入堆栈的攻击者可以逃避检测。出于这个原因,大多数金丝雀值是在运行时随机生成的。许多金丝雀值也包含在开头或结尾的两个空字符:字符串复制函数(如strcpywcscpy)在遇到和写入空字符后停止复制数据;如果攻击者省略了空字符,则溢出将被捕获。

这种保护方法可以捕获基本溢出,并防止函数返回到修改后的地址并执行任意代码。但是,子程序仍然会被执行——内部状态和变量被破坏——因为溢出只有在返回时才会被检测到。攻击者仍然可以利用这一点:例如,内存指针变量可以被修改为指向任意位置。如果子程序然后使用这个指针写入内存,它可能会覆盖程序地址空间中的任何内容。

指针完整性

[编辑 | 编辑源代码]

许多堆溢出通过覆盖下一个堆块开头的管理数据来变得有效,该数据通常至少包含一个链表。分配或释放覆盖的块会导致数据被写入内存中的任意地址。现在,大多数堆系统都会检查链表指向的数据,以确保它们指向另一个堆块或有效数据。

这种保护方法也存在于 Microsoft Windows 的“结构化异常处理”例程中。在调用异常处理程序(其指针驻留在堆栈上,可以被覆盖)之前,首先会检查它以确保该例程驻留在内存的可执行部分内。如果处理程序例程没有,那么它就不会被调用。

安全字符串库

[编辑 | 编辑源代码]

由于标准库字符串函数是堆栈溢出的常见原因,因此出现了许多包含“安全”字符串函数的库来尝试解决这个问题。它们中的大多数在函数参数中要求显式地提供“字符串长度”,并将复制的数据限制为该数量。

程序员显然仍然必须小心,并输入准确的字符串长度值;粗心编程仍然会导致问题。

我们将留给读者作为练习,编写一组安全的字符串函数,这些函数接受长度参数,并执行简单的边界检查以防止溢出。另一个选择是将指向“最大”堆栈位置的指针作为参数,并比较指针以防止溢出。

华夏公益教科书