【问题标题】:Minimum length of string that can crash some application可能使某些应用程序崩溃的最小字符串长度
【发布时间】:2019-08-25 11:29:41
【问题描述】:

我们每周对计算机系统漏洞进行一次测试,其中有以下问题:

以下函数是在 32 位 x86 系统上运行的程序的一部分;编译器不会改变栈上变量的顺序。

void function(char *input) {
    int i = 1;
    char buffer[8];
    int j = 2;
    strcpy(buffer,input);
    printf("%x %x %s\n",i,j,buffer);

}

通过输入参数传递给函数的字符串的最小长度可以使应用程序崩溃?

a)10 b)11 c)12 d)13

我写了一个main 函数来调用void function(... 并使用gcc -m32 test.c -o test 编译程序,因为我在64 位计算机上。下面是主要功能:

int main(int argc, char *argv[]) {
    function(argv[1]);
    return 1;
}

并使用输入进行测试:

~/Dir:./test 1234567
1 2 1234567
~/Dir:./test 12345678
1 2 12345678
~/Dir:./test 123456789
1 2 123456789
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)

只要我输入123456789 作为参数,就会检测到堆栈粉碎,所以这个问题的答案应该是9,但是没有选项可以选择9。上述问题的正确答案应该是什么?我如何知道可以使上述应用程序崩溃的字符串的最小长度?

【问题讨论】:

  • 公平地说,我已经预料到 8 个字符会出现一些奇怪的行为。
  • 它的开发人员有责任确保buffer 有足够的内存空间来存储input,否则会导致缓冲区溢出,进而导致未定义的行为。使用一种模式,例如它只允许将7 字符复制到input
  • 我的意思是,在 8 个字符处,您已经在调用未定义的行为,因此问题和那些可能的答案一开始就没有多大意义。无论如何,您是否尝试过反编译您的可执行文件并检查它如何处理这些参数?另外,我会在编译器调用中添加-O0 以消除任何类型的优化。
  • "...可以使上述应用程序崩溃的最小字符串长度?" >7.它是否崩溃取决于月球。
  • 缓冲区为 8 个字节。在 C 中,字符串需要 1 个额外的字节来存储 0-终止符。所以存储在 8 个字节中的最大字符串长度是 8 个字节减去 1 个字节 = 7 个字节。额外的字节将被写入buffer 的边界之外。在 C 中写出数组的边界会调用臭名昭著的未定义行为,从那时起任何事情都可能发生。

标签: c security


【解决方案1】:

您会收到包含 9 个字符的“检测到堆栈粉碎”,因为您的编译器确实对堆栈上的变量进行了重新排序。 GCC 确实如此,即使在 -O0。为了防止这种情况,请将变量放在结构中。

#include <stdio.h>
#include <string.h>
struct variables {
    int i;
    char buffer[8];
    int j;
};
void function(char *input) {
    struct variables s;
    s.i = 1;
    s.j = 2;
    strcpy(s.buffer, input);
    printf("%x %x %s\n", s.i, s.j, s.buffer);
}
int main(int argc, char *argv[]) {
    function(argv[1]);
    return 0;
}

在关闭优化的情况下编译它,否则编译器很可能会优化 s 本身。

$ ./a.out 1234567                    
1 2 1234567
$ ./a.out 12345678
1 0 12345678
$ ./a.out 123456789
1 39 123456789
$ ./a.out 1234567890
1 3039 1234567890
$ ./a.out 1234567890a
1 613039 1234567890a
$ ./a.out 1234567890ab
1 62613039 1234567890ab
$ ./a.out 1234567890abc
1 62613039 1234567890abc
*** stack smashing detected ***: <unknown> terminated
[2]    6086 abort (core dumped)  ./a.out 1234567890abc

现在您可以看到发生了什么。最多有 7 个字符,加上空终止符,该字符串适合 8 字节缓冲区。有 8 个字符时,字符串开始溢出到内存中的下一个内容,即j。在 32 位 little-endian 机器上,组成 j 的字节的值是 {0x02, 0x00, 0x00, 0x00}。对于 8 到 11 个字符,字符串逐渐接管 j

在 12 个字符处,空终止符会覆盖内存中 s 之后的任何内容。在我的测试中,内存中的这个字节的值恰好是 0,所以没有比j 的覆盖更糟糕的事情发生了。在 13 个字符处,字符串 c 的最后一个字符会覆盖该字节,堆栈保护会检测到该字节,因为该字节实际上是堆栈金丝雀的一部分。

在我的构建中,导致崩溃所需的字符数是 13。但是,这是因为在 j 之后恰好有一个空字节。根据练习的假设,可能使应用程序崩溃所需的字符数为 12。此时,strcpy 调用会写入函数的本地存储空间,这可能是未映射的地址。

为了视觉参考,这是strcpy调用之前的内存内容:

+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| 01 | 00 | 00 | 00 | ?? | ?? | ?? | ?? | ?? | ?? | ?? | ?? | 02 | 00 | 00 | 00 | 00 | ?? |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
 ^-i                 ^-buffer                                ^-j                 ^-stack canary

如果我使用gcc -O0 -fno-stack-protector 编译,实际上需要 21 个字节才能在我的平台上导致崩溃,大概是因为它需要覆盖返回地址。练习(我没看过也不知道它有多难):使用调试器并在汇编代码和一些 x86 ABI 文档的帮助下,找出那里有什么(帧指针?对齐间隙?)。

【讨论】:

    【解决方案2】:

    这个问题是在假设编译器没有重新排序堆栈上的变量并且它没有利用未定义的行为来执行某些优化的假设下工作的,但是您的测试程序正在这样做。它很可能将数组放在堆栈上的最高地址,以减少导致崩溃的字符。

    在问题的限制下,如果int 是 4 个字节,答案将是 12。字符 9 到 12 将被写入 int 变量之一的字节,字符串的终止空字节将被写入超过该字节的一个字节,可能会写入函数的返回地址。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-04
      • 1970-01-01
      • 2023-04-04
      • 2022-12-01
      • 2011-05-18
      • 1970-01-01
      相关资源
      最近更新 更多