当前位置:   article > 正文

C语言字符串安全,关于安全性:C中的字符串处理实践

c语言代码实践安全字符串

我正在开始一个普通C(c99)的新项目,该项目主要用于文本。 由于外部项目的限制,这段代码必须非常简单和紧凑,由一个没有外部依赖的源代码文件或除libc和类似普遍存在的系统库之外的库组成。

根据这种理解,哪些最佳实践,陷阱,技巧或其他技术可以帮助使项目的字符串处理更加健壮和安全?

首先使用C算作陷阱吗? * ouch, stop! just kidding! *

您想使用char *还是宽字符?

@anatolyg:为什么有人想要使用宽字符? UHG。

这个问题令人惊讶......基于对显然"安全"解决方案的误解,这么多错误的答案。哇,C使得安全的字符串处理非常困难。

@Konrad:唯一让它变得困难的是大量的错误信息和糟糕的例子。

@R ..:我认为康拉德所说的是"安全"字符串处理的做法在C中并不明显。当然,如果你知道怎么做,任何事都很容易。但是C是一种"足够的绳索"语言,几乎没有方向和许多不好的先例。这使得做事"正确"变得困难。

@tylerl:我完全同意。"坏先例"代码的数量可能是关于C语言的最糟糕的事情。

我想在此声明,我对这个优秀的问题表示赞赏。该问题在赏金期限结束前两天结束。我认为这是一个非常重要的问题,很难找到准确的信息。

@dotancohen讽刺的是,这个问题代表了将近2年。不过,至少封闭的问题不会被删除。

@tylerl:不要诱惑mods!

如果没有关于代码正在做什么的任何其他信息,我建议您设计所有的接口,如下所示:

size_t foobar(char *dest, size_t buf_size, /* operands here */)

语义如snprintf:

dest指向大小至少buf_size的缓冲区。

如果buf_size为零,则dest可接受null /无效指针,并且不会写入任何内容。

如果buf_size非零,则dest始终以空值终止。

每个函数foobar返回完整的非截断输出的长度;如果buf_size小于或等于返回值,则截断输出。

这样,当调用者可以容易地知道所需的目标缓冲区大小时,可以预先获得足够大的缓冲区。如果调用者不能轻易知道,它可以使用buf_size的零参数或使用"可能足够大"的缓冲区调用该函数一次,并且只有在空间不足时才重试。

您也可以创建类似于GNU asprintf函数的此类调用的包装版本,但如果您希望代码尽可能灵活,我将避免在实际的字符串函数中进行任何分配。在调用者级别处理失败的可能性总是更容易,并且许多调用者可以通过使用在程序中更早获得的本地缓冲区或缓冲区来确保失败是不可能的,以便更大操作的成功或失败是原子的(这极大地简化了错误处理)。

来自长期嵌入式开发人员的一些想法,其中大部分都阐述了您对简单性的要求而不是C特定的:

确定您需要哪些字符串处理功能,并尽可能减小该设置以最小化故障点。

按照R.的建议定义一个在所有字符串处理程序中保持一致的清晰界面。严格,小但详细的规则允许您使用模式匹配作为调试工具:您可能会怀疑任何看起来与其他代码不同的代码。

正如Bart van Ingen Schenau所说,跟踪缓冲区长度与字符串长度无关。如果您将始终使用文本,则可以安全地使用标准空字符来指示字符串结尾,但是您可以确保text + null适合缓冲区。

确保所有字符串处理程序的一致行为,特别是缺少标准函数的情况:截断,空输入,空终止,填充等。

如果您绝对需要违反任何规则,请为此目的创建单独的功能并对其进行适当命名。换句话说,给每个函数一个明确的行为。因此,您可以将str_copy_and_pad()用于始终使用空值填充其目标的函数。

尽可能使用安全的内置功能(例如每个Jonathan Leffler memmove())来完成繁重的工作。但要测试它们以确保它们正在做你认为他们正在做的事情!

尽快检查错误。未检测到的缓冲区溢出可能导致"跳弹"错误,这些错误很难找到。

