保护客户端代码安全,确保数据采集真实可靠

David Senecal

寫於

David Sénécal

July 08, 2025

David Sénécal 是 Akamai 公司的工程总监,主要领导反欺诈与滥用防治团队,并著有《The Reign of Botnets》一书。他满怀热忱地投身于互联网安全事业,是网络欺诈与爬虫程序检测领域的专家。他在 Web 性能、安全和企业网络技术方面拥有超过 25 年的工作经验,担任过支持、集成、咨询、开发、产品管理、架构和研究等领域的多个职务。

要保护客户端代码及其有效负载的安全,需要采取多层防御与多种技术协同作用的纵深防护策略。
要保护客户端代码及其有效负载的安全,需要采取多层防御与多种技术协同作用的纵深防护策略。

目录

众所周知,有效保护 Web 应用程序和网站需要使用 JavaScript 从浏览器中收集客户端数据。这通常包括设备和浏览器特征、用户偏好(指纹识别)以及反映用户与设备交互的数据,例如鼠标移动、触摸和按键(遥测)。

Web 安全从业者和供应商通过各种检测方法(从简单规则到先进的 AI 模型)来处理数据,以验证合法请求来自于人类控制的合法设备的可能性。

将不同的数据点相结合也有助于区分用户并评估他们随时间推移的活动情况。这是爬虫程序管理和欺诈检测产品用于帮助检测撞库攻击帐户接管开户滥用以及内容抓取等攻击的核心原则。

数据收集完整性

确保数据的真实性和完整性是准确评估用户与网站的交互以及标记威胁的关键。在人们知道客户端上执行的任何操作都可能被篡改和操纵时,如何能断言数据的真实性和完整性?

执行客户端 JavaScript 代码时,需要确保代码受到良好保护的原因有两个。

1. JavaScript 代码属于企业的知识产权。必须尽可能地保护它,以防止攻击者和竞争对手进行获取。

2. 数据完整性对于正确了解环境及其风险因素来说至关重要。受保护的 JavaScript 可确保数据值得信任,因为这些数据确实是通过运行脚本收集的并且未被操纵或转换。

如何保护客户端代码并确保数据真实性

与安全性的任何其他方面一样,没有单一的解决方案能够解决此问题。在本博文中,我们将介绍 Akamai 用于保护 JavaScript 代码、强制其执行并确保所收集数据的真实性的一系列方法,包括:

  • 代码混淆处理
  • 数据完整性检查
  • 虚拟机混淆处理
  • 误导性和额外代码插入
  • JavaScript 代码轮换
  • 动态字段轮换
  • JavaScript 构建管道和数据验证

如果您决定采取类似的做法来保护自己的代码,那么我们建议根据您的团队、企业和技术堆栈的需求来结合使用这些方法。

代码混淆处理

混淆处理是保护 JavaScript 代码的最常用方法之一。混淆处理可以让人更难以追踪和理解代码。

良好的开发实践建议函数和变量的命名应当尽可能地具有描述性,并且代码的结构应逻辑清晰,以便于调试和维护。虽然这是一项既节省时间又节省精力的有用实践,但整洁的代码很容易成为逆向工程的目标。

应用混淆处理后,这些良好的开发实践会被破坏,并且具有描述性名称的变量和函数会被替换为随机变量和函数。它们可能会被重新排序和编码,并且一些逻辑可能会被拆分。Web 浏览器仍然可以顺利执行该代码,并且结果也是一样的。但是,任何试图对该代码进行逆向工程的人都将面临更大的挑战。

开发人员仍然可以使用结构完善的代码进行维护和改进。在新版本准备就绪后,代码将在发布前通过混淆引擎进行处理。一些免费/开源以及商业产品(例如 Code Beautify、JScrambler 和 Digital.ai)可用于快速、轻松地对 JavaScript 代码进行混淆处理。

图 1 是进行指纹识别时常用的一个简单 JavaScript 函数的示例,该函数旨在提取各种设备特征,图中展示的是进行混淆处理前的原始代码。

  function getDeviceInfo() {
 return {
   userAgent: navigator.userAgent,
   hardwareConcurrency: navigator.hardwareConcurrency || "unknown",
   screenOrientation: screen.orientation.type,
 };
}

图 1:进行混淆处理前的原始代码

显而易见,原始状态下的代码逻辑清晰,极易理解。即使是编程知识有限的人,也能轻松领会其设计意图,并看懂其实现原理。

