_chkstk
会进行堆栈探测,以确保在进行大量分配(例如alloca)后,每个页面都按顺序被触摸。因为Windows只会逐页增加堆栈大小,直到达到堆栈大小限制。
触摸“守卫页”会触发堆栈增长。它并不防止堆栈溢出;我认为你误解了这种用法中“守卫页”的含义。
该函数名也可能会引起误解。_chkstk
文档仅表示:当您的函数中有多页本地变量时,编译器会调用此函数。它并没有真正地检查任何内容,只是确保在使用esp
/rsp
周围的内存之前已经触摸了介于它们之间的页面。即唯一可能的影响是:什么也没有(可能包括有效的软页故障)或堆栈溢出时发生的无效页面故障(尝试触摸Windows拒绝增加堆栈以包含其内容的页面)。它确保通过无条件写入来分配堆栈页面。
我猜你可以将这看作是通过在堆栈溢出的情况下确保触摸一个不可映射页面来检查堆栈冲突。
当你在旧的栈页面下面的内存位置接触到内存时(如果它在当前栈指针之上),
Linux将增加主线程堆栈1的页面数量(最多达到由ulimit -s
设置的堆栈大小限制;默认为8MiB)。如果你接触到生长极限之外的内存,或者没有先移动栈指针,它将会产生段错误。因此,Linux不需要栈探测,只需通过移动栈指针来保留所需的字节数。编译器知道这一点并相应地生成代码。有关Linux内核和Linux上的glibc pthreads所做的更多低级细节,请参见
如何使用'push'或'sub' x86指令分配堆栈内存?。
在Linux上,足够大的alloca
可以将栈移动到堆栈增长区域的底部以下,超出其下方的保护页,并进入另一个映射;这就是Stack Clash。 https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash 当然,这需要程序使用潜在巨大的alloca大小,取决于用户输入。
CVE-2017-1000364的缓解措施是留下一个1MiB的保护区域,需要比正常情况下更大的alloca才能越过保护页。
这个1MiB的保护区域位于ulimit -s
(8MiB)增长限制以下,而不是当前栈指针以下。它与Linux的正常栈增长机制是分开的。
gcc -fstack-check
gcc -fstack-check
的效果与Windows上一直需要的本质相同(MSVC通过调用_chkstk
实现):在将堆栈指针移动大量或运行时可变量时,触摸前一个和新的堆栈页。
但是,在Linux上这些探测的目的/好处不同;在GNU/Linux上,它从不需要在没有漏洞的程序中保证正确性。它“仅”防御堆栈冲突漏洞/利用。
在x86-64 GNU/Linux上,
gcc -fstack-check
会(对于具有VLA或大型固定大小数组的函数)添加一个循环,使用
or qword ptr [rsp], 0
进行堆栈探测,同时使用
sub rsp,4096
。对于已知的固定数组大小,它可以只是单个探测。代码生成看起来不是很高效;在此目标上通常从未使用过。(
Godbolt编译器资源管理器示例,将堆栈数组传递给非内联函数。)
https://gcc.gnu.org/onlinedocs/gccint/Stack-Checking.html 描述了一些控制 -fstack-check
的 GCC 内部参数。
如果您想要绝对安全地防范堆栈冲突攻击,这应该可以做到。不过,正常操作并不需要它,对于大多数人来说,1MiB 的警戒页已经足够了。
请注意,
-fstack-protector-strong
是完全不同的,它防止本地数组的缓冲区溢出导致返回地址被覆盖。这与栈冲突无关,攻击针对的是已经在小型本地数组上方的堆栈上的东西,而不是通过
移动堆栈针对内存的其他区域。
脚注1:在Linux上,除了初始线程之外的线程的线程堆栈必须完全预先分配,因为魔术增长功能不起作用。只有进程的初始线程(也称为主线程)才能拥有该功能。
(虽然有一个mmap(MAP_GROWSDOWN)
功能,但它并不安全,因为没有限制,并且因为没有阻止其他动态分配从随机选择接近当前堆栈下面的页面,将来的增长受到堆栈冲突的极小大小的限制。此外,它仅在触摸警卫页面时才会增长,因此它需要堆栈探针。由于这些无法解决的原因,MAP_GROWSDOWN
不用于线程堆栈。主堆栈的内部机制依赖于内核中的不同魔术,可以防止其他分配窃取空间。)
ulimit
或pthread_attr_setstacksize
设置堆栈大小。这个内存(像往常一样)不会立即被操作系统分配。它在实际使用时才会被分配。 - geza