为每个功能编写测试以确保其满足合同。一定要覆盖边缘情况(关闭1,空/空字符串,源/目标重叠等)这听起来很明显,但请确保您了解如何创建和检测缓冲区欠载/溢出,然后编写测试明确生成并检查这些问题。 (我的质量保证人员可能因为听到我的指示而感到厌倦"不要只是测试以确保它有效;测试以确保它不会破坏。")

以下是一些对我有用的技巧:

为内存管理例程创建包装器,在分配期间在缓冲区的任一端分配"fence bytes",并在取消分配时检查它们。您也可以在字符串处理程序中验证它们,可能是在设置了STR_DEBUG宏时。警告:您需要彻底测试您的诊断程序,以免它们产生额外的故障点。

创建一个封装缓冲区及其长度的数据结构。 (如果你使用它们,它也可以包含fence字节。)警告:你现在有一个非标准的数据结构,你的整个代码库必须管理,这可能意味着重大的重写(以及附加的故障点)。

让字符串处理程序验证其输入。如果函数禁止空指针,请明确检查它们。如果它需要一个有效的字符串(如strlen()应该)并且您知道缓冲区长度,请检查缓冲区是否包含空字符。换句话说,验证您可能对代码或数据做出的任何假设。

先写下你的测试。这将有助于您理解每个函数的契约 - 正是它对调用者的期望,以及调用者对它的期望。你会发现自己正在考虑使用它的方式,它可能会破坏的方式,以及它必须处理的边缘情况。

非常感谢你提出这个问题!我希望更多的开发人员会考虑这些问题 - 特别是在他们开始编码之前。祝您好运,并祝愿您拥有一款强大而成功的产品!

上半场和最后一段+1。他们足够好,我在下半场拒绝判断。 :-)测试空指针是我考虑的有害之一,虽然花哨的结构可以帮助你调试,但它们也使你的代码更难以使用和与其他代码集成。我会努力严格测试你的函数以满足他们的合同,然后你不需要任何进一步的运行时检查混乱。