图 2 是经过 Code Beautify 在线工具处理后的同一个 JavaScript 函数。

  (function(_0xbf521e,_0x43c80b){var _0x4ad763=_0x3e09,_0x18fc85=_0xbf521e();while(!![]){try{var_0x40d2a7=parseInt(_0x4ad763(0xfc))/(0x18d1+-0xe6d+-0xa63)+-parseInt(_0x4ad763(0xf6))/(0x2*-0x7e4+0x171a+-0x750)+-parseInt(_0x4ad763(0xfb))/(-0x2e7*-0xb+0x6b*0x1f+-0x2cdf)*(parseInt(_0x4ad763(0xef))/(0x40f*-0x4+-0x897+0x18d7))+-parseInt(_0x4ad763(0xf3))/(0x3*-0xb5f+0x462+0x1dc*0x10)*(parseInt(_0x4ad763(0xf0))/(-0xb87*-0x1+0x18e8+-0x3*0xc23))+-parseInt(_0x4ad763(0xfa))/(0x2258+0x8f7+-0x2b48)*(-parseInt(_0x4ad763(0xee))/(0x3e9+-0xe93+0xab2))+parseInt(_0x4ad763(0xf1))/(0x1*-0x81e+0x525*-0x5+0x4*0x878)+parseInt(_0x4ad763(0xed))/(-0x59*-0x1f+0x779+-0x6f*0x2a);if(_0x40d2a7===_0x43c80b)break;else _0x18fc85['push'](_0x18fc85['shift']());}catch(_0x4460fc){_0x18fc85['push'](_0x18fc85['shift']());}}}(_0x1950,-0x1f*-0x38cb+0x17f2fa+-0x10aebf));function getDeviceInfo(){var _0x7a196=_0x3e09,_0x52340e={'VEDsL':_0x7a196(0xf8)};return{'userAgent':navigator[_0x7a196(0xf4)],'hardwareConcurrency':navigator[_0x7a196(0xf2)+_0x7a196(0xfd)]||_0x52340e[_0x7a196(0xf5)],'screenOrientation':screen[_0x7a196(0xf9)+'n'][_0x7a196(0xf7)]};}function _0x3e09(_0x56cbb3,_0x1167d0){var _0xddc250=_0x1950();return _0x3e09=function(_0x363b57,_0x27d74c){_0x363b57=_0x363b57-(-0x6d9+0x1316*0x1+-0xb50);var _0x1b2eec=_0xddc250[_0x363b57];return _0x1b2eec;},_0x3e09(_0x56cbb3,_0x1167d0);}function _0x1950(){var _0x1d7105=['ncurrency','20162890GviEyp','2488DLGTpn','4rCTHCm','65154TKsGUe','7673175smCphy','hardwareCo','670lOXWEG','userAgent','VEDsL','1749116JlgXKK','type','unknown','orientatio','12971xihUJr','2027775PnQRTc','487370FufNiT'];_0x1950=function(){return _0x1d7105;};return _0x1950();}

图 2:经过混淆处理的代码(通过 Code Beautify)

仅从代码长度这一方面来看,经过混淆处理后的代码显然更难以理解。该代码可能看起来很复杂,但存在对这些较简单的混淆技术进行逆向处理的方法,并且攻击者对此非常了解。不过,这样做至少提高了相应的门槛,可以阻止那些经验最不丰富并且最缺乏知识的攻击者。

在安全领域,成功的一半在于让攻击者精疲力尽和/或使攻击您企业的前景失去吸引力,具体取决于策划一次成功攻击所需的感知或实际努力

数据完整性检查

正如我们所看到的,代码混淆处理是一个良好的起点,但仅凭此方法并不足以阻止有动机的攻击者,因为存在能够将代码逆向还原为其原始格式的去混淆方法和工具。除了混淆处理方法之外,还可以实施额外代码和数据完整性检查函数来进一步保护所收集信息的完整性。

代码和数据完整性检查是在整个代码中各个位置添加的小函数,用于验证脚本生成的输出是否确实合法。这些检查通常会使用多个变量(包括现有核心 JavaScript 函数的输出以及特定于用户会话的唯一种子值)来生成次要输出。

图 3 是一个函数示例,该函数接受三个变量作为输入,在简单的数学公式和哈希函数中使用这些变量,并返回结果。变量 a 和 b 可能对应于两个核心函数的输出,而变量 c 可能是唯一种子值。在此示例中,所有属性都必须是数字值。

  function IntegrityCheck(a, b, c) {
   const mathResult = a + b * c;
   const stringResult = String(mathResult);
   let hash = 0;
   for (let i = 0; i < stringResult.length; i++) {
    hash = (hash * 31 + stringResult.charCodeAt(i)) >>> 0; 
 }
   return hash;
}

图 3:包含多个变量并用于确保数据完整性的代码示例

更具体来说,screen.colorDepthnavigator.hardwareConcurrency 属性都会返回数字值,可以用作图 3 简单函数中的变量 a 和 b。该函数实际上并不局限于返回数值的属性,因为在输入到完整性检查函数中之前,任何值都可以进行哈希处理并转换为整数。我们这样做只是为了举一个简单的例子。

