note:删除了printf部分,因为在另一篇文章中对此进行了解释。
几年前,我从K&R第二版中学到了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并继续旋转。我看不到有人可以使用我的代码来攻击它吗?
每次添加int时,要添加额外的代码似乎非常浪费,除非我确实需要注意可变性,而不仅仅是得到错误的结果。
编辑:由于下面的讨论,我将引用加回去:
避免使用单参数
printf
。这样的准则之一是避免使用 带单个字符串参数的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。
答案 0 :(得分:2)
溢出行为未定义。 (这实际上是在C11 3.4.3中定义未定义行为时使用的示例)。系统回绕并不少见,但不是通用的,即使系统采用这种方式,编译器也可以假定不会发生溢出并进行相应的优化。
保证无符号整数 会在溢出时回绕(或更正式地讲,该值将以最大可表示值的模减少)(C11 6.2.5.9)。您仍然应该知道该值是否在可能发生的范围内,因为如果行为不是您想要的那样,则具有明确定义的行为并不会特别有帮助。
关于您是否真的需要在每次添加之前添加这些检查……除非它是真正的关键代码(我的意思是航天飞机制导系统或起搏器控制器的关键任务级别),我认为是那些支票在大多数情况下都是过分杀伤力的。相反,请注意可能的值范围,并选择一种数据类型,以使该值永远不会在可能可能溢出的范围内(以及在某些情况下可能会发生溢出) ,请仅在那些地方进行测试)。为了在用户提供数字的情况下确保这一点,您可能需要在用户首次输入数据时进行一些显式检查,以验证值是否在合理范围内。 (当然,如果这样做,则总是存在用户和您对合理范围的想法不同的风险,但是拒绝输入要比接受输入并返回不正确的结果更好。)
printf("Hello, world!\n")
中没有风险。 puts("Hello, world!")
可能会编译为更有效的代码,但我怀疑这样做是否会带来明显的变化;瓶颈将是实际的I / O。
但是,如果您进行printf(s)
,其中s
包含用户提供的数据,则有 的风险。如果用户导致s
包含例如“ Foo%s”,则它将尝试运行printf("Foo %s")
,扫描到“%s”,并尝试读取(不存在)下一个参数,并崩溃(或执行其他一些未定义的行为)。
答案 1 :(得分:1)
您不需要检查所有单个操作,只需检查可能会溢出的那些操作。一个明显的例子是任何基于不受信任输入的操作,但是即使那样,对参数进行健全性检查而不是一般性地检查所有数学操作也可能更合适。
Ray的说法是:“除非是真正关键的代码(我的意思是航天飞机制导系统或起搏器控制器的关键任务级别),否则我认为这些检查在大多数情况下会过分杀伤……”是危险的。是的,他是对的,您不需要检查所有操作,但是您应该始终检查可能会溢出的操作。
https://undeadly.org/cgi?action=article&sid=20060330071917提供了一个很好的例子,说明即使没有编写航天飞机制导系统或起搏器控制器,也可能会咬到你。
还值得注意的是,编译器有时可以优化它可以证明永远不会失败的检查。例如,如果要使用两个常量调用f()
并检查结果,则编译器很有可能会完全优化它,尤其是在更高的优化级别。
__builtin_*_overflow
内在函数,在Windows上有<intsafe.h>
。如果有较大的类型可用(例如,如果您要检查32位运算的结果并且有64位类型可用),则应该很快使用较大的类型执行操作,然后回退。如果您想要一些可移植的代码来执行此操作,可以使用safe-math module in portable-snippets(免责声明:我的项目)。答案 2 :(得分:0)
1)在某些计算机上,x = MAX_INT + 1
导致引发溢出异常,从而导致控制流发生变化,可能会被利用。
2)检查极限值有溢出的危险。在没有溢出异常的计算机上,即使x和y均为正,x + y >= x
也不总是正确的。因此,如果攻击者可以提供一个长度,则可以通过提供很大的长度来绕过if (pointer + length < end of allocated space)
这样的幼稚检查。
已编辑:删除答案的printf部分,因为问题的那一部分已被删除。链接的页面详细给出了我给出的答案,因此我的答案中的一部分是多余的。
答案 3 :(得分:0)
尽管unsigned int行为未定义,[...]
您误解为“未定义”。这确实意味着未定义。行为没有定义。 什么都可能发生。您不应期望,更不用说依赖于您在答案中描述的任何特定行为。
以下是一些可能性: