Akamai sensor_data 全参数拆解与工程实现

Python爬虫挣钱 2026-08-24 15:56:51 阅读(27) 评论(0)

本文主要记录 Akamai sensor_data 的生成与还原思路。整体请求流程可以概括为:先通过页面 HTML 获取 Akamai JS 地址,再请求 JS 文件,随后连续 POST 三次 sensor_data,看本文建议自己同步尝试。

第一次 JS 地址通常可以直接从 HTML 中提取,示例代码如下:

js_url = f"{urlparse(url).scheme}://{urlparse(url).netloc}" + re.search(
'type="text/javascript" {2}src="(.*?)">', response.text
).group(1)

Akamai sensor_data 全参数拆解与工程实现

本文的目标是复现后续三次 POST sensor_data 的流程。由于 Akamai 每次下发的 HTML 和 JS 都可能变化,为了方便调试,可以先把代码替换到本地环境中运行。

在正式还原之前,建议先处理 toString Hook,否则部分格式化检测可能无法通过。这里不需要做得太复杂,重点处理长函数名的替换即可;其他格式化检测可以按常规混淆还原思路处理。

Akamai sensor_data 全参数拆解与工程实现

1. 定位 sensor_data 来源

先在浏览器里打一个 XHR 断点,观察 sensor_data 的生成链路。从调用栈往上追,可以看到这里的 dcK 是关键位置;继续向上会发现它来源于 RNK,再全局搜索 RNK

Akamai sensor_data 全参数拆解与工程实现

继续追踪后,可以定位到下图位置。这里先做一层简单还原:

Akamai sensor_data 全参数拆解与工程实现

这段代码简单还原后如下图。后续对下图中的 13b 对象做一系列处理。我们的重点不是最终加密成 sensor_data,而是还原 13b 对象中各个属性的来源。

Akamai sensor_data 全参数拆解与工程实现

2. 解混淆思路

这里的解混淆核心思路是把类似下面这种动态属性访问:

 

a[x()[b(c)]]


替换成带记录能力的形式:

a[obj["x()[b(c)]"] = x()[b(c)]]


因为 JS 会先执行右侧的 x()[b(c)],再把结果赋值到 obj 对应的 key 中,随后原逻辑仍然可以继续正常执行。等整体流程跑完后,把 obj 导出,就能得到类似下面的映射:

obj = {
"x()[b(c)]": "charCodeAt"
}


接着再用 AST 把原来的:

a[x()[b(c)]]


替换成:

a["charCodeAt"]


这样就能得到更多明文属性,方便后续定位。具体实现时,可以把规则和几个样例交给 Codex 生成 AST 替换代码,并要求它做验证。

运行完替换 obj 的 AST 脚本后,再放到浏览器中执行一遍。注意需要晃几下鼠标,因为 Akamai 会监听 MouseEvent。随后把导出的 obj 拷贝回来,再跑一遍 AST 替换,就能得到一份还原后的 JS。

需要注意:不是所有位置都会被还原。被还原的位置说明运行时确实跑到了;未还原的位置则说明当前流程没有覆盖到。

3. 参数分析

继续在 13b 对象处打断点。本文不会展开所有参数,如果是浏览器指纹类参数,只简单说明来源;后续可以根据关键字继续追。

3.1 ajr

后续基本都采用类似的搜索方法:先定位 ajr,可以看到它来源于 ORb,而 ORb 的来源如下:

var ORb = IdK()(nz(IZ, [
"startTimestamp", tZ["window"].bmak["startTs"],
"deviceData", XKK(Xr, [X9b]),
"mouseMoveData", HRb,
"totVel", UBK,
"deltaTimestamp", Jqb
]));


Akamai sensor_data 全参数拆解与工程实现

nz(xxx) 的结果如下图,然后继续进入 IdK()Akamai sensor_data 全参数拆解与工程实现可以看到 IdK() 中的关键逻辑:TwK 返回 Math.floor(Math.random() * 100000 + 10000)totVel 在第一次 POST 时为 0mbK 只是字符串累加。到这里,ajr 的生成逻辑基本就明确了。这里传入的 totVel 只有第一次 POST 时才是 0startTs 则是在第一次加载 Akamai JS 时生成的时间戳。Akamai sensor_data 全参数拆解与工程实现还有一个小点:ajr 不是每天都保持同一种格式。目前我观察到的格式包括:

num|num|num
1,68344,2,34172,4,17086|68344


另外还有两种,目前总共观察到四种。后续继续补充如何判断当天 JS 使用的是哪一种 ajr 格式。

3.2 fpt / fpc

fpt: ["", -1, "dis", "", true, true, true, -480, true, screen.colorDepth, screen.pixelDepth, navigator.cookieEnabled, navigator.javaEnabled, -1].join(';')
fpc: "" + sumAscii(fpt)


 

function sumAscii(str) {
if (str == null) return -1;
let sum = 0;
for (let i = 0; i < str.length; i++) {
let code = str.charCodeAt(i);
if (code < 128) sum += code;
}
return sum;
}

3.3 din

din 内部字段是有顺序的。在不替换js的情况下,每次看到的顺序可能不同。这个顺序和 ajr 一样,也需要根据当前 JS 来判断。

下面开始分析 din 中的参数:

Akamai sensor_data 全参数拆解与工程实现

这里不再逐个关键字展开,方法都是一样的:通过关键字定位来源即可。比如 year_div = 2016,这类值的来源并不难追。

function updateDin(startTs, din) {
updateDinField(din, 'she', screen.height);
updateDinField(din, 'swi', screen.width);
updateDinField(din, 'nps', "20030107");
updateDinField(din, 'ash', screen.availHeight);
updateDinField(din, 'nap', "Gecko");
updateDinField(din, 'ua', navigator.userAgent);
updateDinField(din, 'dau', 0);
updateDinField(din, 'wdr', 0);
updateDinField(din, 'hz1', parseInt((startTs / (year_div * year_div)), 10));
updateDinField(din, 'npl', 0);
updateDinField(din, 'asw', screen.availWidth);
updateDinField(din, 'wow', window.outerWidth);
updateDinField(din, 'adp', "cpen:0,i1:0,dm:0,cwen:0,non:1,opc:0,fc:0,sc:0,wrc:1,isc:0,vib:1,bat:1,x11:0,x12:1");
updateDinField(din, 'pha', 0);
updateDinField(din, 'nal', navigator.language);
updateDinField(din, 'ibr', 0);
updateDinField(din, 'wiw', window.innerWidth);
updateDinField(din, 'tsd', 0);
updateDinField(din, 'ucs', sumAscii(navigator.userAgent).toString());
updateDinField(din, 'wih', window.innerHeight);
updateDinField(din, 'ran', "".concat(Math.random()).slice(0, 11) + parseInt(Math.random() * 1000 / 2, 10));
updateDinField(din, 'hal', startTs / 2);
updateDinField(din, 'xag', 12147);
}


其中:

"eem": "do_en,dm_en,t_en" // DeviceOrientationEvent、DeviceMotionEvent、TouchEvent 都存在时就是这个结果
"ffl": js_url.split('/').slice(-4)[0]

3.4 fwd.ssh

ssh 对应的是 nBK。找到 nBK 赋值的位置后打断点。

Akamai sensor_data 全参数拆解与工程实现

下图是 nBK 的赋值位置,参数就是 speechSynthesis.getVoices() 的值拼接后的结果。

Akamai sensor_data 全参数拆解与工程实现

VKK 是下面这个函数:

function arrayToHexString(numbers) {
let hexString = "";
for (let i = 0; i < numbers.length; i++) {
const hex = numbers[i].toString(16);
hexString += hex.length === 2 ? hex : "0" + hex;
}
return hexString;
}


下图是 fSK 函数:

Akamai sensor_data 全参数拆解与工程实现

3.5 mst

mst 中有很多字段,这里先看 jsrf1 和 jsrf2

全局搜索 jsrf1,然后一直向上追到对应函数,重写即可。

Akamai sensor_data 全参数拆解与工程实现

几个字段说明:

mst.dd2: parseInt(parseInt(startTs / (year_div * year_div), 10) / 23, 10)
mst.ww8: 0
mst.mc: mouse_finger.length // 鼠标指纹数量,第一次没有鼠标轨迹,直接是 0
mst.dvc: 'aciY9hfhidiicdxfraiY,614501,f+c+k+i+j+b+a+d+h+e+g+l+'


mst.dvc 的第一段是 VMP 跑出来的结果,第二段是 delt 时间,第三段后面再讲。VMP 部分我在另一篇文章中已经展开过,这里不再深入。

