编写安全的C语言和安全的C语言习惯用法

41
"普通人不想要自由,他只是想要安全。" - H. L. Menken 我正在尝试编写非常安全的C代码。以下列出了我使用的一些技术,并询问它们是否像我认为的那样安全。请不要犹豫,将我的代码/预设彻底摧毁。任何发现甚至最微小漏洞或教给我新思路的答案都将受到高度重视。
从流中读取:
根据GNU C编程教程中的getline:
getline函数将通过realloc函数自动扩大内存块,因此永远不会出现空间短缺的情况,这也是getline如此安全的原因之一。[...]请注意,无论输入有多长,getline都可以安全地处理您的输入行。
我认为,在读取流时,getline应该在所有输入下都能防止缓冲区溢出发生。
  • 我的假设正确吗?在输入和/或分配方案下,这是否会导致漏洞?例如,如果流的第一个字符是一些奇异的控制字符,比如0x08 BACKSPACE(ctl-H)。
  • 是否有任何工作可以数学证明getline是安全的?

malloc在失败时返回空指针:

如果malloc遇到错误,则malloc返回空指针。这会带来安全风险,因为人们仍然可以将指针算术应用于空(0x0)指针,因此维基百科建议

/* Allocate space for an array with ten elements of type int. */
int *ptr = (int*)malloc(10 * sizeof (int));
if (ptr == NULL) {
    /* Memory could not be allocated, the program should handle 
       the error here as appropriate. */
} 

安全的sscanf:

在使用sscanf时,我习惯于将要提取的字符串分配为输入字符串的大小,以避免可能发生的溢出。例如:

const char *inputStr = "a01234b4567c";
const char *formatStr = "a%[0-9]b%[0-9]c":
char *str1[strlen(inputStr)];
char *str2[strlen(inputStr)];

sscanf(inputStr, formatStr, str1, str2);

因为str1和str2的大小与inputStr相同,而且从inputStr中读取的字符数不超过strlen(inputStr),所以似乎不可能在给定inputStr的所有可能值的情况下导致缓冲区溢出?

  • 我说得对吗?有我没有考虑到的奇怪情况吗?
  • 有更好的编写方式吗?已经解决了这个问题的库?

一般问题:

虽然我发布了大量问题,但我并不希望任何人回答所有问题。这些问题更多是指导我寻找答案类型的线索。我真的想学习安全的C语言思维方式。

  • 还有哪些安全的C语言惯用法?
  • 我需要始终检查哪些边角情况?
  • 如何编写单元测试来执行这些规则?
  • 如何以可测试或可证明正确的方式强制执行约束条件?
  • 有哪些推荐的C语言静态/动态分析技术或工具?
  • 您遵循哪些安全的C语言实践,以及如何向自己和他人证明它们的合理性?

资源:

许多资源都是从答案中借用的。


11
或许应该将此设为社群维基,考虑到问题的广泛范围… - R. Martinho Fernandes
2
注意:不要两次调用 strlen(),应该将其存储在变量中: size_t len = strlen(inputStr); char *str1[len]; char *str2[len]; 编译器可能会自动为您完成此操作,但自己手动完成并不难,而且代码可读性更高,确保得到的结果也是你想要的。 - Chris Lutz
1
“普通人不想要自由,他们只想要安全。” Orly,有些架构在未来可能没有指针等内容,但是你仍然可以在上面做任何事情 :) - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
@Chris Lutz,对于非常大的字符串来说,这很有意义,谢谢。 - Ethan Heilman
1
小提示:每当您不想更改字符串const char *时,请使用它,甚至更重要的是,当您不被允许更改时,即在您的示例中应该这样读取:const char *inputStr =“a01234b4567c”; - quinmars
显示剩余2条评论
7个回答

7

我认为你的sscanf示例是错误的。按照那种方式使用仍然可能会溢出。

尝试这个,它指定了要读取的最大字节数:

void main(int argc, char **argv)
{
  char buf[256];
  sscanf(argv[0], "%255s", &buf);
}

请参考这篇IBM开发人员关于防止缓冲区溢出的文章。

在测试方面,我建议编写一个程序来生成随机长度的随机字符串,并将其输入到您的程序中,以确保它们被适当处理。


