“printf("%s", argv[1]);”这行代码的内存漏洞应该描述为堆栈溢出吗?

16
今天,我在Elance.com上参加了一次短暂的"C++技能测试"。其中一个问题如下:
什么是以下代码行的安全漏洞: printf("%s", argv[1]);
选项1:格式化字符串
选项2:堆栈溢出 <-- 这被Elance标记为正确答案
用户有10秒钟时间回答这个问题,在最初几秒钟看到问题后(否则自动失败)。 (还有两个明显不相关的答案,未被Elance标记为正确答案。)
我在寻找缓冲区溢出或缓冲区溢出作为选项。
本能地,我不喜欢答案“堆栈溢出”,因为在我的10秒钟内,我精神上使用了我认为是堆栈溢出的标准定义
在软件中,当堆栈指针超过堆栈边界时,就会发生堆栈溢出。调用堆栈可能由有限的地址空间组成,通常在程序开始时确定...根据“堆栈溢出”的定义,仅当程序尝试在调用程序的总堆栈分配范围之外进行写入(无论是由于缓冲区溢出还是其他合法的写入,例如为基于堆栈的变量分配内存过多)时,才完全可能发生缓冲区溢出而不发生堆栈溢出。
我的第一反应告诉我,“缓冲区溢出”是上述有问题的代码行的更准确描述,因为通常(根据我的经验),在RAM中的垃圾数据中有足够的空字符('\0')来避免实际的堆栈溢出,在这种情况下,实现中的缓冲区溢出似乎是合理的可能或甚至是可能性较大的。但是,printf在此处读取垃圾内容的可能性可能假定argc == 1,因此不存在用户提供的argv[1];如果argv[1]存在,则可以认为调用函数未插入NULL。在问题中没有说明是否存在argv[1]。
因为我想象中可能存在缓冲区溢出问题,即使没有堆栈溢出,我选择了“格式化字符串”,因为只需传递不同的格式化字符串,例如“%.8s”,就可以大部分避免该问题,因此它似乎是一个更通用的答案,因此更好。
我的答案被标记为错误。正确答案被标记为“堆栈溢出”。
我现在意识到,也许假设 argv[1] 存在,那么唯一可能的缓冲区溢出是堆栈溢出,在这种情况下,“堆栈溢出”实际上可能是正确的答案。然而,即使在这种情况下,称之为“堆栈溢出”是否不太奇怪?假设 argv[1] 存在,难道不是“缓冲区溢出”更好地描述了这个问题吗?如果 argv[1] 不存在,那么将问题称为“堆栈溢出”,而不是更准确的“缓冲区溢出”,是否基本上是不正确的?
我想征求此网站专业人士的意见:用“堆栈溢出”来定义上述代码行的内存安全问题是否合适?或者,“缓冲区溢出”或“缓冲区越界”显然更好地描述了该问题?最后,在题目所提供的两个选项中,答案是否含糊不清,还是“堆栈溢出”(或“格式字符串”)显然更好的答案?

与Elance测试相关的旁注(与此发布的问题无关)

Elance的所有“C++技能测试”问题都与C++特定功能,如类、模板、STL中的任何内容或多态的任何方面无关。每个问题都是简单的C语言问题。

因为在Elance所谓的“C++技能测试”中有许多(至少3个)其他问题是毫无疑问错误的(例如这个问题:给定sizeof(int)==sizeof(int*)sizeof(int)==4,那么在代码int *a, *b; a=b; b++; b-a;中,b-a是多少,正确答案列为4,而实际上正确答案是1),并且由于测试中没有C ++特定问题,我已经联系了Elance,并计划与该组织认真解决其有问题的测试。但是,对于本帖讨论的问题,我不确定问题/答案是否有问题。


