NTPsec:安全、强化的 NTP 实现

注意: 本文最初发表于 2016 年 10 月号的 Linux Journal 杂志。

网络时间同步——将您计算机的时钟与其他人使用的协调世界时 (UTC) 对齐——既是必要的,也是一个难题。许多互联网协议都依赖于能够交换精确到小公差的 UTC 时间戳,但是您计算机中的时钟晶体会漂移(其频率随温度变化),因此需要偶尔进行调整。

这就是事情变得复杂的地方。当然,您可以让另一台计算机告诉您它认为现在几点,但是如果您不知道数据包到达您这里需要多长时间,那么这个报告就没什么用处。最重要的是,它的时钟可能坏了——或者在撒谎。

为了有所进展,您需要与几台计算机交换数据包,以便您可以将您对 UTC 的概念与他们的进行比较,估计网络延迟,对结果输入应用统计聚类分析,以获得真实 UTC 的合理近似值,然后调整您的本地时钟以适应它。一般来说,您可以获得持续的精度,在 10 毫秒左右的量级,尽管如果您处于互联网环境较差的区域,不对称路由延迟可能会使情况变得更糟。

用于执行此操作的协议称为 NTP(网络时间协议),最初的实现是由一位名叫 Dave Mills 的古怪天才在互联网时代初期编写的。传说 Dr. Mills 是让一个名叫 Vint Cerf 的孩子对 ARPANET 感兴趣的人。无论这是否属实,几十年来,Mills 都是计算机和高精度时间测量的权威人物。

但最终,Dave Mills 半退休了,然后完全退休了。他的实现(我们现在称之为 NTP Classic)留在了网络时间基金会和 Harlan Stenn 手中,Information Week 杂志在 2015 年将 Harlan Stenn 誉为 “时间之父”。不幸的是,在 NTF 的监管下,累积了一些严重的问题。到那一年,代码库已经有四分之一个世纪的历史了,并且最初构建时处于最先进水平的技术正在显现其弊端。代码变得僵化且难以修改,而实际上很少有人真正理解其核心的拜占庭时间同步算法,这使得问题更加严重。

这些问题的实际症状包括严重的安全问题。在 2015 年的同一年,信息安全研究人员开始意识到,NTP Classic 安装程序经常被用作 DDoS 放大器——攻击者通过远程控制来大量发送数据包攻击目标站点的方式。多年来一直抱怨预算不足和人手不足的 NTF 似乎无法修复这些错误。

本文旨在成为一篇技术文章,因此我将轻轻略过随之而来的政治和筹款方面的复杂性。唉,其中确实存在一些戏剧性事件。当尘埃落定之时,在 2015 年 6 月初,对 Mills 实现进行了一次非常不情愿的分叉,并命名为 NTPsec。我获得了 Linux 基金会的资助,基本上是全职担任 NTPsec 的架构师/技术主管,我们既有了一个有能力的开发团队的核心,也面临着一些严峻的挑战。

关于戏剧性事件,我要说的是,它在技术上是相关的:NTF 的主要问题之一是,尽管 NTP Classic 名义上是在开源许可下,但 NTF 保留了前开源时代的思维习惯。开发是封闭和保密的,在技术和社交上都与世隔绝,因为 NTF 决心继续使用 BitKeeper 版本控制系统。我们从 Linux 基金会获得的任务之一是解决这个问题,而我们最初的严峻挑战之一就是简单地将代码历史迁移到 git。

对于像 NTP Classic 这样庞大而古老的代码库来说,这绝非易事,而且当旧的版本控制系统是专有的,并且您无法触及代码时,问题尤其突出。我最终不得不大量修改 Andrew Tridgell 的 SourcePuller 实用程序——是的,就是触发了 Linus Torvalds 在 2005 年公开与 BitKeeper 决裂的同一段代码——来完成部分工作。剩下的工作是使用 reposurgeon 进行乏味而困难的手工修补。一年后的 2016 年 5 月——为时已晚,无法提供帮助——BitKeeper 开源了。

