新近发现的 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 )关注我们,并与我们的编辑和其他读者朋友交流。
评论