1. 项目概述从一次真实的对抗说起如果你做过Web安全或者爬虫开发尤其是和那些大型电商、航司、票务网站打过交道那你一定对Akamai国内常称阿卡迈这个名字不陌生。它就像一道横亘在自动化程序与目标数据之间的无形高墙而abckcookie和sensor_data就是这道墙上最核心的几把锁之一。最近在分析韩亚航空Asiana Airlines官网的预订流程时我再次和这套机制“正面交锋”了。他们的防护体系相当典型流程清晰逻辑严密是研究Akamaisensor_data生成逻辑一个非常好的样本。简单来说当你在浏览器中访问一个受Akamai保护的页面时页面会加载一段高度混淆的JavaScript代码。这段代码会在你的浏览器环境中默默收集上百种信息——从基础的屏幕分辨率、浏览器插件列表到更底层的Canvas指纹、WebGL渲染特征、音频上下文指纹甚至是你的鼠标移动轨迹、触摸事件、时区变化等等。所有这些信息经过复杂的算法加工最终会生成一个长长的、看似随机的字符串这就是sensor_data。这个数据会被提交到服务器服务器端Akamai的算法会对其进行验证判断这次访问是来自一个“真人”使用的真实浏览器还是一个自动化脚本。验证通过后服务器会下发一个关键的_abckcookie后续的敏感操作如查询航班、提交订单都需要携带这个cookie才能进行。所以逆向sensor_data的核心目标就是理解并复现这套信息收集和加工的算法让我们的程序也能生成出能被Akamai服务器认可的“合法”数据。这不仅仅是字符串拼接而是一场对浏览器环境和用户行为的高度模拟。本次分享我将以韩亚航空的案例为线索带你深入sensor_data的生成现场并重点还原其中几个决定性的关键数组。这些数组是算法逻辑的“骨架”理解了它们你就掌握了破解这道防线的钥匙。2. 核心逻辑拆解sensor_data的构成与生成流程要逆向先得知道它正着是怎么来的。通过对韩亚航空官网请求的抓包和JS代码的静态、动态分析我们可以梳理出sensor_data生成的典型流程。这个过程不是线性的而是一个多阶段、事件驱动的复杂状态机。2.1 数据收集阶段浏览器环境的全面“体检”sensor_data的原材料极其庞杂。Akamai的脚本通常是一个被压缩和混淆的、名字像bmak或类似变体的对象会定义大量的“收集器”collector函数。这些函数在页面加载初期和后续用户交互过程中被触发。我们可以将其分为几大类静态环境信息这部分在页面加载后很快被收集且通常不会改变。包括navigator对象属性userAgent,platform,language,hardwareConcurrency等。screen对象属性width,height,colorDepth,pixelDepth。window对象属性outerWidth,outerHeight,innerWidth,innerHeight。插件列表navigator.plugins的长度和每个插件的名称、描述。MIME类型列表navigator.mimeTypes。字体列表通过尝试渲染特定字符到Canvas或使用document.fontsAPI来检测系统已安装字体。这是非常强的指纹特征。Canvas指纹在Canvas上绘制相同的图形和文字由于系统图形库、抗锯齿算法、字体渲染的细微差别生成的图像数据toDataURL的哈希值几乎是唯一的。WebGL指纹查询WebGL渲染器的厂商、渲染器字符串、扩展名列表以及通过渲染特定场景获取的精度信息。动态行为信息这部分在用户与页面交互过程中持续收集。鼠标/指针事件mousemove,mousedown,mouseup事件的坐标、时间戳、触发元素等。轨迹的平滑度、加速度模式是重点。触摸事件针对移动端touchstart,touchmove,touchend的触点信息。键盘事件keydown,keypress,keyup事件但通常不会收集具体按键值隐私考虑而是收集事件触发本身和时间。滚动事件scroll事件记录滚动位置和速度。焦点事件focus,blur事件判断用户是否在页面内活跃操作。定时器与性能信息Date.now()的精度、performance.timing的各阶段时间戳、requestAnimationFrame的回调间隔等用于检测脚本执行环境是否“真实”。注意收集行为非常隐蔽。脚本会通过addEventListener绑定大量事件并且收集函数本身被包裹在try-catch中防止因某些API不存在而导致的脚本崩溃从而暴露自身。在逆向时你需要耐心地在混淆代码中寻找这些事件绑定的入口点。2.2 数据处理与编码阶段从原始数据到待编码数组收集到的原始数据不会直接拼接。它们首先会被规范化Normalization和量化Quantization。例如鼠标坐标可能会被转换成相对于视口的百分比时间戳会被转换成相对于页面加载时间的偏移量毫秒。然后这些处理后的值会被填充到一个大的JavaScript数组中。这个数组的结构是逆向的关键。在韩亚航空的案例中通过debugger断点和内存dump我观察到一个核心数组我们暂且称它为mainArray的构建过程。这个数组的长度是固定的比如超过150个元素每个索引位置对应一种特定的信息。例如mainArray[0]可能存储页面加载后初始化的一个随机种子。mainArray[1]到mainArray[10]可能存储navigator相关属性的哈希值或编码值。mainArray[30]可能存储Canvas指纹的某个特征值。mainArray[50]到mainArray[70]可能是一系列鼠标移动事件中相邻两点间距离和时间的比值即速度序列的编码。mainArray[100]可能是一个基于多种硬件信息如CPU核心数、内存计算出的综合标识。为什么是数组而不是对象数组结构更紧凑序列化后体积小且索引访问速度快适合大量数据的快速处理和编码。更重要的是固定的索引结构是服务器端验证算法所依赖的“协议”服务器知道index55的位置应该是什么类型的数据从而可以校验其合法性。2.3 关键数组还原与算法应用构建好的mainArray会经过一系列算法处理最终生成sensor_data字符串。这个过程通常包括数组转换将JavaScript数组转换为一个便于处理的格式比如将所有数字元素用特定分隔符如逗号或分号连接成一个长字符串。混淆与加密对这个长字符串应用自定义的混淆算法。这可能包括位移和异或操作对字符串的字符编码进行按位的位移,,或异或^操作使用一个动态生成的密钥。自定义编码表定义一个字符映射表将原始字符替换为表中的其他字符。插入噪声数据在特定位置插入一些随机但符合格式的字符增加直接分析的难度。压缩与编码最后将混淆后的结果进行Base64编码或类似的编码形成最终的sensor_data字符串。有时在Base64之前还会进行简单的压缩如将数字序列转换为更短的表示形式。逆向的突破口直接逆向整个混淆算法是极其困难的。更有效的方法是逆向算法所依赖的关键参数和数组。在韩亚航空的JS代码中我发现了几个至关重要的子数组或函数它们直接参与了mainArray的填充和转换infoCollectors一个函数数组每个函数负责收集一类信息并返回一个或多个值这些值被按顺序压入mainArray。找到这个数组就找到了数据收集的清单。encodingMap一个对象或数组定义了如何将收集到的原始值如字符串”Chrome”映射为一个简短的整数或字符代码。还原这个映射关系是正确生成数组元素的前提。positionConfig一个配置对象明确记录了每种信息在mainArray中的索引位置、数据长度和格式。这可能是硬编码在代码里的也可能是通过某个初始化函数计算出来的。我们的首要目标就是通过动态调试console.log,debugger, Hook函数返回值和静态分析解混淆、代码流分析将这些关键数组和它们的生成逻辑还原出来。3. 实战逆向定位并还原关键数组理论说再多不如动手跟一遍。下面我以韩亚航空官网加载的某个JS文件假设其核心对象为window._0x10ab3为例分享具体的逆向步骤和技巧。3.1 环境准备与代码定位首先你需要一个能拦截和修改请求/响应的环境。我推荐使用任何现代浏览器的开发者工具配合一个本地代理工具如mitmproxy、Charles或Fiddler来持久化地修改JS文件。对于JS动态分析Chrome DevTools的Sources面板和Console面板是你的主战场。清除缓存打开无痕窗口确保加载的是最新的代码。访问目标页面如韩亚航空航班搜索页打开DevTools (F12)切换到Network网络面板勾选Preserve log保留日志。过滤JS请求在大量的请求中寻找那些名称可疑、可能包含防护逻辑的JS文件。通常它们会有诸如/akamai/,/bootstrap,/captcha, 或包含bmak,sensor,collector等字眼的路径。文件体积可能较大几百KB且内容高度混淆打开Preview一看全是_0x1a2b3c这样的变量名。找到入口在Console中尝试输入window然后查看全局对象寻找可疑的对象名。或者在Sources面板的Page标签下在所有加载的JS文件中搜索sensor_data,_abck,akamai等关键词定位到核心代码所在的文件。3.2 动态Hook与关键函数拦截找到核心JS文件后不要尝试直接阅读混淆后的代码。我们的策略是“动态跟踪”。设置XHR/Fetch断点在DevTools的Sources面板右侧XHR/Fetch Breakpoints里添加一个包含sensor或特定提交URL的断点。当浏览器发起提交sensor_data的请求时代码执行会自动暂停。调用栈分析断点触发后查看Call Stack调用栈。你会看到一长串函数调用从底层的网络API一直向上最终会指向一个属于混淆代码的函数。这个函数很可能就是负责组装最终请求数据包含sensor_data的函数。点击调用栈中离顶部最近的那个混淆函数进入其源码上下文。Hook关键对象方法在Console中我们可以直接覆盖或Hook可疑对象的方法。假设我们通过调用栈和全局变量查找确定了核心对象是window._0x10ab3并且它有一个getSensorData方法。// 保存原函数引用 var originalGetSensorData window._0x10ab3.getSensorData; // 覆盖它加入我们的日志 window._0x10ab3.getSensorData function() { console.log([Hook] getSensorData called!); // 在函数内部this指向核心对象我们可以查看其内部状态 console.log([Hook] Current mainArray state:, this.mainArray); console.log([Hook] Current mouse event queue:, this.mouseEvents); // 调用原函数获取结果 var result originalGetSensorData.apply(this, arguments); console.log([Hook] Generated sensor_data:, result); return result; };通过这种方式我们可以在sensor_data生成的关键时刻窥探核心对象内部的状态尤其是mainArray的内容。3.3 还原mainArray的结构与positionConfig通过多次Hook并在不同时间点页面加载后、鼠标移动后、提交前打印this.mainArray我们可以观察数组内容的变化。你会发现数组的某些位置从一开始就是固定的值可能是版本号或标识有些位置随着时间推移被填充如鼠标事件有些位置则在提交前瞬间被计算出来。记录与比对准备一个表格记录每次Hook时mainArray的索引和值。进行多次“干净”的浏览器访问模拟真实用户记录下数组内容。你会发现虽然每次访问的某些值如随机种子、时间戳不同但相同索引位置的数据类型和语义是固定的。例如索引范围推测内容观察到的值示例变化规律0-2版本/标识/随机种子[1, 4056, 123456]每次页面加载固定不同会话变化3-10Navigator属性哈希[102, 0, 5, ...]浏览器环境不变则不变30-35Canvas指纹相关[234, 189, ...]浏览器/硬件不变则不变50-70鼠标移动向量[12, -5, 8, ...]随用户操作实时变化初始为0100-105性能计时数据[87, 120, ...]每次页面加载略有波动这个表格就是你还原的positionConfig雏形。接下来你需要去代码里寻找填充这些位置的逻辑。在混淆代码中搜索对mainArray的赋值操作例如this.mainArray[50] ...。通过断点调试这些赋值语句你可以看到右边的表达式是什么从而理解这个位置的数据是如何计算出来的。3.4 逆向encodingMap与infoCollectors当你定位到类似this.mainArray[5] this._encodeNavigator(appCodeName);这样的代码时就需要深入_encodeNavigator这个函数了。这个函数很可能内部引用了一个encodingMap。静态分析辅助将核心JS文件保存到本地使用一些JS反混淆工具如jsnice.org、prepack.io或自己写的正则脚本进行初步美化让变量名和函数结构稍微可读一些。虽然不能完全还原但有助于识别字符串常量、数组定义和简单的控制流。查找编码映射在美化后的代码中搜索{或[寻找看起来像是映射关系的结构。例如var _0x1234 { Chrome: 1, Firefox: 2, Safari: 3, Edge: 4 };或者是一个二维数组通过索引来映射。动态推导如果静态分析困难就用动态方法。Hook可疑的编码函数记录其输入和输出。// 假设怀疑 _0xabcd 是编码函数 var originalEncoder _0xabcd; _0xabcd function(input) { var output originalEncoder(input); console.log([Encode] Input: ${input} - Output: ${output}); return output; };通过收集大量的输入输出对你甚至可以反向推导出整个映射关系。infoCollectors的定位也类似。在代码中搜索push到某个数组的操作或者遍历一个函数数组并执行的循环。找到这个收集器数组你就掌握了数据收集的完整流程顺序。4. 算法模拟与数据生成在还原了关键数组的结构和编码映射后下一步就是在浏览器环境外例如Node.js/Python脚本模拟这一过程。4.1 环境信息模拟这是最具挑战性的部分。你需要为你的脚本生成一套自洽的、看起来像真实浏览器的环境指纹。基础属性userAgent,platform,language等可以随意构造但需要保持一致。建议从真实的浏览器请求中捕获一套然后固定使用。Canvas/WebGL指纹这是重灾区。真实的Canvas指纹依赖于系统的图形库。在Node.js中你可以使用node-canvas库但它的渲染结果和真实浏览器可能不同。一种策略是直接复用在第一次通过真实浏览器成功获取_abckcookie时将当时生成的sensor_data中的Canvas相关部分通过你还原的positionConfig知道是哪几个索引记录下来以后在模拟时直接使用这些固定的值。前提是服务器端对这些指纹的校验不是每次都必须变化。字体列表同样可以从一次成功的会话中捕获编码后的字体哈希值并固定使用。性能数据performance.timing的各阶段时间差需要模拟一个合理的范围。Date.now()的精度要模拟。实操心得不要追求100%的完美模拟尤其是对于硬件相关的强指纹。Akamai的验证通常是一个综合评分机制。你的sensor_data不需要在所有维度都得满分只要总分超过某个阈值即可。因此优先保证行为数据鼠标移动、时间间隔的逼真度往往比死磕一个完美的Canvas指纹更有效。行为数据是动态的、每次请求都变化的也是Akamai用来检测自动化脚本的主要依据。4.2 行为数据模拟这是模拟的核心也是与真实浏览器交互最像的部分。鼠标移动轨迹生成不要生成直线或完全随机的点。真实用户的鼠标移动具有“费茨定律”特征快速移动到目标区域然后有小范围的微调和停顿。可以模拟这种运动生成一系列目标点比如从屏幕左上角到搜索按钮。使用一个带噪声的贝塞尔曲线或分段线性函数来生成路径点。为每个点加上时间戳时间间隔不是固定的而是符合人类反应时间的分布如大部分在50-150ms偶尔有200ms以上的“思考”停顿。事件序列模拟mousedown-mousemove(多次) -mouseup这样的事件序列并将这些事件的坐标和时间差编码到mainArray对应的位置。编码方式通常是将(dx, dy, dt)三个值合并或哈希成一个整数。定时器与异步触发在浏览器中很多收集器是通过setTimeout或requestAnimationFrame异步调用的。在你的模拟脚本中也需要引入类似的随机延迟而不是一次性同步计算所有数据。4.3 组装与编码按照还原的positionConfig将模拟好的环境数据和行为数据填充到一个数组中。然后严格按照逆向出来的算法流程对这个数组进行转换、混淆和编码。数组序列化将数组元素用特定的分隔符如,或;连接成字符串。这个分隔符需要在逆向时确认。应用混淆算法实现你找到的位移、异或、字符替换等操作。密钥的生成逻辑也需要逆向出来它可能基于mainArray开头的某个随机种子。最终编码执行Base64编码得到最终的sensor_data字符串。验证你的模拟将生成的sensor_data替换到真实浏览器的请求中使用浏览器插件或代理工具提交给服务器观察是否能成功获得_abckcookie。这是一个反复测试和调整的过程。5. 常见问题与排查技巧在逆向和模拟的过程中你会遇到无数坑。这里记录一些典型问题和解决思路。问题现象可能原因排查思路与解决方案提交sensor_data后返回的_abckcookie值无效或很快过期。1. 环境指纹Canvas、字体等被识别为伪造。2. 行为数据过于规律像机器生成。3. 算法还原有误编码格式不对。1.固定指纹法从一次成功的真实会话中“窃取”指纹部分的编码值直接复用。2.引入随机性和人性化在鼠标轨迹中加入更多随机抖动和停顿使速度曲线更自然。3.网络比对用Wireshark或DevTools精确对比你的模拟请求和真实浏览器请求的原始报文确保sensor_data字符串的长度、字符集完全一致。检查请求头如Content-Type、Origin是否一致。根本无法定位到生成sensor_data的核心函数或数组。1. 代码混淆程度极高入口隐蔽。2. 核心逻辑可能在Web Worker或iframe中执行。3. 使用了反调试技术。1.搜索字符串常量在代码中搜索sensor_data、_abck、/akamai/等URL路径这些字符串通常不会被混淆。2.事件监听器在DevTools的Elements面板选中body标签查看Event Listeners找到可能绑定在document或window上的load,mousemove等事件的处理函数这些函数很可能属于防护脚本。3.禁用反调试有些代码会检测debugger语句或DevTools是否打开。可以尝试使用setTimeout包裹debugger、使用Object.defineProperty重写console.log等方式绕过或者直接使用Frida等更底层的工具进行Hook。还原的算法在本地测试成功但部署到服务器后失败。1. 服务器环境时区、语言、IP与测试环境不同影响了某些数据的计算如Date对象。2. 服务器请求频率过高触发风控。3. Akamai脚本版本更新。1.环境一致性确保你的模拟脚本运行的环境其系统时区、语言设置与“真实用户”预期一致。对于IP使用高质量的住宅代理IP至关重要。2.控制请求节奏加入随机延迟模拟真人浏览间隔避免高频访问。3.建立版本监控定期用真实浏览器访问目标页面抓取新的JS文件与旧版本进行diff及时发现算法更新。鼠标轨迹等行为数据模拟了但依然失败。1. 轨迹数据编码方式理解错误例如编码的是相对坐标还是绝对坐标是位移向量还是速度。2. 遗漏了其他关键行为事件如touchstart、scroll、focus/blur。1.动态调试在真实浏览器中在鼠标事件处理函数里下断点直接查看事件对象event被加工成了什么数值然后才存入数组。对比你的模拟输出和这个值。2.全面监控Hook所有可能的事件监听器添加过程 (EventTarget.addEventListener)记录下所有被防护脚本监听的事件类型确保你的模拟覆盖了所有类型。最后一点个人体会逆向Akamai的sensor_data是一个系统工程需要耐心、细致的观察力和强大的逻辑推理能力。它没有一成不变的解决方案今天的有效方法明天可能就失效了。因此核心能力不是记住某个网站的某个算法而是掌握一套通用的分析、定位、Hook、还原和模拟的方法论。保持对代码的敬畏像法医解剖一样对待每一行混淆的JS你总能找到那条通往核心逻辑的路径。这个过程本身就是对浏览器原理、Web安全和JavaScript语言的深度学习。