在C

时间:2017-11-07 09:33:30

标签: c stack stackunderflow

我想在C函数中引发堆栈下溢,以测试我系统中的安全措施。我可以使用内联汇编程序来完成此操作。但是C会更便携。但是,我无法想到使用C引发堆栈下溢的方法,因为在这方面语言可以安全地处理堆栈内存。

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

如评论中所述:堆栈下溢意味着堆栈指针指向堆栈开头下方的地址(堆栈从低到高的架构“下方”)。

10 个答案:

答案 0 :(得分:46)

有一个很好的理由说明在C中引发堆栈下溢很困难。原因是符合标准的C没有堆栈。

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

答案 1 :(得分:16)

您不能在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(强调我的):

  

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

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

  

由unary *运算符解除引用指针的无效值是空指针,a   地址不恰当地对齐指向的对象类型,以及对象后的地址   它的一生结束。

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

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

答案 2 :(得分:9)

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

在20世纪80年代,有些C的实现通过解释来运行C,而不是通过编译。实际上他们中的一些使用动态向量而不是架构提供的堆栈。

  

堆栈内存由语言

安全处理

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

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

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

答案 3 :(得分:7)

关于已经存在的答案:我不认为在开发缓解技术的背景下讨论未定义的行为是恰当的。

显然,如果实现提供了针对堆栈下溢的缓解,则提供堆栈。实际上,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很好地报告了这个问题。

答案 4 :(得分:6)

根据定义,堆栈下溢是一种未定义的行为,因此任何触发此类条件的代码都必须是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);
}

这会分配一个“负”(非常大)大小的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])

答案 5 :(得分:5)

这个假设:

  

C会更便携

不是真的。 C不会告诉任何有关堆栈以及它如何被实现使用的信息。在典型的x86平台上,以下(可怕的无效)代码将访问有效堆栈帧之外的堆栈(直到它被操作系统停止),但它实际上并不是#34;弹出&#34;从它:

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

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

答案 6 :(得分: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在堆栈上分配一堆空间(感谢大数组),
  • bar调用foo。
  • foo获取返回地址的副本(使用gcc扩展名)。这个地址指向条形图的中间,在“分配”和“清理”之间。
  • foo将地址返回到bar。
  • bar清理它的堆栈分配。
  • bar将foo捕获的返回地址返回给main。
  • main调用返回地址,跳到栏的中间。
  • bar运行的堆栈清理代码,但bar当前没有堆栈框架(因为我们跳到了它的中间)。因此堆栈清理代码会使堆栈下溢。

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

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

答案 7 :(得分:0)

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

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

答案 8 :(得分:-2)

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

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

如果您的意思是尝试从空白堆栈中删除内容,请将您的问题发布到 流程网站下的堆栈中,并让我知道你在哪里&#39 ;发现了! : - )

答案 9 :(得分:-3)

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