【问题标题】:integer overflow system variability?整数溢出系统可变性?
【发布时间】:2019-05-16 19:40:10
【问题描述】:

注意:删除了 printf 部分,因为它在另一篇文章中进行了解释。

我几年前从 K&R 第 2 版中学习了 C。我有一段时间没有使用 C,所以我决定浏览一本更现代的书。 “C How to Program, 8th Edition” 由 Deitel 和 Deitel(由 Pearson 于 2015 年出版)提供,具有 C99 和 C11。

与 K&R 不同,他负责安全工作。令我惊讶的一件事是整数溢出。他写道:

第 3.13 节安全 C 编程 • 添加整数会导致 值太大而无法存储在 int 变量中。这被称为 算术溢出并可能导致不可预测的运行时行为, 可能会使系统容易受到攻击。

他在另一个页面:

在执行之前确保 图 2.5 的第 18 行中的算术计算,它们将 不溢出。执行此操作的代码显示在 CERT 网站上 https://www.securecoding.cert.org — 只需搜索指南“INT32-C”。

如果您查看他们推荐的代码:

5.3.3.2 合规解决方案这种合规解决方案确保加法操作不会溢出,无论表示如何:

#include <limits.h>

void f(signed int si_a, signed int si_b)
{
    signed int sum;

    if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
        ((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
        /* Handle error */
    }
    else {
        sum = si_a + si_b;
    }
    /* ... */
}

我的理解是,尽管 unsigned int 行为是未定义的,但它始终是固定大小的位。在我的电脑上,它是一个 32 位的整数。所以在我的笔记本电脑上,我的 INT_MAX = 2147483647,如果我给它加 1,我得到 -2147483648。如果我继续向它添加一个,它最终会变为 0,然后回到 INT_MAX 并继续循环。我看不出有人使用我的代码如何攻击它?

要添加额外的代码,每次添加整数似乎都非常浪费,除非我确实需要注意可变性,而不仅仅是得到错误的结果。

编辑:由于下面的讨论,我将引用重新添加:

避免使用单一参数printfs。一个这样的指导方针是避免使用 带有单个字符串参数的 printf。如果需要显示字符串 以换行符结束,使用puts 函数,它显示 它的字符串参数后跟换行符。例如,在 图 2.1,第 8 行

printf( "Welcome to C!\n" ); 应该写成:puts( "Welcome to C!" ); 我们没有在前面的字符串中包含 \n 因为puts 会自动添加它。如果需要显示字符串 没有终止换行符,使用 printf 和两个 arguments — "%s" 格式控制字符串和要显示的字符串。这 %s 转换说明符用于显示字符串。例如,在 图 2.3,第 8 行

printf( "Welcome " );应该写成:

printf( "%s", "Welcome " );

虽然printf在本章中写成 其实不是不安全的,这些变化是负责任的编码 将消除某些安全漏洞的做法,因为我们 深入了解 C。

【问题讨论】:

  • 问题是未定义的行为是未定义的,有符号整数溢出并不总是环绕。编译器可以假设未定义的行为永远不会发生,因此他们可能会优化您的代码,使溢出导致可利用的行为,而不仅仅是环绕。你必须改掉对 UB 做出假设的习惯。
  • @curiousguy:为什么你认为使用volatile 会影响整数算术溢出的行为?您能否提供任何解释您的建议的权威参考资料?
  • @curiousguy:我想我理解volatile 的作用——我一点也不明白为什么你认为它会对整数溢出的行为产生任何影响。 “全部”volatile 确实是说编译器必须在源代码引用它时对 volatile 限定变量进行适当的引用,因此它不能(例如)优化读取,假设该值仍然与上次读取它的时间——这与读取值后如何评估整数表达式完全无关,这与溢出有关。
  • 如果你想保证有符号整数的环绕行为,nemequ 建议将标志传递给你的特定编译器,强制它对有符号整数使用环绕语义是唯一可靠的方法。
  • @curiousguy 请先阅读并理解this article,然后再继续这个“更小、更快地实现 int ops”的口头禅)

标签: c


【解决方案1】:

整数溢出

有符号整数的溢出行为未定义。 (这实际上是在 C11 3.4.3 中定义未定义行为时使用的示例)。系统回绕的情况并不少见,但它不是通用的,即使系统是这样处理的,编译器也可以假设不会发生溢出并进行相应的优化。

无符号整数保证在溢出时回绕(或更正式地说,该值以最大可表示值为模减少)(C11 6.2.5.9)。您仍然应该注意该值是否在可能发生这种情况的范围内,因为如果该行为不是您想要的,那么具有明确定义的行为并不是特别有用。

至于您是否真的需要在每次添加之前添加这些检查...除非它是真正关键代码(我的意思是航天飞机引导系统或关键任务的起搏器控制器级别),我的意见是这些检查在大多数情况下都是矫枉过正的。相反,请注意可能的值范围,并选择一种数据类型,以使该值永远不会在它可能溢出的范围内(如果有可能,在那些地方并且只在那些地方测试它)。为了在用户提供数字的情况下确保这一点,您可能会在用户首次输入数据时进行一些明确的检查,以验证这些值是否在合理的范围内。 (当然,如果您这样做,用户和您对于哪些范围是合理的,总是有不同的想法,但最好拒绝输入而不是接受它并返回不正确的结果。)