mst.nfas 解密后可以直接查到 nfas 关键字,浏览器里采集即可:

function get_nfas() {
const apis = [
{ obj: navigator, key: 'credentials' },
{ obj: navigator, key: 'appMinorVersion' },
{ obj: navigator, key: 'bluetooth' },
{ obj: navigator, key: 'storage' },
{ obj: Math, key: 'imul' },
{ obj: navigator, key: 'getGamepads' },
{ obj: navigator, key: 'getStorageUpdates' },
{ obj: navigator, key: 'hardwareConcurrency' },
{ obj: navigator, key: 'mediaDevices' },
{ obj: navigator, key: 'mozAlarms' },
{ obj: navigator, key: 'mozConnection' },
{ obj: navigator, key: 'mozIsLocallyAvailable' },
{ obj: navigator, key: 'mozPhoneNumberService' },
{ obj: navigator, key: 'msManipulationViewsEnabled' },
{ obj: navigator, key: 'permissions' },
{ obj: navigator, key: 'registerProtocolHandler' },
{ obj: navigator, key: 'requestMediaKeySystemAccess' },
{ obj: navigator, key: 'requestWakeLock' },
{ obj: navigator, key: 'sendBeacon' },
{ obj: navigator, key: 'serviceWorker' },
{ obj: navigator, key: 'storeWebWideTrackingException' },
{ obj: navigator, key: 'webkitGetGamepads' },
{ obj: navigator, key: 'webkitTemporaryStorage' },
{ obj: Number, key: 'parseInt' },
{ obj: Math, key: 'hypot' }
];

return apis.reduce((fingerprint, api, index) => {
const exists = Boolean(api.obj[api.key]);
return fingerprint + (exists << index);
}, 0);
}


mouse_finger 的格式如下图。鼠标指纹只需要 21 个点:第一个值是下标,第二个值固定是 1,第三个值是时间戳。

Akamai sensor_data 全参数拆解与工程实现

下面这部分是鼠标指纹相关字段。第一次请求时,这些参数基本都是固定写死的;第二、三次 sensor_data 才需要真实鼠标轨迹。

var mev = mouse_finger.slice(0, 21).join(';') + ';'


mst.dmvl: new Date().getTime() - startTs
mst.devl: new Date().getTime() - startTs
mst.mevl: 32 + count_fingerprint(mev) // count_fingerprint 计算 mev 中所有数字的总和
mst.tovl: count_fingerprint(mev) + devl + dmvl
mst.tst: devl + dmvl + count_time_fingerprint(mev) // count_time_fingerprint 计算 mev 第三位的总和

4. 借助 AI 和 AST 自动化还原

可以通过少量补环境、替换代码、运行截获的方式生成下面这些关键内容。只有这些内容组合完整后,最终的 sensor_data 才是有效的。

Akamai sensor_data 全参数拆解与工程实现

最终目标是找到最可能产出 din 的调用点,然后构造一个足够运行的环境,在运行时把结果直接截获出来。

整体方法可以概括为:

AST 锁点 + 条件去干扰 + VM 补环境执行 + Hook 截结果

打个比方,代码中有 A/B/C 三段逻辑,其中 B 是环境检测。我们可以直接替换检测条件,让代码成功跳过 B,继续跑到 C

比如下图中,ZWK 是 dvc 后半部分,但它是拼接出来的。它上面有一堆环境检测,只有条件为 true 才会进入。那么只要能定位这种位置,就可以把 if 条件替换成恒真表达式,让主流程正常跑下去。如果只是检测一些简单的 navigator 字段,也可以通过补环境解决。

Akamai sensor_data 全参数拆解与工程实现

可以看到,ZWK 经常出现在 try / if / else / catch 这样的结构中,这就是它的特征。我们可以用 AST 匹配这个特征来锁定 ZWK

拿到一份新的 JS 时,通常还没有做还原,也没有 sendwindowxhr 这类可读特征。虽然不一定只有一个 ZWK 符合条件,但能同时满足 try / if / else / catch 结构约束的候选不会太多。同时,真正的 ZWK 一定会在多个位置出现,实际数量通常不止六七处。

正常流程基本都是走 try 里面 if 为 true 的分支。所以可以把 if() 中的表达式替换成恒真表达式,例如 if(true)。为了减少被格式化检测影响,表达式长度最好保持和原始代码接近。这里可以让 AI 根据思路生成代码,但必须要求它验证结果。