为了确保多样性,一些完整性检查函数可能会对核心函数的输出进行哈希处理,如图 4 中的示例所示。

  import { createHash } from 'crypto';

function hashTwoVariables(a, b) {
 const concatenatedString = String(a) + String(b); 
 const hash = createHash('sha256').update(concatenatedString).digest('hex');
 return hash;
}

图 4:对输出进行哈希处理的示例

可能有几十个此类小函数,每个小函数都执行不同的操作,并使用分布在整个代码中的核心函数的不同输出来保护关键数据点。作为最终检查,您可能还会对整个有效负载进行“签名”,包括所有指纹和行为数据,以及各个完整性检查函数的结果。一种实现方法是对整个有效负载进行哈希处理并比较初始输入。如果发送方和接收方两侧的哈希值匹配,则有效负载被视为安全且未修改的。

虚拟机混淆处理

这些简单的完整性检查函数不能被公开暴露,也无法使用简单的混淆处理方法进行隐藏。而这正是更高级的虚拟机 (VM) 混淆技术的用武之地,它可以让攻击者更难以理解底层的操作以及如何生成有效的有效负载。

虚拟机混淆处理可以将代码转换为虚拟机字节码:这是机器可以解释,但对攻击者来说更难以进行逆向工程的代码

一些供应商提供虚拟机混淆处理方法,但虚拟机混淆处理并不总是支持所有类型的函数逻辑。使用虚拟机混淆处理时,需要遵循供应商的指导原则并对您的代码进行彻底的回归测试。

回归测试通常是一种良好的做法,不仅适用于虚拟机混淆处理,也值得作为安全例程的一部分来实施。但是,考虑到该方法的复杂代码输出,它在与虚拟机混淆处理结合使用时特别有用。

误导性和额外代码插入

为了让试图对代码进行逆向工程的攻击者面临更大的挑战,需要实施一层额外的措施,即向核心逻辑中添加没有实际用途的代码。此做法旨在诱导攻击者偏离正轨,并让他们产生挫败感,进而迫使他们放弃尝试。

同样地,您可以考虑改变完整性检查函数的结构,以增加去混淆和逆向工程的难度。实现此目标的一种方法是,开发多个结构上不同但功能等效并且可以产生相同输出的函数

功能相同但结构不同的函数在经过虚拟机混淆处理后将生成不同的函数编码,从而使代码的逆向工程变得更加复杂。

图 5 是三个此类函数的示例,这些略有不同的函数始终会返回相同的输出。

  function IntegrityCheck_1(a, b) {
 return a + b * 1; 
}

function IntegrityCheck_2(a, b) {
 return a + 0 + b; 
}

function IntegrityCheck_3(a, b, c) {
 return a + b + c * 0; 
}

图 5:三个实现相同输出但代码不同的示例

JavaScript 代码轮换

增加误导性代码、使用高级混淆技术以及进行完整性检查是很好的做法,但攻击者可能非常执着,并且只要有时间、精力和技术,任何静止不变的代码都可能被逆向工程破解,除非我们限制脚本的有效性。

想象一下,生成数千个功能相同但结构唯一的代码迭代,每个迭代都针对每个新的 JavaScript 代码版本配备了不同的完整性检查函数。每个迭代的使用期和有效期为 10 到 20 分钟,并且采取相应的控制措施来强制客户端定期重新加载新迭代,从而使旧迭代快速过时并失效。

此方法的目的是利用复杂性使攻击者不堪重负并在效率上超过他们,这样他们就别无选择,只能通过浏览器执行 JavaScript 并且无从知晓代码的作用

动态字段轮换

代码本身可能难以阅读和破译,但人们通常可以通过检查输出以及所收集和发送的数据来推断其用途。发送给服务器的一些信息可能显而易见,尤其是涉及到设备和浏览器特征等细节时。

但是,对于那些仅返回布尔值的函数或者返回整数的完整性检查函数,推断其意图将更加困难。

让攻击者更难以预测和理解有效负载结构的一种方法是,更改用于报告每个所收集的数据点的字段名称,以及它们在每个迭代的有效负载中的相对位置

正如我们所讨论的,每个 JavaScript 迭代都有一套独特的代码完整性检查。此外,有效负载将使用不同的字段名称,并且给定数据点的位置会随每个迭代发生变化。

这些字段名称及其位置是在 JavaScript 构建时根据预定义算法确定的,该算法也可以由处理数据的服务器执行,以在正确的位置检索对准确的爬虫程序和欺诈检测至关重要的各种信息。

