栈缓存溢出

原文地址

0 目录


在软件程序中,如果对一个固定长度的目标数据结构进行写操作时,如果超出了其长度,就会写入程序所调用的栈中的相邻内存地址区域中。这就发生了栈溢出(stack buffer overflow)。这会导致被覆盖的内存区域中的数据损坏,从而导致程序崩溃或运行不正常。相比堆上的缓存溢出,栈缓存溢出更有可能破坏程序的执行。因为栈内包含函数的返回地址,这个地址是有效的函数指针,可以被修改执行。

作为攻击的一部分,故意造成栈缓存溢出就是 栈溢出攻击 。如果被攻击的程序正在以特权等级运行,或者接受不受信任的网络主机的数据,那么这个bug就是一个潜在的风险。如果,栈缓存被不受信任的用户填充数据,它们就可以在正在执行的程序内嵌入自己的可执行代码,控制当前进程。这是攻击者未经授权访问计算机的最古老,最可靠的方法之一。

1 利用栈缓存溢出

利用栈缓存溢出的常规方法就是使用一个指针覆盖掉函数的返回地址,该指针指向攻击者所使用的数据(也就是攻击者可以为所欲为了)。在下面的例子中,使用 strcpy() 函数进行阐述:

#include <string.h>

void foo (char *bar)
{
   char  c[12];

   strcpy(c, bar);  // 没有边界检查
}

int main (int argc, char **argv)
{
   foo(argv[1]);

   return 0;
}

这段代码从命令行接受一个实参,并将其拷贝到栈上的局部变量 c 里。从B图中我们可以看出,如果命令行参数传递的字符个数少于12个时程序运行良好。但一旦命令行参数大于12个字符时,就会发生栈损坏。(C语言中字符串使用 ‘\0’ 作为结束符)。

函数 foo() 在几种不同的输入命令行参数下的栈分布:

Figure 15-5-1-ABC

注意在上面的图C中,当在命令行上提供大于11字节的参数时,foo()会覆盖本地堆栈数据,保存的帧指针,最重要的是,函数的返回地址。 当foo()返回时,它会从堆栈中弹出返回地址并跳转到该地址(即从该地址开始执行指令)。 因此,攻击者用指向堆栈缓冲区 char c [12] 的指针覆盖了返回地址,该指针现在包含攻击者提供的数据。 在实际的堆栈缓冲区溢出漏洞中,“A”字符串将改为适合于平台和所需函数的shellcode。 如果此程序具有特殊权限(例如,SUID位设置为以超级用户身份运行),则攻击者可以使用此漏洞在受影响的计算机上获得超级用户权限。[3]

攻击者可以可用这些错误修改内部变量值,看下面这个例子:

#include <string.h>
#include <stdio.h>

void foo (char *bar)
{
   float My_Float = 10.5; // 假设 Addr = 0x0023FF4C
   char  c[28];           // 假设 Addr = 0x0023FF30

   // 打印 10.500000
   printf("My Float value = %f\n", My_Float);

    /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Memory map:
        @ : c 分配的内存
        # : My_Float 分配的内存

            *c                        *My_Float
         0x0023FF30                  0x0023FF4C
             |                           |
             @@@@@@@@@@@@@@@@@@@@@@@@@@@@#####
        foo("my string is too long !!!!! XXXXX");

        memcpy() 将会把地址0x1010C042(小端模式)存入变量My_Float中
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/

    memcpy(c, bar, strlen(bar));  // 没有边界检查

    // 将会打印 96.031372
    printf("My Float value = %f\n", My_Float);
}

int main (int argc, char **argv)
{
   foo("my string is too long !!!!! \x10\x10\xc0\x42");
   return 0;
}

2 平台间的差异

许多系统架构在调用栈的实现方面存在细微差别,这些差异会影响 栈缓冲区溢出漏洞 的工作方式。 某些机器架构将调用堆栈内的顶层返回地址存储在寄存器中。这意味着在稍后展开调用的栈之前,不会使用任何覆盖的返回地址。 可能影响开发技术选择的机器特定细节的另一个例子是大多数RISC型机器架构不允许对内存进行未对齐访问。结合机器操作码的固定长度,这种机器限制可能使得跳转到ESP技术几乎不可能实现(一个例外是当程序实际包含显式跳转到栈寄存器的不太可能的代码时)。

2.1 栈向上增长