function buildTruthyExprOfLength(length) {
if (length < 1) {
throw new Error(`Cannot build a truthy expression with length ${length}`);
}

if (length === 1) return "1";
if (length === 2) return "[]";
if (length === 3) return "~[]";
if (length === 4) return "true";
if (length === 5) return "!![] ";
return `!0/*${"a".repeat(length - 6)}*/`;
}


在这个 try / if / else / catch 模块中,需要记录两个参数:一个是 dvc 后缀的 ZWK,另一个是它后面的 S7K。接着找到 S7K 的消费点。S7K 只会在这里作为第二个参数使用,如下图:

Akamai sensor_data 全参数拆解与工程实现

比较巧的是,这里的 Wrb 刚好就是 din。同时也可以打印 ZWK;这里的函数就是 dvc 前缀部分的 VMP,不过本文不继续研究它。

Akamai sensor_data 全参数拆解与工程实现

所以,只要流程能跑到这个位置,就能拿到 din 和 dvc 后缀。这个方法最终会走到下图指向的位置:

后面还会执行一个 func(a, b, c, d),固定四个参数。我们不需要完整跑到那里,只需要让 ORb 这一段跑完即可。跑完后可以得到类似:

22596|20383|54245


这就是需要的 ajr 格式。

到这里,还剩下 ver 和 previousValue 没有生成。它们用于加解密 sensor_data,同样很重要。

在 rSK 函数中:

Akamai sensor_data 全参数拆解与工程实现

我这里是FAK(previousValue):

Akamai sensor_data 全参数拆解与工程实现

继续找它的生成位置,会发现代码非常混乱,几乎看不出规律。但如果看还原后的代码,就能发现 previousValue 实际上就在 ver 前面。

Akamai sensor_data 全参数拆解与工程实现

因此可以反过来理解:先找到 ver 的消费点,再追它的声明,就能找到 previousValue的参数名。

从下面可以看到,z2K 总是在 func(a, [b, c, ...]) 中 c 的位置出现,而 z2K 自身被消费的位置并不多,形态类似:

obj = cb(something, [qbt, ver, ...])

约束条件包括:

  • • 第二个参数必须是数组;
  • • 数组的第 2 个元素,也就是下标 1
  • • 该元素必须是标识符。

Akamai sensor_data 全参数拆解与工程实现

当然,单独看 obj = cb(something, [qbt, ver, ...]) 这个规律并不强,因为很多地方都可能符合。但放大范围看,这部分代码和前面 verdvc 前半段对应的 func 在同一作用域下。多个规律叠加约束后,命中结果就比较稳定了。

Akamai sensor_data 全参数拆解与工程实现

这些规律都是通过观察多份 JS 总结出来的,不是凭一份源码就能稳定归纳。实际操作时,可以收集多份源码,把规律描述给 AI 生成算法,然后让它在另一份源码上验证是否能成功截取需要的内容。

到目前为止,我们可以通过 AST、代码注入和运行时截获,还原出:

  • • din
  • • ajr
  • • dvc 前半段
  • • previousValue
  • • ver

除此之外,只需要补少量环境即可。由于目标参数都是全局存在即可,不需要深度补原型链。

Akamai sensor_data 全参数拆解与工程实现

function XMLHttpRequest() {}
XMLHttpRequest.prototype.open = function open() {};
XMLHttpRequest.prototype.send = function send() {
return undefined;
};

const location = {
href: urlValue,
};

const document = {
URL: urlValue,
location,
addEventListener() {},
documentElement: {
getAttribute() {
return null;
},
},
createElement() {
return {
getAttribute() {
return "";
},
};
},
getElementsByTagName() {
return [
{
src: srcValue,
getAttribute() {
return "";
},
},
];
},
};

5. 继续构造 sensor_data

5.1 ajr

目前暂时观察到四种 ajr 格式。传入参数包括 ajr 和下面这个对象:

{
"startTimestamp": startTs,
"deviceData": getDinAllValues([din]).join(','),
"mouseMoveData": "",
"totVel": tovl,
"deltaTimestamp": deltaTimestamp
}


其中 ajr 是上面生成的,用来区分当前 JS 使用的是哪种格式;tovl (fk2)的逻辑前面也已经说明。