printf

printf("Hello, world!\n") 没有风险。 puts("Hello, world!") 可能会编译成更高效的代码,但我怀疑它会产生明显的不同;瓶颈将是实际的 I/O。

但是如果您使用printf(s),则存在风险,其中s 包含用户提供的数据。如果用户使s 包含例如“Foo %s”,那么它将尝试运行printf("Foo %s"),扫描到“%s”,尝试读取(不存在的)下一个参数,然后崩溃(或做一些其他未定义的行为)。

【讨论】:

【解决方案2】:
  1. 您无需检查每个操作,只需检查可能溢出的操作即可。一个明显的例子是基于不可信输入的任何操作,但即便如此,对参数进行完整性检查而不是一般检查所有数学操作可能更合适。

    Ray 的声明“……除非它是真正关键的代码(我的意思是航天飞机引导系统或关键任务的起搏器控制器级别),我认为这些检查在大多数情况下都会是矫枉过正……”是危险的。是的,他说得对,您不需要检查每个操作,但您应该始终检查可能溢出的操作。

    https://undeadly.org/cgi?action=article&sid=20060330071917 有一个很好的例子说明这可能会影响到你,即使你不是在编写航天飞机引导系统或起搏器控制器。

    还值得注意的是,编译器有时可以优化掉可以证明永远不会失败的检查。例如,如果您使用两个常量调用 f() 并检查结果,编译器很有可能会完全优化掉它,尤其是在更高的优化级别。

  2. CERT 建议的代码是可移植的,但通常不是最快的选择。 GCC 和 clang 有 __builtin_*_overflow 内在函数,在 Windows 上有 &lt;intsafe.h&gt;。如果有更大的类型可用(例如,如果您想检查 32 位操作的结果并且您有 64 位类型可用),使用更大的类型执行操作然后再回滚应该很快。如果你想要一些可移植的代码来做到这一点,有一个safe-math module in portable-snippets(免责声明:我的项目)你可以窃取。
  3. 静态分析工具在这里非常有用;其中一些可以检测潜在的整数溢出。我知道我在 Coverity 中看到过此类错误,并且我认为我在 scan-build 和 cppcheck 中看到过这些错误。

【讨论】:

  • Ray's statement...is dangerous. ...you don't need to check every operation, but you should always check the ones which can overflow. 我同意,并在稍后的答案中说了一些类似的话。航天飞机和心脏起搏器外壳是您应该到处检查以防万一的地方。
【解决方案3】:

1) 在某些机器上,x = MAX_INT + 1 会引发溢出异常,从而导致可能被利用的控制流更改。

2) 限制检查存在溢出风险。在没有溢出异常的机器上,x + y &gt;= x 并不总是正确的,即使 x 和 y 都是正数。因此,如果攻击者可以提供一个长度,那么像if (pointer + length &lt; end of allocated space) 这样的简单检查可以通过提供一个非常大的长度来绕过。

已编辑:删除 printf 部分答案,因为该部分问题已被删除。链接页面更详细地给出了我给出的答案,因此我的部分答案是多余的。

【讨论】:

  • 3 是有问题的,并且该术语更恰当地是 "conversion specifiers" 而不是 "insertion-specifier"。很难看出它最终会如何访问他们不应该访问的东西——除非程序员完全未能确保传递了一个 nul-terminated 字符串。关键是,不要使用printf ("%s\n", "the string"); 来简单地使用puts ("the string");,这类似于不使用printf ("%c", 'a');,其中putchar('a'); 是正确的(即使一个好的编译器会将printf 优化为putsputchar在这两种情况下)
  • 在许多实现中,printf("%s") 将读取参数列表的末尾,将那里发生的任何内容都视为指针,并从那里愉快地获取字符,直到遇到零字节。 %s 并不是由程序员提供的,它被读取为输入(网络、用户等)。请参阅上面发布的格式字符串漏洞链接。
  • 当然,printf误用 会做坏事(就像误用任何其他功能一样)。在这种情况下,编译器需要警告缺少参数。 (C11 标准还将 printf 参数类型中的不匹配定义为 Undefined Behavior C11 §7.21.6.1(p2 & p9) The fprintf function - insufficient arguments, or incorrect type.
  • 在我所说的情况下,编译器不会发出任何警告。 char *s = read_string_from_socket(); printf(s); s 的内容直到执行才知道。而且,是的,行为是未定义的;但它仍然可以被攻击者利用。
  • 就像我说的,printf误用 可以而且将会导致问题。您的示例违反了printf/fprintf"const char * restrict format, ..." 定义,请参阅C11 Standard - 7.21.6.1p1
【解决方案4】:

虽然 unsigned int 行为是未定义的,[...]

您误解了“未定义”。它确实意味着未定义。没有行为的定义。 任何事情都可能发生。您不应该期望,更不用说依赖您在答案中描述的任何特定行为。

以下是一些可能性:

  • 看起来像环绕算术一样工作
  • 看起来像饱和算术一样工作
  • 打印出意想不到的东西
  • 格式化硬盘
  • 发射导弹
  • 使计算机着火
  • 你想放在这里的任何其他东西

What is undefined behaviour?

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-21
    • 2014-01-24
    • 1970-01-01
    相关资源
    最近更新 更多