关注了就能看到更多这么棒的文章哦~
Trojan Source: tricks (no treats) with Unicode
By Jake Edge
November 3, 2021
DeepL assisted translation
https://lwn.net/Articles/874951/
11 月 1 日披露的一个新的安全漏洞具有一些有意思的地方。它被称为 "Trojan Source (特洛伊式的源代码)" ,实际上是针对人类感知的一种攻击,尤其是会被那些用来审查源代码的工具放过。虽然这个缺陷的具体做法是全新的,但这种伎俩并不是全新的,也就是 Trojan Source 又找到了一种方法来糊弄人类开发者。
剑桥大学的 Nicholas Boucher 和 Ross Anderson 撰写的 Trojan Source 论文详细描述了该漏洞、影响以及协调对其进行披露的过程。在某种程度上来说,该漏洞利用了 Unicode code point 来进行攻击,这些 code point 是用在那些从左到右和从右到左的文字显示方式之间进行切换的,也就是会让各种工具显示代码的顺序跟编译器和解释器的处理顺序不一样。这样一来,向 review 人员展示的代码看起来非常合理,而语言解析工具执行的时候确实一段危险的代码。
Homoglyphs (同音字)
这里介绍了两种独立漏洞,每一个都有一些变种,全都算是 Trojan Source。第一个漏洞有点类似于在各种语言的软件包库中所出现的 "tynosquatting " 问题,或者说也像是那些钓鱼攻击中用来迷惑用户的欺骗性 URL(例如用 "I "替换 "l")。这个 "homoglyph" 漏洞(CVE-2021-42694)使用了视觉上相似的 Unicode code point 来定义函数或变量名称。
论文中所举的例子里定义了两个函数,一个是拉丁文的 "H",另一个是西里尔文的 "Н"。在同一个代码库中如果同时使用这两个字符就很可能有攻击嫌疑了。论文指出,一个用来进行恶意攻击的库就可能会定义两个使用这两个字符来看起来一模一样的函数,但是会执行不同的逻辑。然后攻击者可以提交一个看起来无害的改动,就改为去调用这个恶意函数了:
虽然这个例子里的程序看起来无害,但当比如说使用 import 的 library 的时候如何某个常用的函数被这样利用了的话,这种 homoglyph 攻击就可能会造成重大损失。例如名为 hashPassword 的函数被替换为一个长得很像的函数的话,该函数调用后返回值与原函数相同,但其实完全可能会通过网络泄露了 hash 之前的密码。
Bidirectional attacks
另一个漏洞则更加抓人眼球。CVE-2021-42574 利用了 bidirectional text code point,可以产生各种效果,但在 Trojan Source 源码网页上的演示中(以及论文中也一样)在效果上把一句注释扩展成完全包含了一整行内容,本来的代码看起来似乎是很合理的:
/* begin admins only */ if (isAdmin) {
通过使用 Unicode 的一些技巧把整行都注释掉了,也就相当于去除了对管理员权限的检查(这种都是很重要的判断)。它后面一段距离之后是另一行看起来像是包含了右大括号的代码,但又一次,实际上是被注释掉的。
/* end admin only */ }
有许多 Unicode code point 可以用来实现后续的 code point 顺序的调整。在这个例子中使用到了其中的四个:
RLO (U+202E): 将以下文字视为从右到左的文字
LRI (U+2066): 将下面的文本从左到右处理,不影响周围的文本
RLI (U+2067): 将下面的文字视为从右到左,不影响周围的文字
PDI (U+2069): 终止最近的一次 LRI 或 RLI
论文中展示了这些操作方式:
假如有下面这些 Unicode 字符依次输入:
RLI a b c PDI
那么显示的效果就是:
c b a
LRI 和 RLI 都相当于创建了一段 "独立区域",即若干个字符会被视为一组。PDI 就类似于换行的作用,会终止上一个独立区域。这些独立区域可以是嵌套的,这在论文中也有体现。
例如,考虑有如下 Unicode 字符序列:
RLI LRI a b c PDI LRI d e f PDI PDI
这将被显示为。
d e f a b c
可以看出,这就给那些混淆实际 code point 序列提供了很多方法。在上面的例子中,源文件中的实际 code point 顺序如下:
/*RLO } LRIif (isAdmin)PDI LRI begin admins only */
...
/* end admin only RLO { LRI*/
也就是说事实上这段文本中的大括号跟看起来的效果正相反。这是因为其中的括号等字符位于从右到左的代码部分中,显示顺序是相反的。最终的结果是,编辑器或代码审查工具显示的是 "无害的" 版本,而编译器对注释处理过程中的那些格式控制字符并不会做任何处理,它直接就将这两行都视为了注释,所以无论 isAdmin 标志被设置成什么,中间的所有代码每次都会正常运行。
请注意,在这两种情况下,最后的在注释中的那些左右方向控制的独立区域都是未正常结束的,这样一它们无法帮助攻击起作用,但是在注释中这些未正常结束的左右方向控制独立区域是编译器可以给出 warning 的。正如论文中提到的,编译器通常不允许在程序代码中任意设置格式控制 code point,但注释以及字符串常量则是可以的,而且可以说也确实应该要允许。但是,正如大家所见到的,这里会导致这类问题。论文中的另一个例子则使用了一个 Python docstring,其中放置了 "return" 一词,虽然看起来是无害的,但大家肯定可以猜到,通过对字符串重新排序,就会导致函数立刻返回。
Validation and mitigation
研究人员通过在各种不同的语言中创造出 homoglyph 和 bidirectional-abusing 攻击的几种变体,证明了这些类型的攻击的可能性。包括 C、C++、C#、JavaScript、Java、Rust、Go 和 Python。这些代码可以在 GitHub 和论文的附录中找到。他们还测试了七种不同的编辑器,以及基于网页的代码查看器(使用了三个主流操作系统),看看哪些受到了影响。在大多数情况下,homoglyph 攻击在几乎所有的软件上都显示为 "符合预期"(这个说法有点令人困惑),而 bidirectional attach 则有些环境中会有问题、有些环境中没有问题。例如,Vim 和 SublimeText 都没有完全被这个把戏 "骗过"。所有的测试组合可以在论文的最后附录中看到。
很明显,完全禁止程序源代码内的 bidirectional override code point 是 "解决" 这个问题的一种方法,但这种治疗方法可能还不如不治,尤其是对于显示字符串的场景。论文中提到的另一种防御方法则是不允许在字符串和注释中进行没有结束的 bidirectional 设置,这将确保任何重新排序都会只影响字符串或注释之中的内容,不能影响相邻的代码。对于 homoglyph 攻击,编译器应该要给出 warning 或者 error,正如 gcc、g++ 和 Rust 编译器当前的做法一样。除此之外,显示代码的程序应该要采用一些手段来使得其对人类可见,这样一来,代码 review 人员就可以直接看到这里有潜在的问题。
同时,在使用现有的工具时,或者在使用那些不对此加以防护的语言时,有必要对此进行保护。本文建议在 build pipeline 中加入扫描工具,只要发现这类未结束的 bidirectional 设置时就拒绝继续(或至少给出警告)。
Disclosure
这些问题最初是在 7 月 25 日向 19 个组织进行披露的,其拥有 99 天的封闭保护期。"我们遇到了各种反应,有些承诺会打 patch 修复或者提出 bug 悬赏,也有快速被驳回并引用法律条款的"。这些最初收到信息的人中,有几个人要求把再加一些人,也按他们说的照做了。论文中提到了所获得的赏金,看起来已经超过了 11,000 美元:
其中 11 个收到这个通知的组织有漏洞悬赏计划,会为报出来的漏洞提供报酬。其中有五家支付了赏金,平均支付金额为 2246.40 美元,最高最低相差 4475 美元。
9 月 9 日,这些漏洞被披露给计算机应急响应小组协调中心(CERT/CC),10 月 18 日就获得了两个 CVE 编号。这时,distros 邮件列表被加了进来,这个邮件列表的成员都是来自各种 Linux 发行版和其他 Unix 操作系统的代表。向发行版报告问题的最长封闭期是 14 天,因此得以在 11 月 1 日公开披露。
此外,研究人员对于是否真的发现已经有这类攻击了而感到好奇。为此,他们试着从 GitHub 收集一些数据:
我们组建了一个 regex 正则表达式,用来识别位于注释和字符串中的未结束的 Bidi[双向] override 序列,GitHub 向我们提供了从 2021 年 1 月到 10 月中旬加入 GitHub 的所有那些不是 markup 语言源代码的公开提交的代码的运行结果。
虽然他们确实发现了一些迹象表明类似的技术已经以若干种方式被使用起来了,但他们没有发现产生了攻击的例子。例如,用于 smart contracts(智能合约)的 Slither 静态分析工具在检查 bidirectional override 的时候看出来它们可以被用来交换作为参数传递的变量。另一个利用到的场景是用来混淆 JavaScript 代码。在进行 GitHub 扫描的同时,Rust 项目贡献者对 crates.io 的代码进行了扫描,这是 Rust 的公共包库,没有发现利用这些缺陷的证据。
虽然这些都是严重的问题,但它们似乎并不像一些媒体报道(或至少是头条新闻)所描述的那样是一个世界末日般的事件。当然,这是一个聪明的攻击方式,而且可能导致一些让人不愉快的结果,但利用这些漏洞的时候也不那么容易。要想广泛利用起来的话肯定会遇到使用那些 "特有类型" 编辑器或代码审查工具的人,他们会看到 warning 提醒的。另一方面,针对几个精心挑选的组件的有针对性的攻击很可能会被人们忽视,甚至也许现在仍然会被人们漏掉。
正如 David A. Wheeler 在 LWN 的长篇评论中指出的,在这个领域有相当多的 "现有技术",包括早期的各种混淆和 underhanded 代码竞赛时代。这篇论文也提到了一些其他人。还有一些众所周知的语言陷阱,可以以恶意的方式改变代码但是却很容易被忽视,例如论文中提到的试图对内核进行的后门操作,它利用了 C 语言中=和==运算符的视觉相似性。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~