function genAjr(ajr, fk2) { // fk2 就是上面的对象
if (isAjrHashValid(ajr)) {
var FM9 = fk2['totVel'] || xr2()
var p3 = fUT(arrayToHexString(Exw(FM9)))
var p39 = getDivisors(p3)
return [p39, FM9].join('|')
}
if (isValid(ajr)) {
var FM9 = fk2['totVel'] || xr2()
var p39 = getDivisors(FM9)
return [p39, FM9].join('|')
}
if (ajr.split('|').length === 3) {
try {
atob(ajr.split('|')[0])
var A2 = fk2['totVel'] || xr2();
var Dt = xr2();
var bx = [btoa(Dt * A2), Dt, A2];
return bx['join']('|');
} catch (e) {
var B92 = fk2["totVel"] || xr2();
var G12 = xr2();
var V92 = B92 * G12;
var m92 = parseInt(Math["sqrt"](V92), 10);
var v72 = [m92, G12, B92];
return v72["join"]("|")
}
}
}

5.2 ver / din / dvc

ver 前面已经生成,这里不再展开, ver对于同一个js是固定的。

din 部分就是按前面的 updateDin 逻辑更新对象中的对应值。注意不要改变 din 对象的字段顺序。

dvc 我在另一篇文章里讲过,这里贴一下 VMP 相关代码,通过看日志就可以还原了。参数说明:

  • • token 传 ajr
  • • UA 是 userAgent
  • • post_count 表示第几次 sensor_data,第一次是 0,第三次是 2

function vmp_encode(token, UA, startTs, deltaTimestamp, tovl, post_count) {
let frist = tovl.toString() + startTs.toString() + UA.slice(-32) + post_count.toString();
r = 5381
for (i = 0; i < frist.length; i++) { r = (r * 33) ^ frist.charCodeAt(i) } var first_r = r >>> 0
var b_str = first_r.toString(2).split('')
var s_str = 'a3cd9efghiYjklm7opqrs1uvwQxyBz2'.split('')
var res = []
for (i = 0; i < s_str.length; i++) {
let j = i % b_str.length
if (b_str[j] == '1') {
res.push(s_str[i])
} else {
if (i % 3 == 0) {
res.push(s_str[i])
}
}
}
var time_2 = (post_count.toString() + (startTs + deltaTimestamp)).split('')
var res_2 = []
for (i = 0; i < time_2.length; i++) {
res_2.push(res[time_2[i]])
}
first = deltaTimestamp.toString() + token + post_count.toString()
r = 5381
for (i = 0; i < first.length; i++) { r = (r * 33) ^ first.charCodeAt(i) } second_r = (r >>> 0) + first_r
res_str = res.join('')
second_r_b = second_r.toString(2)
var res_3 = []
for (i = 0; i < 6; i++) { let index = parseInt(second_r_b.charAt(i)) let char = res_str.charCodeAt(i) let code_first = char >> index
let code_second, code_third
let code_forth = (char << 5) | code_first
if (index == 1) {
code_second = (char + 2) ^ 7
code_third = (char << 3) - char
} else {
code_second = (char + 2)
code_third = (char << 0) - char
}
res_3.push(res_str.charAt(Math.abs((code_third * code_forth - code_second)) % res_str.length))
}
return res_2.join('') + res_3.join('')
}

6. 第二次 sensor_data

拿第一次和第二次发送的 JSON 做对比,见下面几张图。

Akamai sensor_data 全参数拆解与工程实现

Akamai sensor_data 全参数拆解与工程实现

Akamai sensor_data 全参数拆解与工程实现

Akamai sensor_data 全参数拆解与工程实现

对比后可以看到:

  • • ajr 变了,但生成逻辑不变,只是 tovl 发生变化;
  • • ran 变了,它本身就是随机数,直接更新即可;
  • • din 的字段顺序没有变化;
  • • ajt 变成 11.1。第一次和第二次对比不明显,但多采几次第二次请求就会发现,每次第二次都是 11.1;也可以直接从代码里确认;
  • • dme 和 doe 本质上是一组固定数字,中间加入了两个时间差。

Akamai sensor_data 全参数拆解与工程实现

mev 是鼠标轨迹,tovl 前面已经讲过,delt 也是时间差。需要注意的是,这个时间差最好不要小于鼠标指纹最后一个时间戳;这一点是观察结果,未进一步验证。

继续看几个字段:

mc: 鼠标轨迹数量,21 个即可
ww8: parseInt(parseInt(startTs / (year_div * year_div), 10) / 23 / 6, 10)
tst: 前面已经说明
dvc: 后缀不变,按前面的逻辑重新生成一次即可


 

其中 444322244 是浏览器 navigator.permissions.query 的结果,其他部分是固定拼接。per 第一次是 8,后面都是这个值,可以自行对照代码确认。

fwd.fmz: "1.75" // 固定值

7. 第三次 sensor_data

第三次内容较长,这里不完整展开。拿一份第三次数据对着 second_sensor_data 看即可。

相同逻辑不再重复,几个差异点如下:

  • • ajt 固定为 10.2
  • • 会多一个 dsi 字段。

Akamai sensor_data 全参数拆解与工程实现

dsi 也可以写死。但如果要还原算法,需要注意它使用的是 iframe.contentWindowiframe.contentWindow 是 window,但不等于当前页面的 window,因为中间多了一层 iframe。

Akamai sensor_data 全参数拆解与工程实现

"wsl": "4395630592,291017801,225932613,100,22,-1,1,-1,0,1,,,,,,,,,1,1"


这一段对应:

jsHeapSizeLimit,totalJSHeapSize,usedJSHeapSize,navigator.connection.rtt,window.speechSynthesis.getVoices().length,1,1,1,0,1,,,,,,,,,1,1

继续看 sww,里面有大量指纹信息。

直接搜 sww 可能搜不到结果,可以搜索 ext,定位到下图位置。如果打断点,会在一堆 Promise 中跳来跳去。下面那些 s0.concat 就是在拼接字符串,最终拼出 s045 这类字段名。

Akamai sensor_data 全参数拆解与工程实现

先打断点观察,发现 Promise 完成之后会得到我们想要的数据。继续看 data.gpu,里面有 gpuVendor,可以把它作为关键字继续追。

Akamai sensor_data 全参数拆解与工程实现

会来到下面这个位置。这里的 jdK 在上面可以看到等于 SharedWorker。第一次看到可能不熟悉,可以查 MDN 或让 AI 解释它的行为。

Akamai sensor_data 全参数拆解与工程实现

这里有一段 JS,可以猜测它是否会发出这个 message。封装后放到浏览器执行即可验证。

Akamai sensor_data 全参数拆解与工程实现

浏览器执行结果和前面看到的指纹信息一致。

Akamai sensor_data 全参数拆解与工程实现

可以按下面的结构整理这段 JSON:

"sww": {
"s024": 0,
"ext": userAgentData.ext,
"swrt": 2,
"wrt": 16,
"s025": chinaDate, // 就是 new Date().toString()
"s026": chinaDate,
"s027": userAgentData.oscpu,
"s028": userAgentData.oscpu,
"s029": userAgentData.tz,
"s030": userAgentData.tz,
"s031": userAgentData.la,
"s032": userAgentData.la,
"s033": userAgentData.las,
"s034": userAgentData.las,
"s035": userAgentData.dm,
"s036": userAgentData.dm,
"s037": userAgentData.hc,
"s038": userAgentData.hc,
"s039": userAgentData.net,
"s040": userAgentData.net,
"s041": userAgentData.ua,
"s042": userAgentData.ua,
"s043": userAgentData.av,
"s044": userAgentData.av,
"s045": userAgentData.pl,
"s046": userAgentData.pl,
"s047": userAgentData.uad.brands,
"s048": userAgentData.uad.brands,
"s049": userAgentData.uad.mobile,
"s050": userAgentData.uad.mobile,
"s051": userAgentData.uad.architecture,
"s052": userAgentData.uad.architecture,
"s053": userAgentData.uad.bitness,
"s054": userAgentData.uad.bitness,
"s055": userAgentData.uad.model,
"s056": userAgentData.uad.model,
"s057": userAgentData.uad.platform,
"s058": userAgentData.uad.platform,
"s059": userAgentData.uad.platformVersion,
"s060": userAgentData.uad.platformVersion,
"s061": userAgentData.uad.uaFullVersion,
"s062": userAgentData.uad.uaFullVersion,
"s063": userAgentData.uad.wow64,
"s064": userAgentData.uad.wow64,
"s065": userAgentData.uad.fullVersionList,
"s066": userAgentData.uad.fullVersionList,
"s067": userAgentData.gpu.gpuVendor,
"s068": userAgentData.gpu.gpuVendor,
"s069": userAgentData.gpu.gpuRenderer,
"s070": userAgentData.gpu.gpuRenderer,
"s071": userAgentData.gpu.gpu2Vendor,
"s072": userAgentData.gpu.gpu2Vendor,
"s073": userAgentData.gpu.gpu2Renderer,
"s074": userAgentData.gpu.gpu2Renderer
},