策略和挑战

将历史记录干净地转换为 git 花了十周时间,尽管这令人精疲力尽,但这仅仅是开始。我遇到了一个问题:我被期望强化和保护 NTP 代码,但我来的时候对时间服务知之甚少,甚至对安全工程也知之甚少。我从我领导 GPSD 的工作中获得了一些关于前者的线索,GPSD 被广泛用于时间服务。关于后者,我有一些关于如何强化代码的基础知识——因为当您深入了解时,那种安全工程是可靠性工程的一种特殊情况,而可靠性工程是我确实理解的。但我没有“对抗性思维”的经验,即优秀的信息安全人员所做的积极防御,也没有任何这方面的直觉。

当我记起 C. A. R. Hoare 的一句名言时,我找到了一条前进的道路:“构建软件设计有两种方法:一种方法是使其非常简单,以至于显然没有任何缺陷,另一种方法是使其非常复杂,以至于没有任何明显的缺陷。” 圣埃克苏佩里的也许更广为人知的格言从略有不同的角度看待这个问题,我将采用它作为 NTPsec 的座右铭:“完美不是当没有什么可以添加的时候实现,而是当没有什么可以删除的时候实现。”

用现代信息安全的语言来说,Hoare 谈论的是减少攻击面、全局复杂性以及导致可利用漏洞的意外交互的范围。这令人振奋,因为它表明也许我实际上不需要学习像信息安全专家或时间服务专家那样思考。如果我可以重构、削减和简化 NTP Classic 代码库,也许所有这些特定领域的问题都会迎刃而解。如果不是,那么至少采用我熟悉的纯软件工程方法可能会为我赢得足够的时间来学习我需要知道的特定领域知识。

我全力以赴地采用了这个策略。这推动了我对我们做出的最早的决定之一的论证,即按照完全现代的 API——纯 POSIX 和 C99 进行编码。这只是部分地为了确保可移植性;主要是我想要一个有原则的原因(我们可以给潜在用户和盟友的理由)来抛弃代码库中来自大型 UNIX 时代的陈旧代码。

而且那里确实有很多。代码中充斥着用于十几个古老的 UNIX 系统的可移植性 #ifdef 和 shim:SunOS、AT&T System V、HP-UX、UNICOS、DEC OSF/1、Dynix、AIX 以及其他更晦涩的系统——所有这些都是 API 标准化真正确立之前的遗物。NTP Classic 的人员太害怕冒犯他们的遗留客户,以至于不敢删除任何这些东西,但我知道他们显然不知道的东西。大约在 2006 年,我对 GPSD 进行了一次清除陈旧代码的传递,将其提升到非常严格的 POSIX 一致性——而且来自 GPSD 非常多样化的用户群的任何人都没有对此表示不满,也没有人告诉我他们怀念那些古老的可移植性 shim。因此,我掌握的是九年的后续 GPSD 现场经验,告诉我标准制定者已经赢得了他们的游戏,而大多数 UNIX 系统程序员实际上并没有完全理解这场胜利的所有含义。

所以我无情地清除了 NTP 代码中的陈旧代码。有时我不得不与自己的本能作斗争才能做到这一点。我也长期以来一直是这种文化的一部分,即说“哦,保留那个旧的可移植性 shim,您永远不知道,可能仍然有一台 VAX 在运行 ISC/5,而且它没有任何危害。”

但是,当您的主要关注点是降低复杂性和攻击面时,这种想法是错误的。没有哪一段单独的过时代码会花费很多,但是在像 NTP Classic 这样老旧的代码库中,对可读性和可维护性的累积负担变得巨大且令人瘫痪。您必须对此保持强硬态度;所有这些都必须消失,否则例外情况会堆积在您身上,而您永远无法实现任务目标。