对于 栈缓存溢出 这个问题,还有一种观点认为,如果使用栈向上增长的方式,(当然了,使用这种栈的架构非常少),就能够解决栈缓存溢出。因为在同一个栈帧中,发生的缓存溢出不会覆盖掉要返回的指针。但是,对这种保护机制作深入的推敲,就会发现这只是一个极其幼稚的想法。因为发生在前一个栈帧中的缓存溢出还是会覆盖掉返回指针,别有用心的人还是可以利用这个 bug 。例如, 上面的例子中, foo 的返回值没有被覆盖,但是,实际上,栈缓存溢出发生在函数 memcpy() 的栈帧中。在调用 memcpy() 函数期间,上一个栈帧中的缓冲区会发生溢出,从而覆盖掉 memcpy() 的返回指针。所以说,这种方式顶多就是改变了栈缓存溢出发生的细节,但是不会显著地减少可被利用的漏洞数量。

3 保护机制

多年来,已经开发了多种完整的方案来抑制恶意代码利用栈缓冲区。通常会被分为3类:

  • 检测栈缓存溢出是否发生,然后阻止返回的指令指针向恶意代码的重定向。
  • 禁止执行来自没有直接检测栈缓存溢出的栈中的代码。
  • 随机化分配内存地址空间,这样发现可执行代码的概率降低。

3.1 栈金丝雀

栈金丝雀,因其类似于煤矿中的金丝雀而命令,用于在恶意代码之前检测栈缓冲溢出。这种方法的原理就是,把一个小的整数值(在程序启动时随机选择)放入内存中,其位置恰好位于栈返回指针之前。大多数缓存溢出都是从低地址到高地址覆盖内存,所以,只要覆盖了栈的返回指针,那么这个“金丝雀”值也会被覆盖。在程序使用栈返回的指针之前,先检查这个值是否发生改变。这样,攻击者为了获得返回指令指针,被迫使用一些非传统的方法,诸如破坏栈上的其它重要变量,从而,大大增加了利用栈缓存溢出的难度。

3.2 不可执行的栈

另一种保护栈缓存溢出漏洞的方法就是,对栈内存强加一种不允许从栈上执行的内存策略(W^X,“写XOR执行”)。这意味着,攻击者为了执行栈上的 溢出代码,既要找到禁用内存中的执行保护机制的方法,又要找到方法去把它的 溢出代码写入到非保护的内存区上。由于大多数的桌面系统处理器在硬件上都支持非执行功能,所以这种方法变得非常流行。

尽管这种方法可以使利用栈缓存溢出的攻击失败,但是也不是没有其它问题。首先,把 溢出代码 存储在未保护的内存区域上,比如 heap,这样的方法很容易找到,这导致使用这种漏洞攻击的方法无需太大变化就可以使用。

即便不是这样,也还有其它方法。最致命的方法就是所谓的 return-to-libc。在这种攻击中,恶意代码不是通过 溢出代码载入正在调用的栈,而是通过调用恰当的栈,以便把执行引导到一系列标准库调用上,而这通常具有禁用内存执行保护和允许 溢出代码 如普通代码一样运行的效果。这能起作用,是因为执行实际上没有引导到栈本身上。

return-to-libc 的一种变体就是 面向返回编程(ROP)R,它设置一系列返回地址,每个返回地址都在现有代码或系统库运行一小段精心挑选的机器指令,然后都以 return结束。这些所谓的小段代码(gadgets)在返回之前,各自都完成一些简单的寄存器操作或类似的执行,将它们串在一起,实现攻击者的目的。甚至可以利用与返回指令类似的指令或指令组来使用 returnless的面向返回编程。

3.3 随机化内存地址

代替代码和数据分离,另外一种防止漏洞的技术就是将 随机化 引入到执行程序的内存地址空间中。因为攻击者需要确定要使用的执行代码的位置,所以既可以提供一个可执行的有效代码(具有可执行权限的栈),又可以使用代码重用重构一个,比如使用 return-to-libc面向return的编程。从概念上来说,随机化内存布局,将会阻止攻击者知晓任何代码的位置。但是,实现中通常不会随机化所有内容;可执行部分被加载到固定地址,因此,即使ASLR(地址空间布局随机化)结合一个不可执行栈,攻击者还是能够使用这个固定内存地址的东西。因此,所有程序都应该使用PIE(与位置无关的可执行文件)进行编译,这样,可执行部分的内存地址也是随机化的。随机化的熵因实现的不同而不同,并且足够低的熵本身就可能成为强制随机化的存储空间的问题。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值