1
@self。绝对没有任何东西。在Elance测试中,用户只提供了我在此帖子中提供的单行代码 - 没有任何问题的上下文,只提供了一个问题以下代码行的安全漏洞是什么,提供了四个多项选择题。(这是在“C++技能测试”中进行的。) - Dan Nissenbaum
4
那些“不相关的答案”指的是什么?它们是否实际上非常相关? - Kerrek SB
3
argv[1] 可以是任何东西。可能没有参数传递给程序,在这种情况下,它指向 "空间中的某个地方"(或者可能是 NULL)。通常,打印一个未知字符数的字符串(基本上不带 nul 结尾)应该不会导致堆栈溢出,因为用于打印的 "堆栈" 不是很多 - 我相信。只有参数(指向字符串的指针)被推送到堆栈上 - 不清楚这是否会导致堆栈溢出(除非你已经用完了堆栈并且到达此处 - 这将是压垮骆驼的最后一根稻草)。 - Floris
7
如果他们那么无能,我不去那里工作反而是我的幸运。你应该能做得更好。 - Marius
2
如上所述,这既不是经典的格式化字符串漏洞(攻击者控制格式化字符串,但在这种情况下并非如此),也不是经典的基于堆栈的缓冲区溢出(在这种情况下,攻击者可以在堆栈帧的固定相对偏移处任意写入一定数量的字节)。两个答案都是错误的。实际上,我不确定正确的答案是什么,我认为从可利用性的角度来看,这并不特别有趣。这是一个潜在的空指针解引用,但可能只会导致崩溃,没有其他后果。 - Niklas B.
显示剩余11条评论
3个回答

6

这里没有潜在的堆栈溢出问题。

标准保证argc是非负数,这意味着它可以是0。如果argc是正数,则argv[0]argv[argc-1]是指向字符串的指针。

如果argc == 0,那么argv[1]不仅仅是一个空指针——它根本不存在。在这种情况下,argv[1]尝试访问一个不存在的数组元素。(argv[1]等价于*(argv+1);指针加法是允许的,但是解引用具有未定义的行为。)请注意,在这种情况下,程序名称,否则可以通过argv[0]访问,是不可用的。

如果argc==1,那么argv[1] == NULL。评估argv[1]是完全有效的,但它产生了一个空指针。使用"%s"选项将空指针传递给printf具有未定义的行为。我想你可以称之为格式字符串问题,但真正的问题是在需要非空字符串指针时使用了空指针。
如果argc >= 2,那么argv[1]保证指向一个字符串,printf("%s", argv[1])将简单地打印该字符串的字符,直到但不包括终止符'\0'(保证存在)。
在这种情况下仍然存在潜在的漏洞。引用N1570 7.21.6.1段15:

任何单个转换可以产生的字符数至少为4095个。

(N1570是C标准的草案;C++在其标准库的部分内容中参考了C标准。)
这意味着实现可能会限制printf调用产生的字符数。实际上,没有理由强制执行固定限制;printf可以简单地逐个打印字符,直到达到字符串的末尾。但原则上,如果strlen(argv[1]) > 4095,并且当前实现施加了这样的限制,则行为可能未定义。
不过,这并不是我所说的“堆栈溢出”——特别是因为C++标准没有使用“堆栈”这个词(除了对“堆栈展开”的几个简短引用)。
通过先进行检查,可以避免大多数这些问题:
if (argc >= 2) {
    printf("%s", argv[1]);
}

或者,如果你感到有些神经质:
if (argc >= 2 && argv[1] != NULL) {
    printf("%s", argv[1]);
}

