是的,这是通常称为“堆栈金丝雀”的实现,这是一种堆栈缓冲区溢出保护方法。你描述的那个例子是 Visual Studio 使用的方法,自 Visual Studio 2005 以来默认启用,自 Visual Studio 2003 以来实现。它也被调用是GS protection
因为 Visual Studio 提供了标志,/GS
启用和/GS-
禁用,即保护并覆盖默认行为。
什么是堆栈缓冲区溢出保护?
有多种由不同编译器和第三方保护工具实现的堆栈缓冲区溢出保护技术,但它们都围绕着相同的基本思想:当堆栈缓冲区溢出被利用时,攻击者通常会覆盖位于堆栈上的返回地址以重定向代码执行到受控地址。堆栈金丝雀的工作原理是在ret
指令前加上某种验证,即堆栈,特别是返回地址,在ret
指令执行之前没有被攻击者更改(这将导致从堆栈中弹出并将弹出的值放入指令指针)。
提供的示例如何保护堆栈免受此类攻击?
为了回答这个问题,我们需要提供您刚刚提供的上半部分的完整实现细节。大多数堆栈溢出金丝雀保护通常包括插入函数 prolog和epilog,而您只提供了前者。
这是一个带注释的示例序言:
sub esp, 8 // allocate 8 bytes for cookie
mov eax, DWORD PTR ___security_cookie
xor eax, esp // xor cookie with current esp
mov DWORD PTR [esp+8], eax // save in stack
这个序言从分配堆栈变量的空间开始,就像任何“常规”函数一样。然后它继续获取在进程启动时随机生成的值,并将其存储在特定的内存地址中,放入寄存器EAX
,然后xor
使用当前堆栈指针对其进行匹配。然后将结果值存储在堆栈中。
和一个评论示例结语:
mov ecx, DWORD PTR [esp+8] // Read saved cookie
xor ecx, esp // Xor saved cookie, should result in the same value
call @__security_check_cookie@4 // Call a short function to validate resulting value is legit, and terminate safely otherwise
add esp, 8
然后,就在函数的ret
指令执行之前,在正常缓冲区溢出攻击情况下,它会获取被覆盖的返回地址并执行攻击者控制的代码,验证原始堆栈 cookie 值是否保留,从而触发安全故障,以防万一值与预期的堆栈 cookie 不同。
大多数基于 cookie/canary 的保护的逻辑如下:
- 在函数入口处存储一个特定的值,从攻击者的角度来看是确定的但不可预测的。最好是依赖于函数的执行条件(例如当前的 ESP)。该值应放置在函数的返回地址和任何缓冲区溢出潜在缓冲区或变量之间。
- 在函数退出时,就在使用可能被覆盖的返回地址之前,验证存储的 cookie 是否与预期相同,如果以某种方式更改则失败。
- 由于经典的缓冲区溢出攻击是以顺序方式重写整个堆栈(意味着要覆盖来自堆栈变量的返回地址
buf
,位于返回地址之间的堆栈上的所有数据buf
也可能被覆盖),任何返回地址的覆盖都必须还修改堆栈金丝雀。只要攻击者在触发函数调用之前无法预测金丝雀,由于在执行之前ret
执行的金丝雀验证,攻击就会失败。
那是基于堆栈的缓冲区溢出的结尾吗?
不,出于多种原因,关于堆栈溢出利用和保护的斗争仍在继续(并且在某些情况下仍在进行中):
- 在 Visual Studio 中实现第一个金丝雀保护后不久,针对 SEH 异常结构(分配在堆栈上以处理异常)的攻击开始了,并提供了几个防 SEH 缓冲区溢出保护(例如 SafeSEH,它经历了多个版本,直到它被在防止此类攻击(包括后者
SEHOP
)方面完全可靠。
- 此外,信息泄漏错误用于预测(并增加预测机会)金丝雀值,这使得绕过金丝雀检查并使基于堆栈的缓冲区溢出成为可能。
- 与 #2 略有相似,但特定于金丝雀保护,在某些情况下,攻击者可以利用进程的执行流程逐字节缓慢地提取金丝雀值,从而将 2**32(4294967296 种可能性)蛮力减少到仅 256 *4(1024 种可能性)蛮力,使许多攻击更加合理。
- 还使用了允许非线性覆盖的缓冲区溢出错误,以“跳过”覆盖堆栈金丝雀(或大部分)以完全避免预测金丝雀值的需要,或将修改范围减少到更低的 1 个字节范围。此类常见示例是仅在特定条件下覆盖的外观或仅覆盖双字的 1 个字节的外观。这些也会使返回地址修改受到限制,但仍然有用(有时甚至更多,在某些情况下也使用了 ASLR)。