我强调这一点,是因为我认为 NTP Classic 陷入困境的大部分原因不是缺乏技能,而是持续缺乏人们可能称之为外科手术勇气的东西——那种进行第一次切口的信心和决心,知道您很可能在修复真正错误的东西的过程中会弄得一团糟。在遗留基础设施代码上工作的软件系统架构师几乎和外科医生一样需要这种品质。

这同样适用于过时的功能。NTP Classic 代码库中充满了死胡同、错误的开始、失败的实验、过时时钟硬件的驱动程序以及其他可能曾经是个好主意但早已过时于其背后假设的代码——模式 7 控制消息、交错模式、Autokey、一个从未符合已发布标准且从未完成的 SNMP 守护程序以及其他六个较小的缺陷。其中一些(尤其是模式 7 处理和 Autokey)是安全缺陷的主要吸引器。

与端口 shim 一样,这些东西之所以在 NTP Classic 代码库中挥之不去,不是因为它们无法删除,而是因为 NTF 珍视对零年的兼容性,并且对删除任何功能的想法产生了过敏反应。

然后还有一些附带问题,其中最大的问题是 Classic 的构建系统。它是一个庞大、摇摇欲坠、充满错误、文档记录不佳的 autoconf 宏的堆积。当我研究 NTF 代码历史的一部分时,让我眼前一亮的一件事是,近年来,他们似乎花费了与修改代码一样多甚至更多的精力来与构建系统中的缺陷作斗争。

但是 NTP Classic 代码有一个非常好的地方:尽管存在所有这些问题,但它仍然可以工作。它发出喘息声和撞击声,并且充满了偶然的安全漏洞,但它完成了它应该完成的工作。当一切都说完并且所有问题都承认后,Dave Mills 是一位杰出的系统架构师,即使在数十年不幸的累积重量下呻吟,NTP Classic 仍然可以正常运行。

因此,我们技术策略核心对 Hoare 建议的大赌注可以分解为两个假设:1) 在陈旧代码和藤壶之下,NTP Classic 代码库从根本上来说是健全的,以及 2) 清理它而不破坏其健全性在实践中是可能的。

这两个假设都不是微不足道的。这可能是对赔率的先验正确赌注,但仍然会失败,因为可怕的 Finagle 神和他疯狂的先知 Murphy 在我们的汤里小便了。或者,在我们刮掉藤壶后留下的代码实际上可能被证明是不健全的,从根本上来说是有缺陷的。

然而,团队和项目在其声明的目标上的成功取决于这些前提。在 2015 年和 2016 年初,这始终是我脑海中挥之不去的问题。如果我错了怎么办? 如果我像那个老笑话里的醉汉一样,在他把钥匙掉在两条昏暗的街道之外时,却在路灯下寻找钥匙,因为“警察,这里我可以看得见。”

关于这个问题的最终结论尚未完全确定;在撰写本文时,NTPsec 仍处于 beta 版。但是,正如我们将要看到的,(在 2016 年 8 月)有确凿的迹象表明该项目走在正确的轨道上。

剥离、清理

在将代码历史迁移到 git 后,我们团队最早的胜利之一是抛弃了 autoconf 构建配方,并用 waf(Samba 和 RTEMS 也使用)这种新式构建引擎编写的配方取而代之。构建变得更快、更可靠。同样重要的是,这使得构建配方缩小了一个数量级,因此可以作为一个整体来理解和维护。

早期的另一个重点是清理和更新 NTP 文档。我们在大多数代码修改之前完成了这项工作,因为完成这项工作所需的研究是构建关于代码库中实际发生的事情的知识的绝佳方式。

这些举措开始了一个良性循环。由于构建配方不再是一个充满错误且不透明的混乱局面,因此可以更快、更自信地修改代码。每次清除一点陈旧代码都会降低代码库的总复杂性,使下一次清除稍微容易一些。

