CVE和平方概率

大约一年前,即2019年7月,我们开始收到有关OpenVz中基于RHEL7的内核的奇怪错误报告。乍看之下,错误是不同的:节点在不同的地方甚至在不同的子系统中崩溃,但是每次调查发现一个或另一个“弯曲”的对象时,它们就会崩溃。对象是不同的,有时在其中找到某种垃圾,有时到释放的内存的链接,有时对象本身被释放,但是在所有情况下,该对象的内存都是从kmalloc-192高速缓存分配的。削减-关于这个故事的详细故事。



图片



在这种情况下,通常的故障排除方法是仔细检查受影响对象的生命周期:查看如何为其分配内存,如何释放内存,如何正确地获取和释放参考计数器,并特别注意错误路径。但是,在我们的案例中,不同的对象被包裹了,检查它们的生命周期没有发现错误。



kmalloc-192高速缓存在内核中非常流行,它结合了数十个不同的对象。其中一个生命周期中的错误是此类错误的最可能原因。即使只是列出所有这些对象也是很成问题的,并且没有检查所有对象的问题。错误报告继续出现,但我们无法通过直接调查找到原因。需要提示。



从我们的角度来看,这些错误已由内存管理专家Andrey Ryabinin进行了调查,该专家在内核开发人员的狭窄圈子中广为人知,称为KASAN的开发人员,KASAN是一种捕获内存访问错误出色技术。 实际上,最适合发现我们错误原因的是KASAN。 KASAN未包含在原始RHEL7内核中,但是Andrey在OpenVz中向我们移植了必要的补丁。我们没有在内核的生产版本中包括KASAN,但是它存在于内核的调试版本中,可以积极地帮助我们的QA查找错误。















除KASAN外,调试内核还包括我们从Red Hat继承的许多其他调试功能。作为调试的结果,内核变得相当慢。 QA表示,在调试内核上进行相同的测试需要花费4倍的时间。对于我们来说,这不是根本,我们不评估那里的性能,而是寻找错误。但是,这样的速度下降对于客户来说是无法接受的,并且我们将调试内核投入生产的要求总是被拒绝。



作为KASAN的替代方法,要求客户端在受影响的节点上启用slub_debug...该技术还允许检测内存损坏。通过使用红色区域和每个对象的内存中毒,内存分配器在每次分配和释放内存时检查是否一切正常。如果出现问题,它将发出错误消息,并在可能的情况下更正检测到的损坏,并允许内核继续工作。此外,还存储了有关谁最后分配和释放对象的信息,因此,在事后检测内存损坏的情况下,可以了解该对象在“过去”中的“身份”。可以在生产内核的内核命令行中启用Slub_debug,但是这些检查还会消耗内存和CPU资源。这对于在开发和QA中进行调试很合适,但是生产客户对此并不热心。



六个月过去了,新年快到了。在使用KASAN的调试内核上进行的本地测试未解决问题,未从启用slub_debug的节点收到任何错误报告,在原材料中找不到任何内容,也未发现问题。 Andrey承担了其他任务,相反,我有一个空白,并被要求分析下一个错误报告。



在分析故障转储后,我很快发现了有问题的kmalloc-192对象:其内存中充满了某种垃圾,这些信息属于另一种对象。这与免税使用的后果非常相似,但是在仔细检查了原材料中受损物品的生命周期之后,我也没有发现任何可疑之处。



我浏览了旧的错误报告,试图在那里找到一些线索,但也无济于事。



最终,我回到了我的错误并开始查看上一个对象。它也被证明正在使用中,但是从其内容上完全无法理解-没有常量,对函数的引用或其他对象。在跟踪了对该对象的几代引用之后,我最终发现它是一个缩小的位图。该对象是释放容器内存的优化技术的一部分。该技术最初是为我们的内核开发的,后来其作者Kirill Tkhai将其投入了Linux主线。



“结果表明性能至少提高了548倍。”



数千个此类补丁补充了原始的岩石稳定RHEL7内核,使Virtuozzo内核对托管者而言尽可能方便。只要有可能,我们都会尝试将开发内容发送到主线,因为这样可以更轻松地将代码保持在良好的状态。



跟随链接之后,我发现了描述我的位图的结构。描述符认为位图大小应为240字节,无论如何这都不是正确的,因为实际上对象是从kmalloc-192高速缓存中分配的。



答对了!



事实证明,使用位图的函数访问的内存超出其上限,并且可以更改下一个对象的内容。就我而言,在对象的开头有一个refcount,当位图使它无效时,随后的放置导致该对象的突然释放。后来,为新对象重新分配了内存,旧对象的代码将其初始化视为垃圾,这不可避免地迟早会导致节点崩溃。



图片


您可以咨询代码作者,这很好!



