Linux 进程堆栈被局部变量溢出(堆栈保护)
Linux process stack overrun by local variables (stack guarding)
来自 What is the purpose of the _chkstk() function?:
At the end of the stack, there is one guard page mapped as
inaccessible memory -- if the program accesses it (because it is
trying to use more stack than is currently mapped), there's an access
violation.
_chkstk()
是一个特殊的编译器辅助函数,它
ensures that there is enough space for the local variables
即它正在做一些堆栈探测(这里是 LLVM example)。
这种情况是 Windows 特有的。所以 Windows 有一些解决问题的方法。
让我们考虑 Linux(或其他类似 Unix)下的类似情况:我们有很多函数的局部变量。第一个堆栈变量访问在堆栈段后面(例如 mov eax, [esp-LARGE_NUMBER]
,这里 esp-LARGE_NUMBER 是堆栈段后面的东西)。是否有任何功能可以防止 Linux(可能是其他类 Unix)或 gcc, clang, etc? Does -fstack-check
(GCC stack checking) somehow solve this problem? This answer 等开发工具中可能出现的页面错误或其他任何内容,表明它与 _chkstk()
.[=22 非常相似=]
P.S。这些帖子 , 2 没有多大帮助。
P.P.S。一般来说,问题是关于操作系统之间的实现差异(最重要的Linux vs Windows)挣扎的方法堆栈变量的数量,爬到堆栈段后面。 C++ 和 C 标签都被添加了,因为它是关于 Linux 本地二进制生成,但汇编代码是与编译器相关的。
_chkstk
确实堆栈 probes 以确保在(可能)大分配之后按顺序触摸每个页面,例如一个分配器。因为 Windows 一次只会将堆栈增长一页,直到达到堆栈大小限制。
触摸 "guard page" 会触发堆栈增长。它不能防止堆栈溢出;我认为您在这种用法中误解了 "guard page" 的含义。
函数名称也可能具有误导性。 _chkstk
文档简单地说:当你的函数中有超过一页的局部变量时由编译器调用。它不是真正的 check 任何东西,它只是确保在使用 esp
/rsp
周围的内存之前已触及中间页面。即唯一可能的影响是:没有(可能包括有效的软页面错误)或堆栈溢出时的无效页面错误(试图触摸 Windows 拒绝增加堆栈以包含的页面。)它 确保堆栈页面通过无条件写入分配。
我想您可以将此视为检查堆栈冲突,方法是在堆栈溢出的情况下确保在继续之前触摸不可映射的页面。
Linux 将主线程堆栈1 增加任意页数(达到 [=14 设置的堆栈大小限制) =];默认 8MiB) 当您触摸旧堆栈页面下方的内存时 如果它位于当前堆栈指针上方 .
如果你触及超出增长限制的内存,或者不先移动堆栈指针,它只会发生段错误。因此Linux不需要堆栈探测,只是将堆栈指针移动您想要保留的字节数。编译器知道这一点并相应地发出代码。
另请参阅 ,了解有关 Linux 内核功能以及 Linux 上的 glibc pthreads 功能的更多底层详细信息。
Linux 上足够大的 alloca
可以将堆栈一直移动到堆栈增长区域的底部,超出其下方的保护页,并进入另一个映射;这是一个 Stack Clash。 https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash It of course requires that the program uses a potentially-huge size for alloca, dependent on user input. The mitigation for CVE-2017-1000364 是要留下一个 1MiB 的保护区域,需要比正常情况下大得多的 alloca 才能通过保护页面。
这个 1MiB 保护区低于 ulimit -s
(8MiB) 增长限制,而不是低于当前堆栈指针。它独立于 Linux 的正常堆栈增长机制。
gcc -fstack-check
gcc -fstack-check
的效果与 Windows 上始终需要的效果基本相同(MSVC 通过调用 _chkstk
实现):移动大量或运行时可变的量时,触摸前一个和新堆栈指针之间的堆栈页。
但是这些探测的目的/好处在 Linux 上是不同的; GNU/Linux 上的无错误程序的正确性永远不需要它。它 "only" 防御堆栈冲突 bugs/exploits.
在 x86-64 GNU/Linux 上,gcc -fstack-check
将(对于具有 VLA 或大型固定大小数组的函数)添加一个循环,该循环使用 or qword ptr [rsp], 0
和 sub rsp,4096
。对于已知的固定阵列大小,它可以只是一个单一的探针。 code-gen 看起来效率不高;它通常从不用于此目标。 (Godbolt 将堆栈数组传递给非内联函数的编译器资源管理器示例。)
https://gcc.gnu.org/onlinedocs/gccint/Stack-Checking.html 描述了一些控制 -fstack-check
功能的 GCC 内部参数。
如果你想绝对安全地防止堆栈冲突攻击,应该这样做。不过,正常操作不需要它,1MiB 保护页对大多数人来说就足够了。
注意 -fstack-protector-strong
是完全不同的,它防止本地数组上的缓冲区溢出覆盖 return 地址。 与堆栈无关冲突,并且攻击是针对已经在一个小本地数组上方的堆栈中的东西,而不是通过移动堆栈很多来针对其他内存区域。
脚注 1:Linux 上的线程堆栈(对于初始线程以外的线程)必须预先完全分配,因为魔法增长功能不起作用。只有进程的初始主线程才能拥有它。
(有一个 mmap(MAP_GROWSDOWN)
特性,但它 不 安全,因为没有限制,并且因为没有什么能阻止其他动态分配随机选择靠近当前堆栈下方的页面, 在堆栈冲突之前将未来的增长限制在一个很小的范围内。还因为它只有在你触摸保护页时才会增长,所以它需要堆栈探测。由于这些 showstopper 原因,MAP_GROWSDOWN
没有被使用对于线程堆栈。主堆栈的内部机制依赖于内核中的不同魔法确实防止其他分配窃取space。)
来自 What is the purpose of the _chkstk() function?:
At the end of the stack, there is one guard page mapped as inaccessible memory -- if the program accesses it (because it is trying to use more stack than is currently mapped), there's an access violation.
_chkstk()
是一个特殊的编译器辅助函数,它
ensures that there is enough space for the local variables
即它正在做一些堆栈探测(这里是 LLVM example)。
这种情况是 Windows 特有的。所以 Windows 有一些解决问题的方法。
让我们考虑 Linux(或其他类似 Unix)下的类似情况:我们有很多函数的局部变量。第一个堆栈变量访问在堆栈段后面(例如 mov eax, [esp-LARGE_NUMBER]
,这里 esp-LARGE_NUMBER 是堆栈段后面的东西)。是否有任何功能可以防止 Linux(可能是其他类 Unix)或 gcc, clang, etc? Does -fstack-check
(GCC stack checking) somehow solve this problem? This answer 等开发工具中可能出现的页面错误或其他任何内容,表明它与 _chkstk()
.[=22 非常相似=]
P.S。这些帖子
P.P.S。一般来说,问题是关于操作系统之间的实现差异(最重要的Linux vs Windows)挣扎的方法堆栈变量的数量,爬到堆栈段后面。 C++ 和 C 标签都被添加了,因为它是关于 Linux 本地二进制生成,但汇编代码是与编译器相关的。
_chkstk
确实堆栈 probes 以确保在(可能)大分配之后按顺序触摸每个页面,例如一个分配器。因为 Windows 一次只会将堆栈增长一页,直到达到堆栈大小限制。
触摸 "guard page" 会触发堆栈增长。它不能防止堆栈溢出;我认为您在这种用法中误解了 "guard page" 的含义。
函数名称也可能具有误导性。 _chkstk
文档简单地说:当你的函数中有超过一页的局部变量时由编译器调用。它不是真正的 check 任何东西,它只是确保在使用 esp
/rsp
周围的内存之前已触及中间页面。即唯一可能的影响是:没有(可能包括有效的软页面错误)或堆栈溢出时的无效页面错误(试图触摸 Windows 拒绝增加堆栈以包含的页面。)它 确保堆栈页面通过无条件写入分配。
我想您可以将此视为检查堆栈冲突,方法是在堆栈溢出的情况下确保在继续之前触摸不可映射的页面。
Linux 将主线程堆栈1 增加任意页数(达到 [=14 设置的堆栈大小限制) =];默认 8MiB) 当您触摸旧堆栈页面下方的内存时 如果它位于当前堆栈指针上方 .
如果你触及超出增长限制的内存,或者不先移动堆栈指针,它只会发生段错误。因此Linux不需要堆栈探测,只是将堆栈指针移动您想要保留的字节数。编译器知道这一点并相应地发出代码。
另请参阅
Linux 上足够大的 alloca
可以将堆栈一直移动到堆栈增长区域的底部,超出其下方的保护页,并进入另一个映射;这是一个 Stack Clash。 https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash It of course requires that the program uses a potentially-huge size for alloca, dependent on user input. The mitigation for CVE-2017-1000364 是要留下一个 1MiB 的保护区域,需要比正常情况下大得多的 alloca 才能通过保护页面。
这个 1MiB 保护区低于 ulimit -s
(8MiB) 增长限制,而不是低于当前堆栈指针。它独立于 Linux 的正常堆栈增长机制。
gcc -fstack-check
gcc -fstack-check
的效果与 Windows 上始终需要的效果基本相同(MSVC 通过调用 _chkstk
实现):移动大量或运行时可变的量时,触摸前一个和新堆栈指针之间的堆栈页。
但是这些探测的目的/好处在 Linux 上是不同的; GNU/Linux 上的无错误程序的正确性永远不需要它。它 "only" 防御堆栈冲突 bugs/exploits.
在 x86-64 GNU/Linux 上,gcc -fstack-check
将(对于具有 VLA 或大型固定大小数组的函数)添加一个循环,该循环使用 or qword ptr [rsp], 0
和 sub rsp,4096
。对于已知的固定阵列大小,它可以只是一个单一的探针。 code-gen 看起来效率不高;它通常从不用于此目标。 (Godbolt 将堆栈数组传递给非内联函数的编译器资源管理器示例。)
https://gcc.gnu.org/onlinedocs/gccint/Stack-Checking.html 描述了一些控制 -fstack-check
功能的 GCC 内部参数。
如果你想绝对安全地防止堆栈冲突攻击,应该这样做。不过,正常操作不需要它,1MiB 保护页对大多数人来说就足够了。
注意 -fstack-protector-strong
是完全不同的,它防止本地数组上的缓冲区溢出覆盖 return 地址。 与堆栈无关冲突,并且攻击是针对已经在一个小本地数组上方的堆栈中的东西,而不是通过移动堆栈很多来针对其他内存区域。
脚注 1:Linux 上的线程堆栈(对于初始线程以外的线程)必须预先完全分配,因为魔法增长功能不起作用。只有进程的初始主线程才能拥有它。
(有一个 mmap(MAP_GROWSDOWN)
特性,但它 不 安全,因为没有限制,并且因为没有什么能阻止其他动态分配随机选择靠近当前堆栈下方的页面, 在堆栈冲突之前将未来的增长限制在一个很小的范围内。还因为它只有在你触摸保护页时才会增长,所以它需要堆栈探测。由于这些 showstopper 原因,MAP_GROWSDOWN
没有被使用对于线程堆栈。主堆栈的内部机制依赖于内核中的不同魔法确实防止其他分配窃取space。)