最初的测试非常随意。大约在 2016 年 5 月,由于最初与 NTPsec 无关的原因,我对 Raspberry Pi 产生了兴趣。然后我突然想到,它们将成为在 NTPsec 构建上运行长期稳定性测试的绝佳方式。因此,现在我家办公室桌子上的窗台上摆放着六台无头 Raspberry Pi,所有这些 Raspberry Pi 都配备了板载 GPS,都在 24/7 全天候运行 NTPsec 的稳定性测试和正确性测试——与传统的服务器机架一样好,但体积和成本都小得多!

在最初的 18 个月中,我们完成了很多工作。标题数字显示了代码库总规模的变化有多大。我们从 227KLOC 减少到 75KLOC,将总行数减少了整整三分之二。

尽管这听起来很戏剧化,但实际上低估了我们实现的攻击面减少,因为复杂性在代码库中分布不均。最糟糕的技术债务和安全漏洞往往潜伏在长期以来没有得到任何开发人员关注的过时和半过时代码中。NTP Classic 在这方面并不例外;我在我工作过的其他大型旧代码库中也看到了相同的模式。

另一个重要的措施是系统地查找并替换所有不安全的 C 函数调用,并用可以证明不会导致缓冲区溢出的等效函数替换它们。我将引用 NTPsec 的黑客指南

  • strcpy、strncpy、strcat:请改用 strlcpy 和 strlcat。

  • sprintf、vsprintf:请改用 snprintf 和 vsnprintf。

  • 在 scanf 和朋友函数中,禁止使用不带长度限制的 %s 格式。

  • strtok:请使用 strtok_r() 或将其展开到明显的循环中。

  • gets:请改用 fgets。

  • gmtime()、localtime()、asctime()、ctime():请使用可重入的 *_r 变体。

  • tmpnam():请改用 mkstemp() 或 tmpfile()。

  • dirname():Linux 版本是可重入的,但此属性不可移植。

这正式确立了我已在 GPSD 上成功使用的方法——与其事后修复缺陷和安全漏洞,不如约束您的代码,使其不可能存在整类缺陷。

经验丰富的 C 程序员可能会想“那么野指针和野索引问题呢?” 诚然,上面警告的“achtung verboten”不会阻止这些类型的溢出。这就是为什么该策略的另一个方面是系统地使用 Coverity 等静态代码分析器,它实际上非常擅长发现导致此类问题的缺陷。它不是 100% 完美,C 始终允许您搬起石头砸自己的脚,但我从之前在 GPSD 上的成功经验中知道,小心编码与自动缺陷扫描相结合可以大大减少您的错误负载。

为了帮助缺陷扫描器更好地工作,我们丰富了代码中的类型信息。这种类型信息中最大的单一变化是将 int 变量更改为 C99 布尔值,只要它们被用作布尔值。

小事情也很重要,例如修复所有编译器警告。我认为 NTP Classic 维护者没有这样做是令人震惊的马虎。这些警告背后的模式检测器之所以存在,是因为它们通常指向真正的缺陷。此外,大量的警告使得很容易忽略实际的错误,这些错误会破坏您的构建。而且您永远不想破坏您的构建,因为稍后,这将使二分测试更加困难。

这种系统的缺陷预防方法奏效的早期迹象是,我们在测试中检测到的在我们清理过程中引入的错误率极低。在最初的 14 个月中,我们平均每 90 天产生的医源性 C 错误少于一个。

如果 GPSD 在过去五年中没有发布几乎同样低的缺陷频率,我将很难相信这一点。从这两个项目中得出的一个主要教训是,应用编码和测试中的最佳实践确实有效。早在 2012 年,我就在 我为 The Architecture of Open Source, Volume 2 撰写的关于 GPSD 的文章中强调了这一点;NTPsec 表明 GPSD 不是侥幸。