图 6 说明了每个字段及其位置在每个迭代中是如何变化的。这些字段名称必须是非描述性的,以使其成为最不显眼的内容。

  Payload Iteration #1

  mx01: [user-agent]
  mx02: [display-mode]
  mx03: [hardconcur]
  mx04: [pixelDepth]
  mx05: [language]
  mx06: [WebGL_Rend]
  mx07: [intg_chck_1]
 
  Payload Iteration #2

  yw01: [display-mode]
  yw02: [intg_chck_1]
  yw03: [user-agent]
  yw04: [pixelDepth]
  yw05: [hardconcur]
  yw06: [WebGL_Rend]
  yw07: [language]
  
  Payload Iteration #3

  za01: [language]
  za02: [WebGL_Rend]
  za03: [hardconcur]
  za04: [pixelDepth]
  za05: [intg_chck_1]
  za06: [user-agent]
  za07: [display-mode]

图 6:字段名称迭代的示例

由于输出中只有七个字段(如以上示例所示),因此很容易发现从一个迭代到另一个迭代的变化,但设想一下,当收集并返回数百个数据点时,要做到这一点就很难了。

JavaScript 构建管道和数据验证

用于保护 JavaScript 代码并确保所收集数据的完整性的各种方法需要开发一种复杂的构建管道和发布流程。首先,开发人员将更新原始和格式良好的 JavaScript 文件、测试功能并运行回归测试。

接下来,开发人员将使用某种算法来生成数千个迭代版本,每个版本都是唯一的,并且有不同的:

  • 数据完整性检查函数,这些函数会根据核心 JavaScript、所使用的数学/哈希函数以及它们在整体逻辑中的相对位置来改变数据点
  • 多组误导性或未使用的代码
  • 有效负载输出字段名称
  • 有效负载输出字段顺序

在生成这些唯一组件后,JavaScript 文件的迭代将经历以下过程:

  • 通过虚拟机对数据完整性检查和其他关键函数进行混淆处理
  • 对整体代码进行混淆处理
  • 将迭代上传到 Web 服务器

在所有迭代生成并上传完毕后,必须在生产中启用新的 JavaScript 集。此更改将与运行接收数据的爬虫程序和欺诈检测引擎的服务器进行协调。它必须运行 JavaScript 构建系统中使用的算法的一部分才能:

  • 确认客户端发送的是当前 JavaScript 迭代的有效负载,而不是过时的有效负载
  • 根据生成有效负载的 JavaScript 迭代来解析有效负载的不同字段
  • 通过运行功能等效的函数来验证代码完整性检查值

对于经过了最终混淆处理的最终产品,必须在发布前的预生产阶段进行彻底的端到端测试,以确保所有组件协调一致并且能够生成预期的结果。这需要为 JavaScript 构建一个相对复杂的构建工作流。

尽管如此,当必须保护其内容以防止好奇的竞争对手和攻击者进行获取,并且其输出会影响互联网用户及其所访问网站的安全时,这便成为一项值得付出的努力。

结论

客户端 JavaScript 代码负责收集设备指纹、遥测数据以及执行爬虫程序与欺诈检测自定义逻辑,所以必须对其进行严密保护。尽管业界存在多种旨在保护代码与数据的策略,但仅凭一两种单一的手段,在面对最老练的攻击者时,所能提供的保护将收效甚微。

要真正保护客户端代码及其有效负载的安全,需要采取多层防御与多种技术协同作用的纵深防护策略,其中包括:代码混淆、植入误导性或无用代码、代码完整性校验函数与虚拟机混淆技术的结合、随机化有效负载的结构以降低其可预测性,以及定期更新代码。

图 7 中的方程总结了为确保提供有效保护而需要制定的整体策略组合的复杂性。

  [JS Code obfuscation[
  + Misleading code 
  + unused code
  + VM Obfuscation [code integrity check] 
  + unique field names
  + field relative position shift] 
  x  [Number of unique iterations] 
  + Limited version validity (10 minutes)
  + Force JS reload]

图 7:JavaScript 保护策略方程

最后,此组合会强制客户执行 JavaScript,减少了它们篡改数据并绕过检测引擎的机会。为了减少开发工作量,强烈建议针对一些最复杂的步骤(例如虚拟机混淆处理)采用商业解决方案。但是,代码完整性检查、误导性代码段以及多个迭代等一些策略应在内部进行构建和维护,以便在攻击者开发的去混淆工具可用时能够提供保护。



David Senecal

寫於

David Sénécal

July 08, 2025

David Sénécal 是 Akamai 公司的工程总监,主要领导反欺诈与滥用防治团队,并著有《The Reign of Botnets》一书。他满怀热忱地投身于互联网安全事业,是网络欺诈与爬虫程序检测领域的专家。他在 Web 性能、安全和企业网络技术方面拥有超过 25 年的工作经验,担任过支持、集成、咨询、开发、产品管理、架构和研究等领域的多个职务。