26

我想在 C 函数中引发堆栈下溢,以测试我系统中的安全措施。我可以使用内联汇编器来做到这一点。但是 C 会更便携。但是,我想不出使用 C 引发堆栈下溢的方法,因为在这方面,堆栈内存是由该语言安全处理的。

那么,有没有办法使用 C(不使用内联汇编程序)引发堆栈下溢?

如评论中所述:堆栈下溢意味着使堆栈指针指向堆栈开头下方的地址(“下方”用于堆栈从低到高增长的体系结构)。

4

10 回答 10

46

在 C 中很难引发堆栈下溢是有充分理由的。原因是符合标准的 C 没有堆栈。

阅读 C11 标准,您会发现它讨论了范围,但没有讨论堆栈。这样做的原因是该标准尽可能地避免在实现上强制任何设计决策。对于特定的实现,您可能能够找到一种在纯 C 中导致堆栈下溢的方法,但它将依赖于未定义的行为或实现特定的扩展,并且不可移植。

于 2017-11-07T09:42:47.860 回答
17

您不能在 C 中执行此操作,仅仅是因为 C 将堆栈处理留给了实现(编译器)。同样,您不能在 C 中编写一个错误,即您将某些内容压入堆栈但忘记弹出它,反之亦然。

因此,在纯 C 中不可能产生“堆栈下溢”。在 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(强调我的):

一元 * 运算符表示间接。如果操作数指向一个函数,则结果是一个函数指示符;如果它指向一个对象,则结果是一个指定该对象的左值。如果操作数的类型为 ''pointer to type'',则结果的类型为 ''type''。如果已为指针分配了无效值,则一元 * 运算符的行为未定义 102)

那么问题是“无效值”的定义是什么,因为这不是标准定义的正式术语。脚注 102(信息性而非规范性)提供了一些示例:

通过一元 * 运算符取消引用指针的无效值包括空指针、与指向的对象类型不适当对齐的地址以及对象在其生命周期结束后的地址。

在上面的例子中,我们显然不是在处理一个空指针,也不是一个已经过了生命周期的对象。代码确实可能导致访问不对齐——这是否是一个问题取决于实现,而不是 C 标准。

“无效值”的最后一种情况是特定系统不支持的地址。这显然不是 C 标准提到的,因为特定系统的内存布局没有被 C 标准涵盖。

于 2017-11-07T12:20:34.887 回答
9

在 C 中不可能引发堆栈下溢。为了引发下溢,生成的代码应该有比推送指令更多的弹出指令,这意味着编译器/解释器不正确。

在 1980 年代,有通过解释而不是编译运行 C 的 C 实现。确实其中一些使用动态向量而不是架构提供的堆栈。

堆栈内存由语言安全处理

堆栈内存不是由语言处理,而是由实现处理。可以运行 C 代码而根本不使用堆栈。

ISO 9899 和K&R都没有说明该语言中是否存在堆栈。

可以制造技巧并粉碎堆栈,但它不适用于任何实现,仅适用于某些实现。返回地址保存在堆栈中,您有修改它的写权限,但这既不是下溢也不是可移植的。

于 2017-11-07T11:35:07.943 回答
8

关于已经存在的答案:我认为在利用缓解技术的背景下谈论未定义的行为是不合适的。

显然,如果实现提供了针对堆栈下溢的缓解措施,则提供了堆栈。在实践中,void foo(void) { char crap[100]; ... }最终会将数组放在堆栈上。

对此答案的评论提示的注释:未定义的行为是一件事,原则上任何执行它的代码最终都可能被编译为绝对任何东西,包括与原始代码一点也不相似的东西。但是,漏洞利用缓解技术的主题与目标环境以及实际发生的情况密切相关。在实践中,下面的代码应该“工作”得很好。在处理这类事情时,您总是必须确认生成的程序集。

这让我想到了在实践中会产生下溢的情况(添加了 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很好地报告了这个问题。

于 2017-11-07T15:46:15.970 回答
6

根据定义,堆栈下溢是一种未定义的行为,因此触发此类条件的任何代码都必须是 UB。因此,您不能可靠地导致堆栈下溢。

也就是说,以下对可变长度数组 (VLA) 的滥用将导致在许多环境中(使用 x86、x86-64、ARM 和带有 Clang 和 GCC 的 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);
}