剩下还有一些字段,要么是固定值,要么是随机数。通过 s148s151 这类关键字可以直接查到。

Akamai sensor_data 全参数拆解与工程实现

到这里,三次 sensor_data 的主要内容就已经完成了。

8. 加密 JSON

要形成最终的 sensor_data,还需要做一层加密。这部分网上资料较多,这里需要注意:

  • • bm_sz 来自第一次 GET HTML 时返回的 Cookie;
  • • previousValue 前面已经生成,稍微步入几个函数即可还原。

function BbJ(XmJ, ver) {
return ["3", "0", 1, 0, XmJ[0], ver].join(";");
}
function Qj(bm_sz, previousValue) {
var dH = [8888888, previousValue];
var rF = bm_sz;
var Gl = decodeURIComponent(rF).split("~");
var dj = parseInt(Gl[2], 10);
dj = isNaN(dj) ? previousValue : dj;
dH[0] = dj;
return dH;
}
function Ng(Eh) {
var dX = Eh[0];
var p7 = Eh[1];
var TO = ":";
var Oj = dX.split(TO);
for (var sS = 0; sS < Oj.length; sS++) { var pm = ((p7 >> 8) & 65535) % Oj.length;
p7 *= 65793;
p7 &= 4294967295;
p7 += 4282663;
p7 &= 8388607;
var Sj = ((p7 >> 8) & 65535) % Oj.length;
p7 *= 65793;
p7 &= 4294967295;
p7 += 4282663;
p7 &= 8388607;
var ET = Oj[pm];
Oj[pm] = Oj[Sj];
Oj[Sj] = ET;
}
return Oj.join(TO);
}
var WO = [];
var N2 = "";
function P7(Tj, QX) {
if (!N2) {
for (var sl = 0; sl < 127; ++sl) {
if (sl < 32 || sl === 39 || sl === 34 || sl === 92) {
WO[sl] = -1;
} else {
WO[sl] = N2.length;
N2 += String.fromCharCode(sl);
}
}
}
var Xb = "";
for (var kn = 0; kn < Tj.length; kn++) { var Eb = Tj.charAt(kn); var XF = (QX >> 8) & 65535;
QX *= 65793;
QX &= 4294967295;
QX += 4282663;
QX &= 8388607;
var Kl = WO[Tj.charCodeAt(kn)];
var ws = Eb.codePointAt(0);
if (ws >= 32 && ws < 127) { Kl = WO[ws]; } if (Kl >= 0) {
var Y2 = XF % N2.length;
Kl += Y2;
Kl %= N2.length;
Eb = N2[Kl];
}
Xb += Eb;
}
return Xb;
}
function encrypt_sensor_data(sensor_data, bm_sz, previousValue, ver) {
var XfJ = Qj(bm_sz, previousValue);
var JWJ = JSON.stringify(sensor_data);
JWJ = Ng([JWJ, XfJ[1]]);
JWJ = P7(JWJ, XfJ[0]);
var vmJ = 0;
var LlJ = Math.floor(Math.random() * 4);
var PsJ =
String(Math.floor(Math.random() * 6) + 8) +
",0,0," +
vmJ +
"," +
LlJ +
",0";
var cvJ = BbJ(XfJ, ver);
JWJ = cvJ + ";" + PsJ + ";" + JWJ;
var VkJ = JSON.stringify(JWJ);
return '{"sensor_data":' + VkJ + "}";
}

9. 总体流程

整体流程可以整理为:

  1. 1. GET HTML
  2. 2. 从 HTML 中提取 Akamai JS 地址;
  3. 3. GET JS
  4. 4. 通过 JS 获取 verdin 等关键参数;
  5. 5. 直接计算三次 sensor_data
  6. 6. 分批次 POST 即可。

有些 Akamai 版本还会出现 ffsvevinf 等参数。只要完成解混淆,这些参数通常都比较容易继续还原。

下面提供一个 Python 请求流程示例:

import json
import subprocess

from pathlib import Path
from bs4 import BeautifulSoup
from curl_cffi import requests
from loguru import logger
from urllib.parse import urlparse
import re

session = requests.Session(impersonate="chrome")

def get_tracking(url):
    response = session.get(url, http_version=3)
    bm_sz = response.cookies["bm_sz"]
    with open('./home.html', 'w', encoding='utf-8') as f:
        f.write(response.text)

    # 获取 ffs
    # ffs = generate_fingerprint(response.text)
    ffs = ""  # 有些 Akamai 需要,有些不需要,解混淆后可以直接从代码中看到
    # logger.info("FFS: {}", ffs)

    js_url = f"{urlparse(url).scheme}://{urlparse(url).netloc}" + re.search(
        'type="text/javascript" {2}src="(.*?)">', response.text
    ).group(1)

    logger.info("JS URL: {}", js_url)
    logger.info("第一次请求 HTML,Cookies: {}", dict(response.cookies))
    return bm_sz, ffs, js_url

if __name__ == "__main__":
    url = 'https://www.***.com/cn/zh/home?adults=1&flight-type=2&origin=PVG'
    bm_sz, ffs, js_url = get_tracking(url)
    logger.info("bm_sz: {}", bm_sz)
    logger.info("ffs: {}", ffs)
    logger.info("js_url: {}", js_url)

    # GET 请求 JS 文件
    response = session.get(js_url)
    with open('./home.js', 'w', encoding='utf-8') as f:
        f.write(response.text)

    # 这里直接生成三次 sensor_data
    sensor_result = run(bm_sz, url, js_url, ffs, str(BASE_DIR / "home.js"))

    parsed = urlparse(url)
    origin = f"{parsed.scheme}://{parsed.netloc}"
    post_headers = {
        "accept": "*/*",
        "content-type": "text/plain;charset=UTF-8",
        "origin": origin,
        "referer": url,
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "same-origin",
        "user-agent": headers["User-Agent"],
    }

    # 第一次 POST 请求
    logger.info("第一次 sensor_data 长度: {}", len(sensor_result['first']))
    response = session.post(js_url, headers=post_headers, data=sensor_result['first'])
    logger.info('_abck 长度: {}, _abck: {}', len(response.cookies.get("_abck", "")), response.cookies.get("_abck", ""))

    # 第二次 POST 请求
    logger.info("第二次 sensor_data 长度: {}", len(sensor_result['second']))
    response = session.post(js_url, headers=post_headers, data=sensor_result['second'])
    logger.info('_abck 长度: {}, _abck: {}', len(response.cookies.get("_abck", "")), response.cookies.get("_abck", ""))

    # 第三次 POST 请求
    logger.info("第三次 sensor_data 长度: {}", len(sensor_result['third']))
    response = session.post(js_url, headers=post_headers, data=sensor_result['third'])
    logger.info('_abck 长度: {}, _abck: {}', len(response.cookies.get("_abck", "")), response.cookies.get("_abck", ""))

10. 补充说明

由于 Akamai 会校验 TLS 指纹,普通 requests 会被拦截,因此请求部分建议使用 curl_cffi,并通过 impersonate="chrome" 让 TLS / HTTP 指纹更接近真实 Chrome。同时一个js有几分钟复用时间,所以不需要每次都从js中还原新的ajr,din这部分内容。同时我们的请求是纯算出来的,只要把一些浏览器指纹鼠标指纹自己写一个配置文件就可以了。

本文由 子虚 投稿


长期征稿

稿费 1000-2000 元 / 篇,长期有效 。征集爬虫逆向、数据行业、爬虫副业、合规避坑类原创首发文章。详情见:

1000 元一篇,把你的逆向笔记变现

猿人学的业务:
1、猿人学爬虫逆向课2、海外代理IP(不限量、动态/静态住宅IP)3、youtube 等音视频下载回流4、大模型预训练数据5、SERP (google/bing等搜索结果)6、海外电商/海外社媒数据若有朋友需要可以加我微信,谢谢。Akamai sensor_data 全参数拆解与工程实现

猿人学banner宣传图

我的公众号:猿人学 Python 上会分享更多心得体会,敬请关注。

***版权申明:若没有特殊说明,文章皆是猿人学 yuanrenxue.con 原创,没有猿人学授权,请勿以任何形式转载。***

说点什么吧...