我认为这是这两个项目最重要的收获之一。我们真的不必满足于历史上被认为是 C 代码中“正常”缺陷率的东西。现代工具和实践可以在将这些缺陷率驱动到接近于零的方向上走得很远。做正确的事情不再非常困难;通常缺失的是对可能性的把握和追求它的决心。

这是真正的回报。早在 2016 年初,针对 NTP Classic 发布的 CVE(安全警报)开始发布,而 NTPsec 躲过了这些警报,因为我们在知道存在漏洞之前就已经切除了它们的攻击面!这实际上变成了一件经常发生的事情,随着时间的推移,躲过的子弹的百分比不断增加。在某个地方,Hoare 和圣埃克苏佩里可能会微笑。

清理工作尚未完成。我们正在测试对用于处理 NTP 数据包的中央协议机器进行重大重构和简化。我们认为这已经揭示了大量潜在的安全缺陷,以前从未有人对此有所了解。这些缺陷中的每一个都将是另一个可归因于我们实践和战略方向正确的躲过的子弹。

功能?什么功能?

我还没有提到新功能,因为 NTPsec 没有太多新功能;这不是我们投入精力的地方。但是,这里有一个直接来自清理工作的功能。

当最初编写 NTP 时,计算机时钟只能提供微秒级的精度。现在它们可以提供纳秒级的精度(尽管并非所有精度都是准确的)。通过更改一些内部表示形式,我们使 NTPsec 能够在步进时使用现代时钟的全部精度,这可以使使用真实硬件(例如 GPSDO 和专用无线电报时机)的精度提高 10 倍或更多。

修复此问题大约需要四行代码补丁。如果代码没有出于历史原因而使用微秒和纳秒精度的不安混合,则可能会更早地注意到这一点。就目前而言,任何低于我们正在进行的系统 API 使用更新的方法都不太可能发现问题。

我们已经开始解决一个长期存在的痛点,即 ntp.conf 文件的几乎难以理解的语法。我们已经为声明参考时钟实现了一种新的语法,这种语法比旧的语法更容易理解。我们计划进行更多的工作,以使编写 NTP 配置不再是一门黑魔法。

NTP Classic 附带的诊断工具混乱、没有文档记录且陈旧。我们有一个新工具 ntpviz,它为时间服务器操作员提供了服务器日志文件中发生的情况的图形化且信息量更大的视图。这将有助于理解和减轻各种不准确性的来源。

我们未来的方向

我们认为我们的 1.0 版本离发布不远了——事实上,考虑到正常的发布延迟,在您阅读本文时,它很可能已经发布了。我们的早期采用者队伍包括一家高频交易公司,对于该公司而言,准确的时间至关重要。该公司实际上尚未将 NTPsec 投入生产,尽管其负责时间的技术人员积极为我们的项目做出贡献,并期望在不久的将来将其用于生产。

在 1.0 版本之后,还有许多工作要做。我们正在与 IETF 密切合作,开发 Autokey 公钥身份验证的替代品,该替代品实际上可以工作。我们希望将尽可能多的 C 代码移出 ntpd 本身到 Python 中,以减少长期维护负担。核心守护程序本身有可能被分成两部分,以将 TCP/IP 部分与本地参考时钟的处理分开,从而大大降低全局复杂性。

除此之外,我们正在深入了解核心时间同步算法,并怀疑这些算法存在真正的改进可能性。对网络天气和拓扑测量敏感的更好的统计滤波看起来是可能的。

这是一次冒险,我们欢迎任何有兴趣加入的人。NTP 是至关重要的基础设施,在未来数十年内保持其健康将需要一个庞大而蓬勃发展的社区。您可以在我们的项目网站上了解更多关于如何参与的信息。

Eric S. Raymond 是一位游走的人类学家和惹麻烦的哲学家。他也以编写少量代码而闻名。实际上,如果“ESR”标签对您来说毫无意义,那么您在这里读这本杂志做什么?

加载 Disqus 评论