在 C 中引发堆栈下溢
Provoke stack underflow in C
我想在 C 函数中引发堆栈下溢以测试我系统中的安全措施。我可以使用内联汇编器来做到这一点。但是 C 会更便携。但是,我想不出一种使用 C 来引发堆栈下溢的方法,因为在这方面,堆栈内存由该语言安全地处理。
那么,有没有办法使用 C(不使用内联汇编程序)引发堆栈下溢?
如评论中所述:堆栈下溢意味着堆栈指针指向堆栈开头下方的地址("below" 对于堆栈从低到高增长的体系结构)。
很难在 C.The 中引发堆栈下溢的一个很好的原因是符合标准的 C 没有堆栈。
读一读C11标准,你会发现它讲的是作用域,但没有讲栈。这样做的原因是该标准尽可能避免将任何设计决策强加于实现。对于特定的实现,您也许能够找到一种在纯 C 中导致堆栈下溢的方法,但它将依赖于未定义的行为或特定于实现的扩展,并且不可移植。
这个假设:
C would be more portable
不正确。 C 没有说明任何有关堆栈的信息以及实现如何使用它。在典型的 x86
平台上,以下( 极其无效 )代码将访问有效堆栈框架之外的堆栈(直到它被 OS 停止) ,但它实际上不会 "pop" 来自它:
#include <stdarg.h>
#include <stdio.h>
int underflow(int dummy, ...)
{
va_list ap;
va_start(ap, dummy);
int sum = 0;
for(;;)
{
int x = va_arg(ap, int);
fprintf(stderr, "%d\n", x);
sum += x;
}
return sum;
}
int main(void)
{
return underflow(42);
}
因此,根据您对 "stack underflow" 的确切含义,此代码可以在 some 平台上执行您想要的操作。但是从 C 的角度来看,这只是暴露了 未定义的行为 ,我不建议使用它。 根本不是"portable"。
在 C 中不可能引发堆栈下溢。为了引发下溢,生成的代码应该有比压入指令更多的弹出指令,这意味着 compiler/interpreter 不合理。
在 1980 年代,运行 C 通过解释而非编译实现了 C。确实其中一些使用了动态向量而不是架构提供的堆栈。
stack memory is safely handled by by the language
堆栈内存不是由语言处理的,而是由实现处理的。 运行 C 代码完全不使用堆栈是可能的。
ISO 9899 和 K&R 均未指定任何有关语言中堆栈存在的信息。
可以耍花招,砸栈,但对任何实现都不起作用,只对某些实现起作用。 return 地址保存在堆栈中,您有修改它的写权限,但这既不是下溢也不是可移植的。
您不能在 C 中执行此操作,原因很简单,因为 C 将堆栈处理留给了实现(编译器)。同样,您不能在 C 语言中编写错误,即您将某些内容压入堆栈但忘记将其弹出,反之亦然。
因此,在纯C中是不可能产生"stack underflow"的。C中不能出栈,C中也不能设置栈指针。栈的概念是关于甚至比C语言还低。为了直接访问和控制堆栈指针,必须编写汇编程序。
您可以 在 C 中做的是有意地写出堆栈的边界。假设我们知道栈从 0x1000 开始向上增长。那么我们可以这样做:
volatile uint8_t* const STACK_BEGIN = (volatile uint8_t*)0x1000;
for(volatile uint8_t* p = STACK_BEGIN; p<STACK_BEGIN+n; p++)
{
*p = garbage; // write outside the stack area, at whatever memory comes next
}
为什么你需要在不使用汇编程序的纯 C 程序中测试它,我不知道。
如果有人错误地认为上面的代码调用了未定义的行为,这就是 C 标准实际所说的,规范文本 C11 6.5.3.2/4(强调我的):
The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; if it points to an object, the result is an lvalue designating the object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined 102)
那么问题是 "invalid value" 的定义是什么,因为这不是标准定义的正式术语。脚注 102(信息性而非规范性)提供了一些示例:
Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an address inappropriately aligned for the type of object pointed to, and the address of an object after the end of its lifetime.
在上面的例子中,我们显然不是在处理空指针,也不是在处理生命周期结束的对象。代码确实可能导致未对齐的访问 - 这是否是一个问题取决于实现,而不是 C 标准。
而 "invalid value" 的最后一种情况是特定系统不支持的地址。这显然不是C标准中提到的,因为具体系统的内存布局并没有被C标准覆盖。
栈下溢是有办法的,但是很复杂。我能想到的唯一方法是定义一个指向底部元素的指针,然后递减其地址值。 IE。 *(指针)--。我的括号可能已关闭,但您想递减指针的值,然后取消引用指针。
通常 OS 只会看到错误并崩溃。我不确定你在测试什么。我希望这有帮助。 C允许你做坏事,但它试图照顾程序员。大多数绕过这种保护的方法是通过操纵指针。
关于已经存在的答案:我认为在利用缓解技术的背景下讨论未定义的行为是不合适的。
显然,如果实现提供了针对堆栈下溢的缓解措施,那么就会提供堆栈。实际上,void foo(void) { char crap[100]; ... }
最终会将数组放在堆栈上。
对此答案的评论提示的注释:未定义的行为是一回事,原则上任何使用它的代码最终都可以编译成任何东西,包括与丝毫的原始代码。但是,漏洞利用缓解技术的主题与目标环境以及实践中发生的情况 密切相关。实际上,下面的代码应该 "work" 就好了。在处理这种东西时,您总是必须验证生成的程序集才能确定。
这让我想到在实践中会产生下溢(添加 volatile 以防止编译器优化它):
static void
underflow(void)
{
volatile char crap[8];
int i;
for (i = 0; i != -256; i--)
crap[i] = 'A';
}
int
main(void)
{
underflow();
}
Valgrind 很好地报告了问题。
根据定义,堆栈下溢是一种未定义的行为,因此任何触发这种情况的代码都必须是 UB。因此,您无法可靠地导致堆栈下溢。
也就是说,以下滥用可变长度数组 (VLA) 会在许多环境中导致可控的堆栈下溢(使用 Clang 和 GCC 使用 x86、x86-64、ARM 和 AArch64 进行测试),实际设置堆栈指向其初始值之上的指针:
#include <stdint.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
uintptr_t size = -((argc+1) * 0x10000);
char oops[size];
strcpy(oops, argv[0]);
printf("oops: %s\n", oops);
}
这会分配一个大小为 "negative"(非常非常大)的 VLA,这将环绕堆栈指针并导致堆栈指针向上移动。 argc
和argv
用于防止优化取出数组。假设堆栈向下增长(所列架构的默认值),这将是堆栈下溢。
strcpy
将在调用时或在写入字符串时触发对下溢地址的写入,如果 strcpy
是内联的。最终的 printf
应该无法访问。
当然,这一切都假设编译器不仅使 VLA 成为某种临时堆分配——编译器可以完全自由地进行分配。您应该检查生成的程序集以验证上面的代码是否按照您实际期望的那样执行。例如,在 ARM (gcc -O
) 上:
8428: e92d4800 push {fp, lr}
842c: e28db004 add fp, sp, #4, 0
8430: e1e00000 mvn r0, r0 ; -argc
8434: e1a0300d mov r3, sp
8438: e0433800 sub r3, r3, r0, lsl #16 ; r3 = sp - (-argc) * 0x10000
843c: e1a0d003 mov sp, r3 ; sp = r3
8440: e1a0000d mov r0, sp
8444: e5911004 ldr r1, [r1]
8448: ebffffc6 bl 8368 <strcpy@plt> ; strcpy(sp, argv[0])
是否可以在符合标准的 C 语言中可靠地做到这一点?否
是否可以在至少一个实用的 C 编译器上完成它而不求助于内联汇编器?是
void * foo(char * a) {
return __builtin_return_address(0);
}
void * bar(void) {
char a[100000];
return foo(a);
}
typedef void (*baz)(void);
int main() {
void * a = bar();
((baz)a)();
}
使用“-O2 -fomit-frame-pointer -fno-inline”在 gcc 上构建它
基本上这个程序的流程如下
- 主要调用栏。
- bar在栈上分配了一堆space(感谢大数组),
- bar 调用 foo。
- foo 获取 return 地址的副本(使用 gcc 扩展)。该地址指向栏的中间,在 "allocation" 和 "cleanup" 之间。
- foo returns bar 的地址。
- bar 清理它的堆栈分配。
- bar returns 由 foo 捕获到 main 的 return 地址。
- main调用return地址,跳转到bar中间
- 运行 bar 的堆栈清理代码,但 bar 当前没有堆栈帧(因为我们跳到了它的中间)。所以堆栈清理代码下溢堆栈。
我们需要 -fno-inline 来阻止优化器内联内容并破坏我们精心设计的结构。我们还需要编译器通过计算而不是使用帧指针来释放堆栈上的 space,-fomit-frame-pointer 是当今大多数 gcc 构建的默认设置,但指定它也无妨明确地。
我相信这个技术应该适用于几乎任何 CPU 架构上的 gcc。
所以 C 中有一些较旧的库函数不受保护。 strcpy 就是一个很好的例子。它将一个字符串复制到另一个字符串,直到它到达一个空终止符。一件有趣的事情是传递一个程序,该程序使用这个删除了空终止符的字符串。它会 运行 发疯,直到它到达某个地方的空终止符。或者有一个字符串副本给自己。所以回到我之前所说的,C 支持指向任何东西的指针。您可以在最后一个元素处创建一个指向堆栈中元素的指针。然后你可以使用C内置的指针迭代器来减少地址的值,将地址值更改为堆栈中最后一个元素之前的位置。然后将该元素传递给 pop。现在,如果您对操作系统进程堆栈执行此操作,它将非常依赖于编译器和操作系统实现。在大多数情况下,指向 main 的函数指针和递减量应该可以使堆栈下溢。我没有在 C 中尝试过这个。我只在汇编语言中做过这个,在这样的工作中必须非常小心。大多数操作系统都擅长阻止这种情况,因为它长期以来一直是一种攻击媒介。
你是说堆栈溢出?将更多的东西放入堆栈中,而不是堆栈可以容纳的东西?如果是这样,递归是实现它的最简单方法。
void foo();
{foo();};
如果您的意思是尝试从空堆栈中删除 东西,那么请post将您的问题提交到堆栈下流网站,让我知道你在哪里找到的! :-)