6
C标准没有使用“stack”一词,但安全专家使用“stack overflow”这个词,并具有精确的含义。仅因C标准未使用此词并不意味着该概念不存在。(这是我仍然感到困惑的思维转变,而且我不是唯一一个在安全意识方面遇到困难的人,考虑到当我问https://dev59.com/j3_aa4cB1Zd3GeqP2WPg时获得所需答案的艰难程度,也许这又是我的错,因为我没有解释清楚)。 - Pascal Cuoq
1
我认为*(argv+1)中的指针加法没有未定义行为,因为它是“一个过去”。明显解引用它是未定义的行为。 - Pascal Cuoq
1
@Jeffrey:是的,所以如果 argc == 0,那么 argv[0] 肯定是 0。这对于 argv[1] 有什么影响呢? - Keith Thompson
1
@Jeffrey: 是的,已经修复了。(虽然这可能有些棘手。如果“argc == 0”,那么似乎会暗示一个零长度的数组——但C语言没有零长度的数组。) - Keith Thompson
1
@KeithThompson,C++也没有零长度数组(C风格数组)。当然,除了std::array - Shoe
显示剩余2条评论

4
在Unix系统上,argv[1]本身可能会是无效的内存访问(当argc==0时),指向格式正确的字符串(argc >= 2),或者为NULL(argc == 1)。 printf("%s", argv[1]);存在问题的地方在于使用一个指针(argv[1])而没有检查其是否有效。后面发生的任何事情都只是次要影响。问题在于在使用它之前没有验证argv[1]是否符合意图。它可能属于非常一般的CWE20:不当输入验证。将其称为缓冲区溢出或堆栈溢出是具有误导性的。

2
但是测试问题并没有对先前的测试做出任何假设,因此无法确定该printf语句是否正确/易受攻击。 - CiaPan
我猜你的意思是 argc == 1 - Dan Nissenbaum
1
@DanNissenbaum argv 是以空字符结尾的,这就是为什么 Pascal 写道在 argc == 1 的情况下,我们有 argv[1] == NULL。确实,在 argc == 0 的情况下(这是否可能?),argv[1] 是未初始化的,这可能是最棘手的情况。 - Niklas B.
@DanNissenbaum argv 总是以 NULL 结尾。在程序的 main 被调用时,属性 argv[argv]==0 总是成立的。 - Pascal Cuoq
@NiklasB。任何人都可以使用仅包含NULL的数组来调用execve。我在我链接的博客文章中推荐一个有趣的练习,即找到一个特权程序,因为它假定argc >= 1(因此argv [0]!= NULL),所以可以做一些有趣的事情。请注意,当argc == 0时,argv [1]本身就是越界访问。问题不在于内存位置未初始化,而在于它不存在! - Pascal Cuoq
显示剩余3条评论

3

C++标准响应

就语言本身而言,可能出现以下情况:

  1. argc < 2
  2. argc >= 2

在第一种情况下,printf("%s", argv[1])是完全不确定的行为。

在第二种情况下,程序是良好定义的(从argv[0]argv[argc-1]被保证是有效的空终止字符串):

§3.6.1/2 [basic.start.main]

对于两种形式,为了表述方便,第一个函数参数称为argc,第二个函数参数称为argv,其中argc应该是从程序运行环境中传递给程序的参数数目。如果argc非零,则这些参数应该通过argv[0]到argv[argc-1]作为指向以null结尾的多字节字符串(ntmbs s)(17.5.2.1.4.2)的指针来提供,并且argv[0]应该是表示用于调用程序或""的ntmbs的初始字符的指针。argc的值应为非负值。argv[argc]的值应为0。【注:建议在argv之后添加任何进一步(可选)参数。--endnote】

(我强调的)

为什么栈溢出非常不精确

在没有提供其他信息(如编译器或架构)的情况下,“堆栈溢出”的回答就是不精确的。C++标准没有试图定义“堆栈”是什么,因此“堆栈溢出”对于C++标准来说几乎毫无意义。

标准以保证内存模型的抽象机器方式进行推理。

真正发生的事情

argc < 2的情况下,没有人知道会发生什么。标准没有保证也没有指定任何内容。在argc >= 2的情况下,程序是良好定义的。


无效的格式字符串和经典的缓冲区溢出都会导致未定义行为,因此我不明白这个答案对问题有什么帮助。 - Niklas B.
如果 argc == 1,那么 argv[1] 不是未定义行为(但对其进行解引用是未定义行为)。 - Pascal Cuoq
实际上,在安全领域中,“堆栈溢出”有一个非常明确的含义。但我更喜欢使用“基于堆栈的缓冲区溢出”这个术语,以区分它与普通的编程错误,即仅仅是堆栈空间不足而导致崩溃,这通常没有安全相关的后果。尽管我同意在给定的代码片段中没有明显的缓冲区溢出问题。 - Niklas B.
@NiklasB。我同意。就测试者所知,那段代码可能在一个没有堆栈的系统上运行,并且在发生未定义行为时会简单地崩溃。 :) - Shoe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接