1
你能解释一下什么是溢出吗?我避免使用指定要读取的字符的格式字符串,即%255s,因为我声称我的sscanf习惯用法即使格式字符串错误地列出要读取的字符也是安全的。 - Ethan Heilman
1
更好的方式是使用 sscanf (argv [0],"%*s", sizeof buf-1,buf),避免根据缓冲区大小硬编码项目。 - wallyk
1
@wallyk 这适用于多个格式字符吗?例如,这样做是否可以安全地工作: sscanf(argv[0], "%* [5-9] abcdefg%* [0-2]",sizeof str1 - 1,str1,sizeof str2,str2); 星号的作用是什么?对S进行解引用? - Ethan Heilman
1
这些在问题中很好用,但在评论中不起作用。我甚至在写评论时都没有格式化工具栏。 - Ethan Heilman
2
用重音符号(与波浪线相同的键)将您的代码包装起来...这将允许您在评论中发布行内代码。 - brianreavis
3
在扫描程序的上下文中,*是一种用于抑制赋值的标记,因此sscanf (argv[0], "%*s", sizeof buf - 1, buf);不能正常工作。可以参考http://www.opengroup.org/onlinepubs/009695399/functions/scanf.html . 有没有想过如何在不重写字符串的情况下实现你想要的操作?你可以参考这里的解决方案:https://dev59.com/-nRB5IYBdhLWcg3wtJGF - Ethan Heilman

4

一个很好的开始是查看David Wheeler的杰出安全编码网站

他的免费在线书籍 "Linux和Unix安全编程HOWTO" 是一个经常更新的绝佳资源。

你还可以尝试使用他的优秀静态分析器 FlawFinder 来获取一些进一步的提示。但请记住,没有任何自动化工具能替代经验丰富的人类眼睛,正如David所形象地表达的那样...

任何静态分析工具,例如Flawfinder,都只是工具。没有任何工具能替代人类思考!简而言之,"一个笨蛋手里拿着工具仍然是个笨蛋"。认为分析工具(如flawfinder)能够替代安全培训和知识是错误的

我个人已经使用David的资源几年了,发现它们非常出色。


4
  1. 从流中读取

    getline() "将根据需要自动扩大内存块" 的事实意味着这可能会被用作拒绝服务攻击,因为很容易生成一个输入,它的长度足以耗尽进程(或更糟,系统!)可用的内存。一旦发生内存不足的情况,其他漏洞也可能出现。在低/无内存情况下的代码行为很少是好的,并且很难预测。在安全敏感的应用程序中,我认为最好对所有内容设置合理的上限。

    此外(正如您提到特殊字符所预示的那样),getline() 只给您一个缓冲区;它不对缓冲区的内容做任何保证(因为安全性完全取决于应用程序)。因此,对输入进行消毒仍然是处理和验证用户数据的重要部分。

  2. sscanf

    我倾向于使用正则表达式库,为用户数据定义非常狭窄的正则表达式,而不是使用 sscanf。这样,您可以在输入时执行大量验证。

  3. 一般评论

    • 可用模糊测试工具生成随机输入(有效和无效),可用于测试您的输入处理
    • 缓冲区管理至关重要:缓冲区溢出、下溢、内存不足
    • 竞争条件可以利用否则安全的代码
    • 二进制文件可能会被操纵以将无效值或过大的值注入到头文件中,因此文件格式代码必须非常稳健,并且不能假设二进制数据有效
    • 临时文件通常是安全问题的来源,必须仔细管理
    • 代码注入可用于使用恶意版本替换系统或运行时库调用
    • 插件提供了一个巨大的攻击向量
    • 作为一般原则,我建议具有明确定义的接口,在这些接口中,用户数据(或来自应用程序外部的任何数据)被认为是无效和敌对的,直到它被处理、消毒和验证,并且用户数据进入应用程序的唯一方式

1

截至2019年12月28日,这两个URL都已过时。 "通过示例进行不安全编程"链接最后一次被WayBack Machine跟踪是在2013年6月 - 请参见http://web.archive.org/web/20130630235940/http://community.corest.com/~gera/InsecureProgramming/。'博客'网站仍然说“新网站是http://morenops.com”,但新网站已不再可用。 WayBack Machine上次跟踪它是在2013年6月(再次) - 请参见http://web.archive.org/web/20130613023201/http://morenops.com/。 - Jonathan Leffler

1
Yannick Moy在他的博士期间开发了一种针对C语言的Hoare/Floyd最弱前置条件系统,并将其应用于CERT管理的字符串库。他发现了一些漏洞(详见他回忆录的第197页)。好消息是,现在该库在他的工作下变得更加安全了。

1
你也可以查看Les Hatton的网站这里,以及他的书Safer C,你可以在亚马逊上购买。

0
不要使用gets()进行输入,而应该使用fgets()。如果要使用fgets(),并且您的缓冲区是自动分配的(即在堆栈上),那么请使用以下习惯用法:
char buf[N];
...
if (fgets(buf, sizeof buf, fp) != NULL)

如果您决定更改buf的大小,此方法将继续工作。 我更喜欢这种形式:

#define N whatever
char buf[N];
if (fgets(buf, N, fp) != NULL)

因为第一个表单使用buf来确定第二个参数,更加清晰明了。


检查fclose()的返回值。



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