本地变量导致Linux进程栈溢出问题(堆栈保护)

9

来自 什么是_chkstk()函数的目的?:

在栈的末尾,有一个守卫页面映射为不可访问的内存 -- 如果程序访问它(因为它试图使用比当前映射的更多的栈),就会发生访问冲突。

_chkstk() 是一个特殊的编译器辅助函数,它

确保本地变量有足够的空间

即它正在进行一些堆栈探测(这里是一个LLVM示例)。
这种情况只适用于Windows。因此,Windows有一些解决方案来解决这个问题。

让我们考虑在Linux(或其他类Unix系统)下的类似情况:我们有很多函数的局部变量。第一个堆栈变量访问在堆栈段之后(例如mov eax,[esp-LARGE_NUMBER],这里的esp-LARGE_NUMBER是堆栈段之后的内容)。在Linux(或其他类Unix系统)或开发工具(如等)中是否有任何功能可以防止可能的页面错误或其他错误?-fstack-checkGCC stack checking)是否能解决这个问题?这个答案表明它与_chkstk()非常相似。

附注:这些帖子12并没有太大帮助。

另外,这个问题主要涉及操作系统(尤其是Linux和Windows)在处理大量栈变量(超出栈段)时的实现差异。添加了C++和C标签,因为它涉及到Linux本地二进制文件的生成,但汇编代码与编译器相关。


StackClash的存在并没有什么意义。 - user253751
@MarcoBonelli 已更新。 - narotello
gcc有包含堆栈保护选项,至少可以让程序在调用栈被破坏时创建正确的转储。 - Swift - Friday Pie
2
在Linux上,您可以使用ulimitpthread_attr_setstacksize设置堆栈大小。这个内存(像往常一样)不会立即被操作系统分配。它在实际使用时才会被分配。 - geza
1个回答

8

_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不用于线程堆栈。主堆栈的内部机制依赖于内核中的不同魔术,可以防止其他分配窃取空间。


太好了!非常感谢您的澄清。那么-fstack-check与提到的问题无关吗? - narotello
"它并不能防止堆栈溢出;我认为你误解了。" - 是的。它与堆栈溢出毫不相干。但是仅靠页面保护本身看起来像是次品工作。只有在编译器修复程序(如“_chkstk”)中才真正有意义,对吧? - narotello
@narotello:我更新了这个答案,加入了一些关于堆栈冲突的信息以及当针对Linux时gcc -fstack-check实际上是做什么的。我在最初撰写这篇文章时忘记了堆栈冲突。(好的代码首先不使用可能会很大的VLAs或alloca。) - Peter Cordes
因此,人们可以得出结论,为 Windows 编写的常规代码需要使用 _chkstk()(或类似于此)进行发射,仅因为有限的守卫间隙大小(例如 4K)吗?即 fstack-check 是否与 _chkstk() 做相同的事情? - NK-cell
“在正常操作中不需要它”,如果Windows增加了间隙的大小(例如增加到1 MiB),那么对于“大多数人的正常操作”,编译器将不再需要_chkstk()(以及fstack-check也是可选的,有1MiB的间隙)。 - NK-cell
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接