通过查看他与Kirill的代码,我们很快发现了发现的差异的根本原因。随着容器数量的增加,位图应该增加,但是我们省略了其中一种情况,因此有时会跳过调整大小的位图。在我们的本地测试中,未发现这种情况,在Kirill发送给主线的补丁版本中,代码被重新设计,并且那里没有bug。



经过4次尝试,我和Kirill共同编写了这样一个补丁,我们在本地测试中运行了一个月,然后在2月底发布了具有固定内核的更新。我们有选择地检查了其他崩溃转储,还在附近找到了错误的位图,庆祝胜利并狡猾地注销了旧的错误。



但是,老妇人不断下落。这些错误报告的细流已缩小,但尚未完全枯竭。



通常,这是预期的。我们的客户是托管人。他们非常不喜欢重新启动节点,因为重新启动==停机时间==损失了金钱。我们也不喜欢频繁发布内核。此更新的正式发布是一项相当繁琐的过程,需要运行许多不同的测试。因此,新的稳定内核大约每季度发布一次。



为了确保将错误修复快速交付给客户端实现节点,我们使用ReadyKernel实时补丁。我认为,除了我们以外,没有其他人可以这样做。 Virtuozzo 7使用不寻常的策略来使用实时补丁。



通常,生命补丁仅仅是安全性。在我们的国家/地区,有3/4的修复程序是错误修复程序。修复了我们的客户已经发现或将来可能会偶然发现的错误。实际上,这些事情只能用您的分发工具包完成:没有用户的反馈,就不可能理解对他们来说重要的是什么而不是什么。



实时修补肯定不是万能药。通常不可能连续修补所有内容-技术不允许。也不会以这种方式添加新功能。但是,大部分错误是通过最简单的单行修补程序修复的,这对于生命周期修补非常有用。在更复杂的情况下,原始补丁必须“通过文件进行创造性地修改”,有时实时补丁修复机制存在漏洞,但是我们的生命补丁修复专家Zhenya Shatokhin非常了解他的工作。例如,最近,他出土了令人着迷的kpatch错误,出于充分的原因,通常值得编写一个单独的Opera。



随着适当的错误修复累积(通常每隔一到两周一次),Zhenya会发布另一系列的ReadyKernel实时补丁。发布后,它们会立即飞往客户端节点,并防止对我们已经知道的rake进行攻击。所有这一切都无需重新启动客户端节点。并且不必要地频繁释放内核。持续的利益。



但是,实时补丁通常无法及时到达客户端:它关闭的问题已经发生,但是节点尚未崩溃。



因此,对于我们已经解决的问题,出现新的错误报告并不奇怪。一次又一次地解析它们出现了熟悉的症状:旧内核,kmalloc-192中的垃圾,其前面的“错误”位图以及已修复的未加载或延迟加载的实时补丁。



其中一个这样的情况是OVZ-7188从FastVPS,这向我们走来,在最后二月。 “非常感谢您的错误报告。我们的哀悼。立即与已知问题非常相似。很遗憾,OpenVZ中没有实时补丁。等待稳定的内核发布,切换到Virtuozzo或使用带有错误修正的不稳定内核。”



错误报告是OpenVZ给我们的最有价值的东西之一。对它们进行研究使我们有机会在任何胖客户介入之前发现严重问题。因此,尽管存在已知问题,但我还是要求为我们填写崩溃转储。



解析它们中的第一个有点令我沮丧:找不到“弯曲的” kmalloc-192对象前面的“错误”位图。



不久之后,问题在新内核上重现。然后是另一个,另一个。



糟糕!



为何如此?不固定?我仔细检查了原材料-一切都很好,补丁到位,没有任何损失。



再次腐败?在同一个地方?



我不得不再次弄清楚。



图片