这将分配一个具有“负”(非常非常大)大小的 VLA,这将环绕堆栈指针并导致堆栈指针向上移动。argcargv用于防止优化取出数组。假设堆栈增长(默认在列出的架构上),这将是堆栈下溢。

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])
于 2017-11-07T18:43:06.110 回答
5

这个假设:

C会更便携

不是真的。C 没有告诉任何关于堆栈以及实现如何使用它的信息。在您的典型x86平台上,以下(非常无效的)代码将访问有效堆栈框架之外的堆栈(直到它被操作系统停止),但它实际上不会从中“弹出”:

#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);
}

因此,根据您对“堆栈下溢”的确切含义,此代码可以在某些平台上执行您想要的操作。但是从 C 的角度来看,这只是暴露了未定义的行为,我不建议使用它。它根本不是“便携式”的。

于 2017-11-07T09:54:42.390 回答
4

是否有可能在符合标准的 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 上构建它

https://godbolt.org/g/GSerDA

基本上这个程序中的流程如下

  • 主叫吧。
  • bar 在堆栈上分配了一堆空间(感谢大数组),
  • 酒吧调用 foo。
  • foo 获取返回地址的副本(使用 gcc 扩展)。这个地址指向栏的中间,在“分配”和“清理”之间。
  • foo 将地址返回给 bar。
  • bar 清理它的堆栈分配。
  • bar 将 foo 捕获的返回地址返回给 main。
  • main调用返回地址,跳到bar中间。
  • 来自 bar 的堆栈清理代码运行,但 bar 当前没有堆栈帧(因为我们跳到了它的中间)。所以堆栈清理代码会下溢堆栈。

我们需要 -fno-inline 来阻止优化器内联内容并破坏我们精心布置的结构。我们还需要编译器通过计算而不是使用帧指针来释放堆栈上的空间,-fomit-frame-pointer 是当今大多数 gcc 构建的默认值,但明确指定它并没有什么坏处。

我相信这个技术应该适用于几乎任何 CPU 架构上的 gcc。

于 2017-11-07T19:09:32.583 回答
0

有一种方法可以使堆栈下溢,但它非常复杂。我能想到的唯一方法是定义一个指向底部元素的指针,然后递减它的地址值。即*(ptr)--。我的括号可能是关闭的,但你想减少指针的值,然后取消引用指针。

通常操作系统只会看到错误和崩溃。我不确定你在测试什么。我希望这有帮助。C允许你做坏事,但它试图照顾程序员。绕过这种保护的大多数方法是通过指针操作。

于 2017-11-07T14:30:49.800 回答
-2

你的意思是堆栈溢出吗?将更多的东西放入堆栈而不是堆栈可以容纳?如果是这样,递归是实现这一目标的最简单方法。

void foo();
   {foo();};

如果您的意思是尝试从空堆栈中删除东西,那么请将您的问题发布到flow 网站下的堆栈,并告诉我您在哪里找到的!:-)

于 2017-11-08T14:57:46.230 回答
-3

因此,C 中有一些较旧的库函数不受保护。strcpy 就是一个很好的例子。它将一个字符串复制到另一个字符串,直到它到达一个空终止符。一件有趣的事情是传递一个程序,该程序使用这个字符串,去掉了空终止符。它会疯狂地运行,直到它到达某个地方的空终止符。或者有一个字符串副本到自己。所以回到我之前所说的,C 支持指向任何东西的指针。您可以在最后一个元素处创建指向堆栈中元素的指针。然后,您可以使用 C 中内置的指针迭代器来递减地址的值,将地址值更改为堆栈中最后一个元素之前的位置。然后将该元素传递给pop。现在,如果您对操作系统进程堆栈执行此操作,这将非常依赖于编译器和操作系统实现。在大多数情况下,指向主函数的函数指针和减量应该可以使堆栈下溢。我没有在 C 中尝试过这个。我只在汇编语言中做过这个,在这样的工作中必须非常小心。大多数操作系统都擅长阻止这种情况,因为它长期以来一直是攻击媒介。

于 2017-11-08T13:21:37.903 回答