@R。:我同意彻底的测试是关键,而"花哨的结构"往往弊大于利。但正确使用的简单方法也可以提高稳健性和可测试性,特别是如果它们是设计的一个组成部分。你能解释一下你对空指针测试的厌恶吗?我是测试我所能做的一切的粉丝,但我经常使用宏(例如#ifdef TEST)来包围可能昂贵或冗余的测试。谢谢你的想法!

我在关于测试NULL指针的优点或缺点的长篇讨论中的部分是:stackoverflow.com/questions/4390007/

感谢(非NULL)引用!那里有很多好主意。我认为tylerls的情况很不寻常,因为他从头开始,因此控制着完整的设计。所以,在这里,它不是一个"与他人玩得很好"的问题,而是关于定义一套连贯的严格规则并在各地执行它们的问题。至少在代码成熟之前,Id过于验证而不是错过传播通过开发系统的微妙错误。

查看strlcpy和strlcat,有关详细信息,请参阅original paper。

连接几乎总是一个不好的习惯用语(在安全性,性能和复杂性方面)。

两分钱:

始终使用字符串函数的"n"版本:strncpy,strncmp,(或wcsncpy,wcsncmp等)

始终使用+1惯用语分配:例如char * str [MAX_STR_SIZE + 1],然后传递MAX_STR_SIZE作为字符串函数的"n"版本的大小,并以str [MAX_STR_SIZE] =' 0'结束;确保所有字符串都已正确完成。

最后一步很重要,因为如果达到最大大小,字符串函数的"n"版本在复制后将不会附加' 0'。

-1 s / always / never / for strncpy。这个功能并没有像人们认为的那样做,而且几乎从来没用过。

@R ..:你认为它的作用究竟是什么?你能举个具体的例子吗?

幸运的是,我们可以查看库源代码,看看它实际上在做什么:git.uclibc.org/uClibc/tree/libc/string/strncpy.c。像C的所有其余部分一样,如果您明白您的字符串必须以零结束并且您遵循hillels建议,对我来说似乎很好。 +1。

strncpy不会终止null,并且会对目标缓冲区的其余部分进行空填充,这是一个巨大的浪费时间。您可以通过添加自己的空填充来解决前者,但不能添加后者。它从未打算用作"安全字符串处理"功能,但是用于处理Unix目录表和数据库文件中的固定大小字段。 snprintf(dest, n,"%s", src)是标准C中唯一正确的"安全strcpy",但它可能会慢得多。

顺便说一下,截断本身可能是一个主要的错误,并且在某些情况下可能会导致权限提升或DoS,因此抛出"安全"字符串函数会在问题中截断其输出并不是使其"安全"或"安全"。相反,您应该确保目标缓冲区的大小合适,并且只需使用strcpy(或者更好,如果您已经知道源字符串长度,则memcpy)。

请注意,strncat()在其界面中比strncpy()更令人困惑 - 这又是什么长度参数?根据您提供的内容strncpy()等,它不是您所期望的 - 因此它甚至比strncpy()更容易出错。为了复制字符串,我越来越认为有一个强烈的论据,你只需要memmove(),因为你总是提前知道所有的大小,并确保提前有足够的空间。优先使用memmove() strcpy(),strcat(),strncpy(),strncat(),memcpy()中的任何一个。

嗯,我不知道所有这些,所以我今晚学到了很多东西。谢谢(你的)信息。

@R:我同意strnxxx的API有它的问题,但我不同意这是完全避免它的理由。特别是当您建议的替代方案不是标准的并且需要重新实现字符串函数或使用非可移植扩展时(也有它的问题)。您可以对性能和截断问题做出一些有效的观点,但这些可能是可以接受的。很多情况。我会向你投票,因为你的方法(做得对)在安全性和性能方面看起来确实优越。另请注意,MS具有用于此目的的非可移植strcpy_s函数系列。

我不认为单独的陷阱是不使用strn*功能的原因。他们只是故事的一半。另一半是这些功能是为完全不同的目的而设计的,恰好有类似于所需行为的行为。这类似于使用UTF-8编码/解码函数作为非字符整数数据的可变长度编码。它可能有用,但它在行为之间滥用巧合的相似性,并且可能有你不想处理的丑陋角落案例。

当谈到时间与空间时,不要忘记从这里挑选标准位

在我早期的固件项目中,我使用查找表来计算O(1)操作效率中设置的位。

一些重要的问题是:

在C中,字符串长度和缓冲区大小之间根本没有关系。字符串总是运行到(并包括)第一个'\0'字符。作为程序员,您有责任确保在该字符串的保留缓冲区中找到该字符。

始终明确跟踪缓冲区大小。编译器会跟踪数组大小,但在您知道之前,这些信息将丢失给您。

使用堆栈上的数组

只要有可能并正确初始化它们。您不必跟踪分配,大小和初始化。

char myCopy[] = {"the interesting string" };

对于中等大小的琴弦,C99具有VLA。

因为你,它们的可用性稍差

无法初始化它们。但你仍然有

上面的前两个

好处。

char myBuffer[n];

myBuffer[0] = '\0';

VLA是一个等待发生的堆栈溢出。 (糟糕的那种SO,不是那种为你做功课的好人。:-)

@R,是的,如果有人误用了这个功能。但有适当的用途,必要的检查。毕竟,你也不会从未经检查的用户输入中执行malloc。

@Matthew:我发现的唯一很好用的VLA需要一些严肃的发明,它仍然只是一个理论上的问题类:递归问题,其中所有递归级别的总内存使用量已知O(n)但任何一个level可以使用高达C*n的空间(显然只有有限数量的级别实际上可以使用这么多)。 VLA是唯一可以在没有malloc的情况下实现的工具,假设O(n^2)不适合堆栈。但对于我见过的每一个真实世界的情况,一个中等大的恒定尺寸就像VLA一样好。

@R ..有一些VLA的情况并不太糟糕,特别是指向VLA的指针甚至可以与malloc一起使用,如果你愿意的话。通过这种方式,您可以拥有某种非常基本的动态类型。但确定这不是主题,在这里。为了在堆栈上使用它们,我坚持使用我在答案中提到的"中等大小"。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/492534
推荐阅读
相关标签
  

闽ICP备14008679号