(这是什么?请参见此处



在每个新的故障转储中,调查再次偶然发现了kmalloc-192对象。通常,这样的对象看起来很正常,但是在对象的最开始,每次都找到错误的地址。跟踪对象的关系,我发现两个内部字节在地址中为空。



in all cases corrupted pointer contains nulls in 2 middle bytes: (mask 0xffffffff0000ffff)
0xffff9e2400003d80
0xffff969b00005b40
0xffff919100007000
0xffff90f30000ccc0


在列出的第一种情况下,应该是“正确的”地址0xffff9e24740a3d80,而不是“错误的”地址0xffff9e2400003d80。在其他情况下也发现了类似情况。



原来,一些无关的代码用2个字节使我们的对象无效。最有可能的情况是释放后使用,当对象释放后,将其前几个字节中的某些字段清零。我检查了最常用的对象,但没有发现可疑的东西。又一次死胡同。



快速VPS应我们的要求,我在KASAN上运行了调试内核一周,但并没有帮助,从未重现该问题。我们要求注册slub_debug,但这需要重新启动,该过程花费了很长时间。在3月至4月,节点再次崩溃了几次,但是slub_debug被关闭了,这没有给我们新的信息。



然后停了下来,问题停止了再现。四月结束,五月过去-没有新的瀑布。



等待在6月7日结束-终于在启用slub_debug的情况下出现了问题。在释放slub_debug对象时检查红色区域时,我发现超出其上限的两个零字节。换句话说,事实证明它不是免费使用的,先前的对象再次是罪魁祸首。有一个正常的结构nf_ct_ext。此结构指的是连接跟踪,即防火墙使用的网络连接的描述。



但是,目前尚不清楚为什么会这样。



我开始凝视conntrack:在其中一个容器中,有人使用ipv6敲开了开放端口1720。通过端口和协议,我找到了相应的nf_conntrack_helper。



static struct nf_conntrack_helper nf_conntrack_helper_q931[] __read_mostly = {
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .data_len               = sizeof(struct nf_ct_h323_master),
                .tuple.src.l3num        = AF_INET, <<<<<<<< IPv4
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .tuple.src.l3num        = AF_INET6, <<<<<<<< IPv6
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
};


在比较结构时,我注意到ipv6帮助程序没有定义.data_len。我进入git弄清楚它的来源,我发现了一个2012补丁。



提交1afc56794e03229fa53cfa3c5012704d226e1dec

作者:Pablo Neira Ayuso <pablo@netfilter.org>

日期:2012年6月7日星期四12:11:50 +0200



netfilter:nf_ct_helper:实现可变长度助手私有数据



此补丁使用了新的可变长度conntrack扩展。



我们不使用包含所有

帮助程序私有数据信息的并集nf_conntrack_help ,而是分配可变长度

区域来存储私有帮助程序数据。



此修补程序包括所有现有助手的修改。

它还包括几个include标头,以避免编译

警告。



该修补程序向助手添加了一个新的.data_len字段,该字段指示相应的网络连接处理程序需要多少内存。该补丁原本应该为当时可用的所有nf_conntrack_helpers定义.data_len,但是错过了我发现的结构。



结果,事实证明,通过ipv6到开放端口1720的连接启动了q931_help()函数,它写入了一个没有人为其分配内存的结构。一个简单的端口扫描使两个字节无效,正常协议消息的传输使该结构充满了更有意义的信息,但是在任何情况下,其他人的内存都磨损了,这迟早会导致节点崩溃。



Florian Westphal在2017年再次重新设计了代码并删除了.data_len,而我发现的问题却未被注意到。



尽管实际上在当前的linux内核主线中不再发现该错误,但该问题已由许多linux发行版的内核继承,包括仍然最新的RHEL7 / CentOS7,SLES 11和12,Oracle Unbreakable Enterprise Kernel 3和4,Debian 8和9以及Ubuntu 14.04和16.04 LTS。



该错误在我们的核心和原始RHEL7上的测试节点上均得到了很小的复制。显式安全性:远程管理的内存损坏。 1720 ipv6端口打开的地方-实际上是ping死亡。



6月9日,我制作了一个含模糊描述的单行修补程序,并将其发送到主线。我将详细说明发送给Red Hat Bugzilla,并将其分别写给Red Hat Security。



没有我的参与,进一步的事件发展了。

6月15日,Zhenya Shatokhin为我们的旧内核发布了ReadyKernel实时补丁。

https://readykernel.com/patch/Virtuozzo-7/readykernel-patch-131.10-108.0-1.vl7/



6月18日,我们在Virtuozzo和OpenVz中发布了一个新的稳定内核。

https://virtuozzosupport.force.com/s/article/VZA-2020-043



在6月24日,Red Hat Security为该错误分配了一个CVE ID

https://access.redhat.com/security/cve/CVE-2020-14305



问题在CVSS v3得分异常高的情况下受到了中等程度的影响,在接下来的几天中,其他

SUSE发行版响应了公共帽子的错误https://bugzilla.suse.com/show_bug.cgi?id=CVE-2020-14305

Debian https:/ /security-tracker.debian.org/tracker/CVE-2020-14305

Ubuntuhttps://people.canonical.com/~ubuntu-security/cve/2020/CVE-2020-14305.html



7月6日,KernelCare发布了针对受影响发行版的实时补丁。

https://blog.kernelcare.com/new-kernel-vulnerability-found-by-virtuozzo-live-patched-by-kernelcare



在7月9日,此问题已在稳定的Linux内核4.9.230和4.4.230中修复。

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-4.9.y&id=396ba2fc4f27ef6c44bbc0098bfddf4da76dc4c9



发行版不过还没有解决...



“对了,科斯蒂亚,”我对我的伴侣科斯蒂亚·霍兰科说,“我们的炮弹两次击中了同一个坑!上次我遇到neponyi时,我和一个超越对象的对象进入了这里,它在这里连续两次拜访了我们。告诉我,这是平方概率吗?还是不正方形?

-可能性是平方,是的。但是,您必须在这里查看-发生什么事件的概率是多少?连续两次准确遇到异常错误的事件的平方概率。它是连续的。



好吧,Kostya很聪明,他知道得更多。



All Articles