前车之鉴:苹果的 GoToFail Bug

阅读数:859 2014 年 3 月 12 日

话题:安全语言 & 开发架构文化 & 方法

新近发现的 iOS 和 OS X安全缺陷揭示出编码规范、单元测试、系统测试、代码复查方案、错误管理策略和工具部署方面的缺陷冰山。

ZDNet 的 Larry Seltzer 称这一缺陷“震撼且尴尬”,苹果同时为 iOS 6 发布补丁更是从侧面证实了其严重性。“苹果并不愿意 iOS 用户继续使用 iOS 6 系统,但尽管如此,他们还是修复了缺陷。这足以说明其严重程度。”Larry 说。

到这篇文章完成时,苹果已经为 iOS 和 OS X 发布了软件更新,用以修复该问题:一个能让攻击者拦截、获取并修改加密数据的通信漏洞。但从这个故事中,我们还是能够学到某些东西。

谷歌的 Adam Langley解释说,这一缺陷位于苹果开源发布的 SSL/TLS 协议实现——SecureTransport 框架中,缺陷的引发原因在于部分代码不可达。试看如下代码:

static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams,
                                 uint8_t *signature, UInt16 signatureLen)
{
	OSStatus        err;
	...

	if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
		goto fail;
	if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
		goto fail;
		goto fail;
	if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
		goto fail;
	...

fail:
	SSLFreeBuffer(&signedHashes);
	SSLFreeBuffer(&hashCtx);
	return err;
}

注意其中有两个连续的 goto fail 语句。由于 if 语句没有加大括号,因此执行时,第二处 goto fail 会让程序直接跳转到 fail 标签,从而避开 if 语句之后的验证。这会导致在跳转的那一刻 err 变量不含任何错误信息,方法也无法返回任何。Adam Langley 继续解释道:

签名验证将检查 ServerKeyExchange 消息中的签名。DHE 和 ECDHE 密文族将这一技术用于连接过程中的临时密钥交换。服务器发起请求:“这里是临时密钥和签名,你可以通过我的证书证实我是发起者。”如果这时临时密钥和证书链之间的关联被破坏,所有的验证都会失效。这种情况下虽然仍然可以向客户端发送正确的证书链,但握手过程可能用错误的私钥签名,甚至没有签名!我们无法证实服务器端是否持有与证书中公钥配对的私钥。

诚如 Larry Seltzer 所言,因为苹果没有给出更多细节,我们无从知道这一 bug 是如何被发现的,但这件事让他开始好奇苹果的代码复查实践。他还指出,虽然这个错误一眼就能够发现,但当你面对足有 1970 行长度的整个文件时,将其识别出来就会很难。

Twitter 上 #gotofail 主题的很多评论者公然视“使用 goto”为罪魁祸首,最著名的当属Dijkstra 论文中对 goto“有害”的定性。这点是无可争议的,荷兰代尔夫特理工大学的软件工程教授 Arie van Deursen试着通过返回错误代码这种惯用法实现一种类似 C 中的异常机制来解释 goto 的用法,这样一来,那种惯用法就成了罪魁祸首。

Van Deursen 写道,实际上,返回错误代码惯用法普遍存在于含 bug 的文件中,并且本身存在一定的问题。2005 年 van Deursen 与 Magiel Bruntink,Tom Tourwe 合著论文的主要成果之一就是,通过审查某相当规模的代码库发现“每千行代码中因为 return-error-code 惯用法而导致的缺陷密度达 2.1”。 这么高的缺陷密度的罪魁祸首是未检查调用、未正确传播的返回值以及未正确处理的错误条件导致的,落实了“这一惯用法相当容易出错”的罪名。

苹果前职员Kevin Marks在 van Deursen 的跟帖后留言指出,可以使用预处理宏使返回错误代码的方式更安全。这方面的例子包括BailOSErr以及致力于实现 C 的异常机制方面的工作就是这样用的例子。

说起错误代码的管理,Chris Leishman 在评论 van Deursen 观点时称,错误指示变量被初始化为 success,这才是两行 goto 真正引发漏洞的主要原因。如果错误指示变量的初始化改为 OSStatus err = OSUnknownError,系统执行会更安全。

另一方面,Plausible Labs的软件工程师Landon Fuller提供了 bug 所影响那部分代码的可测性分析并证实SSLVerifySignedServerKeyExchange能够进行隔离性单元测试。C. Keith Ray强调了 TDD 的一个观点:“如果你要编写一条 if 语句,那你必须事先准备一个调用这条语句的测试。你需要在条件为真和为假的情况下分别测试”。

Arie van Deursen 指出这一事件还有更多值得争议的地方。

他最先注意到含 bug 的文件“明显不是自动规范化的,其中有大量格式不一的空格、制表符和代码注释”,而“正确的缩进能立刻显示正在发生的可疑事”,并让 bug 查找更容易。除此之外,他进一步建议“将编码格式作为一项安全特性”,而“将空格作为一项安全关注点”。

Langley 在其博客中写道,他认为代码复查是防止这类问题发生的有效手段。但 Arie van Deursen 指出,先前微软有一项代码复查研究曾证实以下观点:

复查并不是像很多项目成员所想的那样,通常情况下都能找出缺陷;而找出深层、微妙或是“宏”层次的问题就更罕见了。也许依靠这种程度的代码复查来保证质量境况堪忧。

最后一点,这种情况下工具也没起到作用,如 Langley 所说,Clang 的 -Wall 参数不会发现两行 goto 和其后的死代码。据Simon Nicolussi所言,Clang 提供了能发现该问题的 -Weverything 标识,但 GCC 默认去除了这一标识。这点也为Peter Nelson——另一位特定 -Wunreachable-code 参数存在指出者所证实。Van Deursen 指出主要问题在于死代码的删除从根本上讲是一个不可判定问题,这需要完备性和误报间的权衡,也正是因为这一原因,缺省情况下才不含不可达检验。

查看英文原文:Lessons Learned from Apple's GoToFail Bug


感谢邵思华对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。