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

pZpIvFK.png

声明

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

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

前言

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

例如某度翻译,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 (c) {
            // 异常处理: 存储异常并生成错误信号
            this.V = new e(c),
            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 文件中该值是固定的,最开始这个值是明文,现在转成字节数组了。

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

结果验证

QDjpXI.png

for (k = first_adj_chan ; (k <= last_adj_chan); k += 2) { int effchfactor; if ((k == acs_ch_idx) || (k <= 0)) continue; obss_weighted = 0; effchfactor = k - acs_ch_idx; if(effchfactor < 0) effchfactor = 0 - effchfactor; effchfactor = effchfactor >> 1; if(effchfactor == 0) effchfactor =1; if((acs->acs_noisefloor[k] != NF_INVALID) && (acs->acs_noisefloor[k] >= acs->acs_noisefloor_threshold )){ acs_info(ADJCHAN, "Adjacent channel NF (%4d) exceeded threshold (%4d), " "add 100 each to RSSI and load", acs->acs_noisefloor[k], acs->acs_noisefloor_threshold); adj_chan_stats->adj_chan_loadsum += 100 / effchfactor; adj_chan_stats->adj_chan_rssisum += 100 / effchfactor ; } else{ adj_chan_stats->adj_chan_loadsum += (acs->acs_chan_load[k] / effchfactor) ; adj_chan_stats->adj_chan_rssisum += (acs->acs_chan_snr[k] / effchfactor) ; } obss_weighted = (acs->acs_chan_nbss_near[k] * acs->acs_obss_near_range_weightage) + (acs->acs_chan_nbss_mid[k] * acs->acs_obss_mid_range_weightage) + (acs->acs_chan_nbss_far[k] * acs->acs_obss_far_range_weightage); adj_chan_stats->adj_chan_obsssum += (obss_weighted / effchfactor); adj_chan_stats->adj_chan_srsum += (ACS_DEFAULT_SR_LOAD / effchfactor) * acs->acs_srp_supported[k]; acs_info(ADJCHAN, "Per-adjacent-channel stats - " "pri_chan (%3d), " "adj_chan (%3d), " "effchfactor (%3d), " "acs_chan_load (%3d), " "acs_chan_rssi (%4d), " "acs_obss_weighted (%4d), " "acs_sr_load (%4d)", acs_ch_idx, k, effchfactor, adj_chan_stats->adj_chan_loadsum, adj_chan_stats->adj_chan_rssisum, adj_chan_stats->adj_chan_obsssum, adj_chan_stats->adj_chan_srsum); }分析代码功能
10-24
<think>我们分析给定的C语言循环代码段,该代码用于遍历相邻信道并计算相关统计信息。 循环结构:for (k = first_adj_chan; k <= last_adj_chan; k += 2) 说明:k从first_adj_chan开始,每次增加2(即步长为2),直到k大于last_adj_chan结束。因此,它处理的是从first_adj_chan到last_adj_chan的每一个偶数或奇数索引(取决于起始值的奇偶性)的信道。 接下来,我们逐步分析循环体内的代码: 1. 跳过条件: if ((k == acs_ch_idx) || (k <= 0)) continue; 如果当前信道k等于acs_ch_idx(可能是主信道)或者k<=0(无效信道索引),则跳过当前循环。 2. 初始化变量: obss_weighted = 0; // 用于计算加权OBSS(重叠基本服务集)值 3. 计算effchfactor(有效信道因子): effchfactor = k - acs_ch_idx; // 当前信道与主信道的索引差 if (effchfactor < 0) effchfactor = 0 - effchfactor; // 取绝对值 effchfactor = effchfactor >> 1; // 右移一位(相当于除以2,向下取整) if (effchfactor == 0) effchfactor = 1; // 如果为0则设为1(避免除以0) 注意:由于步长为2,所以索引差为偶数。右移一位相当于除以2,得到的是信道间隔数(例如,相邻信道间隔为1,间隔一个信道则为2,等等)。 4. 根据噪声阈值条件更新adj_chan_loadsum和adj_chan_rssisum: if ((acs->acs_noisefloor[k] != NF_INVALID) && (acs->acs_noisefloor[k] >= acs->acs_noisefloor_threshold)) { // 如果当前相邻信道的噪声值有效且超过阈值 acs_info(ADJCHAN, ...); // 输出信息 // 将100除以effchfactor(即按间隔数衰减)后加到负载和RSSI的和上 adj_chan_stats->adj_chan_loadsum += 100 / effchfactor; adj_chan_stats->adj_chan_rssisum += 100 / effchfactor; } else { // 否则,使用实际测量的信道负载和信噪比(除以effchfactor衰减)加到相应的和上 adj_chan_stats->adj_chan_loadsum += (acs->acs_chan_load[k] / effchfactor); adj_chan_stats->adj_chan_rssisum += (acs->acs_chan_snr[k] / effchfactor); // 注意:这里变量名是snr,但注释说是rssi,实际可能是信噪比 } 5. 计算加权OBSS: obss_weighted = (acs->acs_chan_nbss_near[k] * acs->acs_obss_near_range_weightage) + (acs->acs_chan_nbss_mid[k] * acs->acs_obss_mid_range_weightage) + (acs->acs_chan_nbss_far[k] * acs->acs_obss_far_range_weightage); // 将加权OBSS值除以effchfactor(衰减)后加到adj_chan_obsssum adj_chan_stats->adj_chan_obsssum += (obss_weighted / effchfactor); 6. 计算SR(Spatial Reuse,空间复用)负载: // 将默认的SR负载值(ACS_DEFAULT_SR_LOAD)除以effchfactor(衰减)后乘以当前信道是否支持SRP(空间复用参数) // 然后加到adj_chan_srsum adj_chan_stats->adj_chan_srsum += (ACS_DEFAULT_SR_LOAD / effchfactor) * acs->acs_srp_supported[k]; 7. 输出每个相邻信道的统计信息: acs_info(ADJCHAN, ...); // 输出各个统计量的值 总结: 该循环遍历所有相邻信道(间隔为2,可能是为了避免同频干扰?),计算每个相邻信道对主信道的干扰影响,并根据信道间隔进行衰减(间隔越远,影响越小)。统计量包括: - 信道负载和(adj_chan_loadsum) - 信道RSSI(或信噪比)和(adj_chan_rssisum) - 加权OBSS和(adj_chan_obsssum) - 空间复用负载和(adj_chan_srsum) 这些统计量将用于信道选择算法,以评估每个候选信道的干扰情况。 注意:代码中有一个细节,当相邻信道的噪声超过阈值时,使用固定值100(而不是实际测量值)来增加负载和RSSI,这可能是为了惩罚高噪声信道。 相关问题: 1. 为什么循环步长是2?为什么不是1? 2. 噪声阈值的作用是什么?为什么超过阈值要使用固定值100? 3. 加权OBSS的计算中,三个权重(near, mid, far)通常如何设置? 4. 空间复用(SR)负载的计算中,ACS_DEFAULT_SR_LOAD的值是多少?有什么意义? 5. 为什么在计算effchfactor时,当绝对值右移后为0时设置为1(即effchfactor=1)?这种情况在什么条件下发生? </think>### C语言代码功能分析:相邻信道统计计算 #### 1. **循环结构核心功能** ```c for (k = first_adj_chan; (k <= last_adj_chan); k += 2) { /* 信道统计计算 */ } ``` 该循环遍历从`first_adj_chan`到`last_adj_chan`的**相邻信道索引**(步长为2),主要功能是: - 计算相邻信道对主信道(`acs_ch_idx`)的干扰影响 - 根据信道间隔距离衰减干扰权重 - 聚合四个关键统计量: $$ \begin{cases} \text{负载总和} & \sum \frac{\text{channel\_load}}{\text{effchfactor}} \\ \text{RSSI总和} & \sum \frac{\text{SNR}}{\text{effchfactor}} \\ \text{OBSS总和} & \sum \frac{\text{obss\_weighted}}{\text{effchfactor}} \\ \text{SR负载} & \sum \frac{\text{SR\_load} \times \text{SRP}}{\text{effchfactor}} \end{cases} $$ #### 2. **关键计算步骤** ##### (1) 信道间隔因子计算 ```c effchfactor = k - acs_ch_idx; // 信道索引差 if(effchfactor < 0) effchfactor = -effchfactor; // 取绝对值 effchfactor = effchfactor >> 1; // 除2取整(右移) if(effchfactor == 0) effchfactor = 1; // 避免除0 ``` - **数学表示**:$$ \text{effchfactor} = \left\lfloor \frac{|k - \text{acs\_ch\_idx}|}{2} \right\rfloor $$ - **物理意义**:信道间隔越远,干扰衰减越大(如间隔2信道时effchfactor=1,间隔4信道时effchfactor=2) ##### (2) 噪声阈值检测 ```c if((acs->acs_noisefloor[k] != NF_INVALID) && (acs->acs_noisefloor[k] >= acs->acs_noisefloor_threshold)) { adj_chan_loadsum += 100 / effchfactor; // 惩罚性加权 adj_chan_rssisum += 100 / effchfactor; } else { adj_chan_loadsum += acs->acs_chan_load[k] / effchfactor; adj_chan_rssisum += acs->acs_chan_snr[k] / effchfactor; } ``` - **噪声超标处理**:当噪声超过阈值时,采用固定惩罚值100替代实际测量值 - **正常情况**:使用实际信道负载(`acs_chan_load`)和信噪比(`acs_chan_snr`) ##### (3) OBSS加权计算 ```c obss_weighted = (acs->acs_chan_nbss_near[k] * acs->acs_obss_near_range_weightage) + (acs->acs_chan_nbss_mid[k] * acs->acs_obss_mid_range_weightage) + (acs->acs_chan_nbss_far[k] * acs->acs_obss_far_range_weightage); adj_chan_obsssum += obss_weighted / effchfactor; ``` - **三维干扰模型**:根据OBSS(重叠基本服务集)设备的距离(近/中/远)进行加权求和 - **距离衰减**:不同距离段有独立权重系数(`near_range_weightage`等) ##### (4) 空间复用(SR)计算 ```c adj_chan_srsum += (ACS_DEFAULT_SR_LOAD / effchfactor) * acs->acs_srp_supported[k]; ``` - **SRP支持检测**:`acs_srp_supported[k]`标记信道是否支持空间复用协议 - **默认负载**:`ACS_DEFAULT_SR_LOAD`为基础负载值 #### 3. **物理层意义** - **信道选择优化**:为动态信道分配(DCA)算法提供干扰评估数据 - **5G/WiFi应用**:在多信道系统中避开高噪声/高干扰信道[^1] - **频谱管理**:通过$$ \sum \frac{\text{metric}}{\text{effchfactor}} $$ 模型实现: - 邻近信道高权重(effchfactor小) - 远端信道低权重(effchfactor大) #### 4. **调试输出分析 ```c acs_info(ADJCHAN, "Per-adjacent-channel stats - pri_chan (%3d), adj_chan (%3d)..."); ``` - 输出每个相邻信道的详细统计参数 - 包含关键变量:`effchfactor`(信道间隔因子), `adj_chan_loadsum`(聚合负载)等 --- ### 相关问题 1. 为什么使用`k += 2`遍历信道而非连续索引?这与信道分配策略有何关联? 2. 当噪声超过阈值时采用固定值100的物理意义是什么?如何校准该惩罚值? 3. OBSS的三段距离权重(`near/mid/far_range_weightage`)通常如何设置? 4. 空间复用协议(SRP)支持检测`acs_srp_supported[k]`的实现原理是什么? 5. 信道间隔因子`effchfactor`采用右移而非除法有何性能优势?[^2]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值