下面的代码选自TortoiseSVN项目。代码中包含的错误被PVS-Studio诊断为:V618 以一种危险的方式调用printf函数,因为传递进去的那一行应该包含格式化说明。安全使用printf的例子:: printf("%s", str);
解释
当你打算打印或者,比如说,写一个字符串到文件中,很多程序员会这样写:
一个优秀的程序员应该时刻记得,这样组织代码是非常不安全的。事情是这样的,如果格式化说明不知怎样进入一个字符串里,将会导致无法预测的后果。
让我们回过头来看最初的例子。如果文件名是"file%s%i%s.txt",那么这个程序会崩溃,或者输出一些不知所云的东西。但这不还是问题的全部。事实上,这样的函数调用是一个真正的漏洞。在它的帮助下,我们能发动攻击。用一些刻意的字符串,就可以打印内存里的私有数据。
更多关于这个漏洞的信息可以查看这篇文章。花点时间去通读一遍,我保证,会很有趣的。你不仅能看到理论基础,还可以看到实际的例子。
正确代码
建议
像printf()这样的函数会引起很多有关安全的问题。最好一点也不要使用它们,你可以用其他的来代替啊。比如说,你会发现boost::format 或者 std::stringstream也很有用。
一般来说,草率地使用printf(), sprintf(), fprintf(),等等函数不仅会导致运行不当,而且会引发潜在的漏洞,然后别人就可以利用这个漏洞来攻击你。
bug是在GIT的源代码中发现的。代码中包含的错误被PVS-Studio诊断为:V595 在还没有验证‘tree‘指针是否为空之前就使用它。检查134,136行。
解释
无疑,这是一个糟糕的做法,因为它间接引用了空指针,而这样间接引用的结果就是未定义行为。我们都同意这背后的理论基础。
但是,当具体运用的时候,程序员们就开始争论不休了。总有人声称,这段代码能够正确运行。他们甚至以项上人头做担保——对他们来说,它也总是能运行。所以,我要给出更多的理由来证明我的观点。这就是为什么这篇文章是改变他们观点的又一尝试。
我故意选了这么一个能够引发更多讨论的例子。当tree指针被调用,类成员不仅是在使用,也在计算该成员的地址。那么,如果(tree == nullptr),就永远不会用到成员的地址,而且函数已经退出了。很多人都认为这段代码是正确的。
但并不是。你不应该这么写代码。未定义行为不一定造成程序崩溃,比如赋值给空地址,或者诸如此类的行为。只要你调用了一个等于null的指针,未定义行为可以是任何操作。这个时候再讨论这段代码会如何运行已经没有意义了,因为此时它可以做它任何想做的操作。
未定义行为的一个标志是,编译器会把"if (!tree) return;"删掉——编译器看到指针已经被调用了,而指针不是空的,那么这一行检查就会被编译器移除。这只是众多版本中的一个,而这个版本会引起程序崩溃。
我建议阅读这一篇文章:http://www.viva64.com/en/b/0306/,里面给出了更多细节。
正确代码
要注意未定义行为,即使一切看上去都没什么问题。没必要冒险。即使我已经写了,但还是很难表现出它的价值。尝试着去避免未定义行为,即使一切看上来都没问题。
有人会想,他清楚的知道,未定义行为是怎样运作的。而且,他可能会想,这意味着,他可以做一些其他人不能做的事,还能保证代码不出错。但并非如此。下一章节将会说明未定义行为真的非常危险。
这次很难给出实际应用的例子。但是,我经常有看到会导致下面要所描述问题的可疑代码。这个错误是在处理大数组的时候出现的,而我不知道哪个项目会用到那么大的数组。我们没有真的收集到64位的错误,所以今天的例子是刻意的。
让我们来看一段刻意出错的代码:
解释
如果你构建的是这个项目的32位版本,代码是可以正确运行的。但是如果我们要编译64位版本,情况就会变得很复杂。
这个64位项目开始的时候申请5GB的缓冲区,然后初始化为0.接着,用循环修改为非0值:用“|1”来保证非0.
现在来猜一下,如果在x64位版本下用Visual Studio 2015编译的时候,这段代码会如何运行?你有答案吗?如果有,那我们继续。
如果你运行的是这个项目的调试版本,它会因为下标溢出而崩溃。在一定程度上,下标会溢出,而且其值会变成?2147483648 (INT_MIN).
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)