为了账号安全,请及时绑定邮箱和手机立即绑定

【JS逆向百例】某度 Acs-Token、ab_sr 逆向分析

标签:
Python

pZpIvFK.png

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!

前言

最近浏览 CSDN 博客的评论时,看见有位粉丝表示,案例网站做了更新,之前的教程已经失效了。相信很多小伙伴的入门练手案例都有某瓣、某道、某度翻译,因为复杂度相对较低,但是人家也不可能一直都是“软柿子”,这些年也有过几次更新。

例如某度翻译,K 哥就曾伴随更新写过两篇文章,22 年新增的 Acs-Token 参数,相关加密算法的 js,最初还自注释为“玉门关”,这应当是尚未完全更新,有些注释都描述的很清晰,后续又更新了几轮,这些痕迹自然就没有了。在加密算法的处理上,也用上了当时较火的 JSVMP,算是上了一道坎。

爬虫教程是有时效性的,大多数网站或多或少都会进行更新,“毕竟得干活”。不过呢,K 哥有空会对失效的教程进行更新,以供粉丝们更好的学习、成长。本文就再次对该站进行逆向分析:

pZpopSe.png

逆向目标

  • 目标:某度 Acs-Tokenab_sr 参数逆向分析

  • 网址:aHR0cHM6Ly9mYW55aS5iYWlkdS5jb20v

抓包分析

进入网站,打开开发者人员工具,随便输入一个需要查询的英文单词,比如 spider,就能抓包到相关翻译结果的接口。不同翻译方案对应的接口名有所不同,请求参数上,差异不大,相较于之前,还与时俱进的更新了 AI大模型翻译(translateIncognitoAi),不过限制请求频率,当然,这些不在本文的讨论范围内。

观察相关接口,会发现负载旁边多了个 EventStream,这是一种持续不断、单向(服务器 → 客户端)推送数据的响应方式。不像普通的接口只返回一次结果,SSE 接口会保持连接不断开,服务器可以源源不断地向浏览器发送新数据,响应头中的 Content-Type 参数值为 text/event-stream。该方案常用于 AI 聊天或文本生成(如 ChatGPT、Claude 的流式输出):

Server-Sent Events (SSE):中文通常叫“服务器发送事件”或“事件流响应”。

pZpodX9.png

SSE 与 WebSocket 的部分对比如下:

pZpbu40.png

请求头中的 Acs-Token 就是本文需要分析的加密参数:

pZpbNU1.png

同时,cookie 中还有个 ab_sr 参数,这个参数的生成流程挺有意思的,本文也会对其进行分析:

pZpbw8K.png

逆向分析

ab_sr

清除缓存,刷新网页,从数据接口的 cookie 中,复制该参数的值,ctrl + f 查找下,发现其是 abdr 接口响应返回的 cookie 值:

pZpbTbj.png

请求负载中的参数值是根据一些环境生成的:

pZpbban.png

接下来,就分析下,dataenckey_id 都是如何生成的。从堆栈中跟进到 abclite-2060-s.js 文件中(带有 2060 的都是和加密算法相关的文件,当年这个数字上有一行注释,“渠道号,由管理员分配”,这么多年,还没改过),可以看到,函数名是动态调用的,字符串也经过 Unicode 转义,代码经过了一些简单的混淆处理,直接跟或者使用 AST 技术解混淆都可以,AST 相关可以找蔡老板学习:

pZpbq5q.png

解完混淆后,替换对应文件,可以看到清晰了很多,瞬间就舒坦了,AST 好啊,AST 得学啊。解完混淆后的文件会分享到知识星球中,以供学习研究:

QxUSf4.png

跟栈进去,下断点,此时参数值已经全部生成了,即 aY:

QxU1lh.png

向上跟栈,找到生成该值的算法的位置,由下图可知,大对象 ad 经过 uH 函数处理后,生成了目标值,ad 中包含了相当多的环境参数,其中部分函数相互关联:

QxUPV9.png

跟到 uH 函数直接跟进去,data 参数是标准的 AES 加密,ak 为 key,al 为 iv,皆为定值,可以去 kgtools.cn/secret/aes 验证下:

QxURJY.png

主要来看看 ad 中的各参数是如何生成的,跟栈到下图位置,就是各个参数生成的关键点,ab 为键,ad 为值或生成值的相关函数:

QxUrTH.png

可以插桩打印出来看看,'ab ---> ', ab, '|| ad ---> ', ad

QxUtgZ.png

接下来断到各个值,向上跟栈,逐个分析即可。这些就是一堆环境检测,包括 canvas、部分参数相互绑定、多个自动化、事件、函数、属性检测等等,有的甚至加密了整个 js 文件内容,如果有某度系网站严格校验这个参数的话,可能存在的反爬、风控点很多。感兴趣的小伙伴,建议逐一研究下,其中很多思路是值得学习,并且有趣的。

Acs-Token

从 translate 接口(机翻·通用领域)跟栈到 index.c68bad98.js 文件中,断住后,可以看到,此时 Acs-Token 参数的值已经生成了:

QxUzYU.png

跟到异步生成器执行器位置,给 e 下日志断点,发现第二次断到此处时,参数值就生成了:

QxU4dq.png

那么,第一次断住后,单步往下跟,看是何时生成的 Acs-Token 参数的值。跟到下图位置,有个 switch-case 条件分支语句,case 0 启动一个 SSE 请求,监听消息、错误、关闭等事件,并根据返回内容更新翻译进度或出错处理(太长,部分代码经过折叠处理),整体是个 Promise 异步操作:

QxUJus.png

从 case 0 到 case 14 的 b = e.sent 处时,Promise 返回的结果也从 undefined 变成了 Acs-Token 参数的值,证明目标值的生成与 v(y = v.promise)息息相关:

QVg5e6.png

跟到 i.HB 中去,跳转到 d 函数,实际执行的 p 函数,核心生成器函数如下:

c().mark((function e() {
return c().wrap((function(e) {
for (; ; )
switch (e.prev = e.next) {
case 0:
return e.next = 2,
s.getAsync();
case 2:
return e.abrupt(“return”, new Promise((function(e, t) {
// ACS 实例获取逻辑
})));
case 3:
case “end”:
return e.stop()
}
}), e)
}))

下断,n.getSign 处目标值已经生成了:

QVgOjj.png

向上跟栈到 acs-2060.js 文件中(最好替换固定一套),该处下断,会不断断住,打日志断点分析下:

QVgtUY.png

'函数 —> ', i, '|| 数组 —> ', o

观察插桩出来的日志,最终输出了 Acs-Token 参数的值:

QOSDUV.png

找到值第一次生成的位置,跟到函数中去,接着插桩观察:

1: function f(t) {
return n.G(r, f).run(this, [t])
}

同样,找第一次出现的位置,根据下图可以推测,最终的值大概率就是由上面打印出来的对象,经过某个算法加密得到的:

QVgIGa.png

下断点,断住后,单步跟栈,跟到此处会发现有一个类似字节码解释器的位置,指令获取 → 指令解码 → 指令执行 → 程序计数器更新,最后返回结果:

t.prototype.run = function(t, n) {
// 获取字节码指令数组
var r = this._[2];

// 初始化执行环境: 设置 this 上下文和函数参数
for (this.q.A(“this”, t),
this.q.A(“arguments”, n || []); ; ) {

// 获取当前指令, 检查有效性
var i = r[this.Y];
if (!i)
throw new Error(“StackFrameInterpreter: instruct is undefined”);

// 解码指令: 操作码和准备信号变量
var o = i[0] // 操作码
, a = null; // 控制流信号

// 执行指令并处理异常
try {
// 从指令表 P 中根据操作码找到对应处理函数并执行
a = (0, P[o])(i, this) || null;
} catch © {
// 异常处理: 存储异常并生成错误信号
this.V = new e©,
a = this.W.P(1, c);
}

// 根据信号类型处理控制流
if (null !== a) {
// 处理跳转信号
if (j.U(a)) {
// 直接跳转到指定指令位置
this.Y = a.value;
} else {
// 检查是否为返回信号
if (!j.S(a)) {
// 处理其他类型信号或抛出错误
throw j.R(a) ? (this.nn(a.value), a.value) :
new Error(“StackFrameInterpreter: signal is not return or error”);
}

// 处理返回信号: 获取返回值
var f = this.V // 返回值容器
, u = null == f ? void 0 : f.get(); // 安全获取实际返回值

// 生成正常返回信号并检查
if (a = this.W.P(0, u),
j.S(a)) {
// 确认是最终返回,退出函数
return u;
}

// 不是最终返回, 继续执行
this.Y = a.value;
}

// 执行清理或状态更新操作
this.tn();
} else {
// 没有控制流信号, 简单前进到下一条指令
this.Y++;
}
}
};

还是插桩分析下,先看下 f.value,由下图可知,Acs-Token 参数的值是由三部分拼接而成的,17627112083951762711208395xa33oL...,第二部分也就是上面对象中 clientTs 参数的值,后文分析:

QVgMz7.png

再插桩观察下传进来的参数 n,可以看到,目标第一次打印出来的位置,还有两个参数:

QDXJdc.png

有些 VMP 站点的参数值都是 AES 加密,然后时不时更新 key,猜测这里就是 AES 加密算法的 key 和 iv,去 KGtools 验证后,证实了猜想,标准算法,并没有魔改:

KGtools AES 加解密:https://kgtools.cn/secret/aes

QDXCYf.png

从打印出的日志,还能看出来,加密对象中各个参数的值,都是一个个生成的:

QDXjw3.png

下断在 var r = this._[2] 处,断到 d0 已经生成值时,向上跟栈到 C[47] 中,a 数组中的两个元素就是目标键名和键值,同时这里还能插桩观察下调用函数:

QDXf7m.png

‘函数 —>’, o, ‘|| 对象 —>’, i, ‘|| 参数数组 —>’, a, ‘|| 调用结果 —>’, o.apply(i, a)

d0 的生成流程,直接就给打印出来了,再结合跟栈分析,逻辑即可复现:

QDXsXh.png

let d0_1 = Math.random().toString(32).substring(2);
let d0_2 = Date.now().toString(32);
let d0 = d0_1 + d0_2;
console.log(d0);

结合插桩、下断、单步分析,剩余每个参数的生成逻辑,都不难复现,所有算法都可以用 Python 还原,感兴趣的可以自行分析下。

Acs-Token 参数值的第一部分,看起来像个时间戳,其实在不同的 acs-2060.js 文件中该值是固定的,最开始这个值是明文,现在转成字节数组了。

至此,本案例的逆向分析就完成了,相关解混淆文件、算法会分享到知识星球中,仅供学习交流。

结果验证

图片描述

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消