网易云音乐Web端加密参数逆向:AES与RSA混合加密实战解析

1. 项目概述:从一次失败的请求说起

那天,我像往常一样,想写个小脚本把我网易云音乐歌单里的歌名和歌手信息扒下来,做个本地备份。用开发者工具一看,一个简单的搜索请求, POST 数据里赫然躺着三个让人头大的参数: csrf_token params encSecKey 。直接复制 curl 命令?秒速返回 -460 错误码。这就是网易云音乐 Web 端核心的加密防线,也是无数爬虫爱好者和前端安全研究者绕不开的“必修课”。这三个参数,特别是后两者,构成了一个典型的非对称与对称加密混合的客户端加密方案,目的很明确:防止请求被轻易模拟和篡改,保护核心业务接口。

如果你也遇到过类似问题,或者对前端 JavaScript 逆向、加密算法实战感兴趣,那么这次对网易云音乐登录、搜索、评论等核心接口的 csrf params encSecKey 算法逆向详解,就是为你准备的。我将带你深入 JavaScript 代码丛林,一步步定位关键加密函数,还原其完整的生成逻辑。这不仅是一次爬虫技巧的分享,更是一次对现代 Web 应用前端安全机制的深度剖析。无论你是想理解其原理,还是需要在自己的项目中实现模拟请求,这篇文章都将提供清晰的路径和可复现的代码。

2. 核心加密机制总览与逆向思路

在动手逆向之前,我们必须先理解我们要破解的究竟是什么。网易云音乐 Web 端( music.163.com )对关键接口的请求体进行了整体加密,其结构通常如下:

{
  "params": "C7B...(很长的一段密文)",
  "encSecKey": "84B...(很长的一段密文)",
  "csrf_token": "1234567890abcdef"
}

这三个参数各有其职:

  • csrf_token : 相对简单,通常与用户会话绑定,可以从 Cookie 或页面 HTML 中提取,主要用于防御跨站请求伪造。它的逆向不是重点,重点是获取它的方式。
  • params : 这是核心的业务参数经过加密后的结果。比如你搜索“周杰伦”,那么 {“s”: “周杰伦”, “type”: 1} 这个对象就会被加密成 params 的值。它使用的是 AES-128-CBC 对称加密算法。
  • encSecKey : 这是解密 params 所需密钥的密文。因为 AES 加密需要密钥,但这个密钥不能明文传输。所以网易云采用 RSA 非对称加密,将 AES 密钥用公钥加密后,作为 encSecKey 发送。服务器用私钥解密 encSecKey 得到 AES 密钥,再用这个 AES 密钥解密 params 得到原始业务参数。

所以,整个流程是: 客户端生成一个随机的 AES 密钥 -> 用这个 AES 密钥加密业务参数得到 params -> 用固定的 RSA 公钥加密这个 AES 密钥得到 encSecKey -> 将 params encSecKey 发送给服务器。

我们的逆向目标,就是在 JavaScript 代码中找到: 生成随机 AES 密钥的函数、执行 AES 加密的函数、以及执行 RSA 加密的函数和那个固定的公钥。

注意 :网易云音乐的加密逻辑可能会随着版本更新而改变,本文分析的逻辑基于一个较长时期内稳定的版本。逆向的核心思路是通用的,但具体函数名和密钥需要你根据当时的实际情况进行定位。

2.1 逆向工具与前期准备

工欲善其事,必先利其器。进行 JS 逆向,你需要以下工具:

  1. 浏览器开发者工具 :Chrome 或 Edge 的 DevTools 是主战场。重点关注 Sources (源码)和 Network (网络)面板。
  2. 代码搜索与格式化 :在 Sources 面板中,对于混淆的、压缩成一行的 JS 文件,可以使用 {} (Pretty-print)按钮进行格式化,让代码可读。
  3. 断点调试 :这是逆向的灵魂。通过 XHR/fetch Breakpoints (在 Sources 面板的 XHR/fetch Breakpoints 区域添加包含特定 URL 关键词的断点),可以在发起目标网络请求时自动暂停 JavaScript 执行,从而定位到加密发生的代码位置。
  4. Node.js 环境 :用于在本地运行和测试我们还原出来的加密算法。确保已安装 Node.js。

逆向心法 :不要试图去理解每一行混淆后的代码。我们的策略是“顺藤摸瓜”,从网络请求的发起处(如 fetch XMLHttpRequest.send )设置断点,一步步回溯,找到加密参数被赋值的地方,再向上追溯加密函数的调用。

3. 定位关键加密函数与参数生成逻辑

打开网易云音乐网页,进入任意页面(如首页)。打开开发者工具的 Network 面板,勾选 Preserve log ,然后进行一次搜索操作。

在 Network 面板中,找到类型为 xhr fetch 的请求,其请求 URL 可能包含 weapi 字样(如 /weapi/search/get )。点击这个请求,查看其 Headers Payload 。确认它的请求体正是我们关注的 params , encSecKey , csrf_token 格式。

接下来是关键步骤:

  1. 设置全局断点 :在 Sources 面板,找到 XHR/fetch Breakpoints ,点击 + 号,添加一个包含 weapi 关键词的断点。这样,任何发起包含 weapi 的请求的代码执行时,都会自动暂停。
  2. 触发断点 :在网页上再次执行搜索操作。此时 JavaScript 执行会立即暂停。
  3. 调用栈分析 :查看右侧的 Call Stack (调用栈)。这里显示了从断点处往回的函数调用链。我们需要在调用栈中寻找看起来与加密相关的函数名,或者寻找我们的目标参数( params , encSecKey )被赋值的时刻。
  4. 回溯与搜索 :在调用栈中,逐个点击上层的函数,观察其代码。通常,你会看到一个函数,它接收一个明文对象(如 {s: “周杰伦”} ),然后经过一些处理,最终生成了 params encSecKey 。这个函数很可能被命名为 window.asrsea 或类似的名字(经过混淆,名字可能不同,但逻辑一致)。

另一种高效的方法是直接在全站加载的 JS 文件中搜索关键词。在 Sources 面板中,按 Ctrl+Shift+F 进行全局搜索。可以尝试搜索 encSecKey params AES RSA CBC 等。通常,你会找到一个核心的、被大量引用的加密函数定义。

以我最近一次分析为例,核心加密函数是一个名为 window.asrsea 的函数(函数名可能变化,但功能一致)。它的结构非常清晰:

function d(a, b, c, e) { // 这个函数往往被命名为 asrsea
    // a: 明文业务参数 (JSON字符串)
    // b: 固定值,与加密算法有关
    // c: 固定值,通常是 RSA 公钥的模数 (n)
    // e: 固定值,通常是 RSA 公钥的指数 (e)
    var f, g, h = {}, i = []; // i 是填充数组
    // ... 省略部分混淆代码 ...
    f = e(a); // e 是一个函数,用于生成16字节随机字符串作为 AES 密钥
    g = d(f, b); // d 是一个函数,用 AES-CBC 加密数据a,密钥为f,得到params
    h.encText = g; // 这就是 params
    h.encText = b(g, f); // 注意:这里可能有混淆,实际是 h.encSecKey = ...
    // 更常见的逻辑是:
    // h.encText = g; // params
    // h.encSecKey = r(f, c, e); // r 是一个函数,用 RSA 公钥(c, e)加密 AES 密钥(f)
    return h;
}

通过断点调试,我们可以确认传入这个函数的四个参数 (a, b, c, e) 中:

  • a 是我们要加密的业务数据(JSON 字符串)。
  • b 是一个固定字符串,如 "010001"
  • c 是一个很长的固定16进制字符串,即 RSA 公钥的模数 n
  • e 是一个固定字符串,如 "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"

而函数内部, e(a) (注意这个 e 是传入的第四个参数,与函数内的变量 e 不同,这是混淆的常见手法)这个调用返回了一个16字节的随机字符串,它就是 AES 密钥。随后,这个密钥被用于加密 a 得到 params ,其自身又被 RSA 公钥 (c, b) 加密得到 encSecKey

4. 算法还原与本地 Node.js 实现

定位到核心函数后,我们需要将其逻辑用清晰的、可维护的 Node.js 代码还原出来。关键在于识别出内部的 AES 和 RSA 函数,并找到它们对应的标准库实现。

4.1 提取关键常量与函数

首先,我们从 JS 代码中提取出固定的常量。这些通常是写死在代码里的:

  1. RSA 公钥的模数 n (对应参数 c ) :一个很长的16进制字符串。
  2. RSA 公钥的指数 e (对应参数 b ) :通常是 "010001" (十进制 65537,这是 RSA 最常用的公钥指数)。
  3. AES 加密的初始向量 iv :通常是 "0102030405060708" 的16进制表示,或者全零 "0000000000000000" ,需要在代码中确认。
  4. AES 加密模式与填充 :确认是 AES-128-CBC 模式,填充方式为 PKCS#7

4.2 还原 AES 加密逻辑

在核心函数内部,用于加密业务参数 a 的函数(上面伪代码中的 d(f, b) ,但实际参数顺序可能不同)就是 AES 加密。在 Node.js 中,我们可以使用 crypto 标准库来实现。

关键点

  • 密钥 :是上面生成的16字节随机字符串。
  • 明文 :需要加密的原始业务参数字符串(如 {"s":"周杰伦","type":1} )。
  • 模式 aes-128-cbc
  • IV :固定值,需要从源码中确认。
  • 输入输出编码 JS 端处理的是二进制数据,但常以16进制字符串形式传递。我们需要确保在 Node.js 中,密钥、IV、明文都以 Buffer 类型处理。

一个典型的 AES 加密函数还原如下:

const crypto = require('crypto');

function aesEncrypt(text, key, iv) {
    // text: 明文字符串
    // key: 16字节的密钥字符串
    // iv: 16字节的初始化向量字符串
    const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    // 网易云音乐通常会将结果转换为大写,需要确认
    return encrypted.toUpperCase();
}

4.3 还原 RSA 加密逻辑

用于加密 AES 密钥的函数是 RSA 加密。在 JS 源码中,它可能是一个自定义的大整数运算函数,因为 n 非常大(2048位或以上),超出了 JavaScript 原生整数的表示范围。在 Node.js 中,我们同样可以使用 crypto 库,或者使用 node-rsa 等库。但为了精确还原,我们需要实现和 JS 端一模一样的“教科书式” RSA 加密。

网易云音乐使用的 RSA 加密是 “无填充” 的。这意味着它直接计算 (密钥 ^ e) mod n 。由于密钥是16字节(128位),而 n 是2048位,所以直接计算是可行的。

我们可以使用 BigInt 类型来处理大整数运算:

function rsaEncrypt(key, pubKeyN, pubKeyE) {
    // key: 16字节的随机字符串(AES密钥)
    // pubKeyN: RSA 模数 n (16进制字符串)
    // pubKeyE: RSA 指数 e (16进制字符串),通常是 "010001"
    
    // 1. 将 AES 密钥(文本)反转(这是网易云的一个特定步骤,务必确认!)
    // 例如,在 JS 源码中可能看到:key = key.split('').reverse().join('');
    const reversedKey = key.split('').reverse().join('');
    
    // 2. 将反转后的密钥转换为16进制
    const hexKey = Buffer.from(reversedKey, 'utf8').toString('hex');
    
    // 3. 进行 RSA 加密: (text ^ e) mod n
    // 这里 text 是 hexKey 表示的 BigInt
    const biText = BigInt('0x' + hexKey);
    const biE = BigInt('0x' + pubKeyE);
    const biN = BigInt('0x' + pubKeyN);
    
    const biResult = biText ** biE % biN; // 使用 BigInt 运算
    
    // 4. 将结果转换为16进制字符串,并填充至指定长度(通常是256字节,512个16进制字符)
    let result = biResult.toString(16);
    // 补前导零
    while (result.length < 512) {
        result = '0' + result;
    }
    return result;
}

实操心得 key 的反转操作是网易云音乐加密中的一个经典特征,也是早期很多逆向分析容易忽略导致失败的点。务必通过断点调试,对比加密前的 key 和传入 RSA 函数时的 key ,确认是否有此步骤。此外,最终 encSecKey 的长度是固定的(512位16进制字符),如果长度不对,服务器会拒绝请求。

4.4 整合与 csrf_token 获取

最后,我们将所有步骤整合,并解决 csrf_token 的问题。

const crypto = require('crypto');

// 从 JS 源码中提取的固定值(示例,需根据实际情况替换)
const PUB_KEY_N = ‘00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7’;
const PUB_KEY_E = ‘010001’;
const AES_IV = ‘0102030405060708’; // 也可能是 ‘0000000000000000’

function getRandomString(length) {
    // 生成指定长度的随机字符串,作为 AES 密钥
    const chars = ‘abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789’;
    let result = ‘’;
    for (let i = 0; i < length; i++) {
        result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
}

function aesEncrypt(text, key, iv = AES_IV) {
    const cipher = crypto.createCipheriv(‘aes-128-cbc’, Buffer.from(key, ‘utf8’), Buffer.from(iv, ‘utf8’));
    let encrypted = cipher.update(text, ‘utf8’, ‘hex’);
    encrypted += cipher.final(‘hex’);
    return encrypted.toUpperCase();
}

function rsaEncrypt(key, n = PUB_KEY_N, e = PUB_KEY_E) {
    // 关键:反转密钥
    const reversedKey = key.split(‘’).reverse().join(‘’);
    const hexKey = Buffer.from(reversedKey, ‘utf8’).toString(‘hex’);
    
    const biText = BigInt(‘0x’ + hexKey);
    const biE = BigInt(‘0x’ + e);
    const biN = BigInt(‘0x’ + n);
    
    const biResult = biText ** biE % biN;
    
    let result = biResult.toString(16);
    while (result.length < 512) {
        result = ‘0’ + result;
    }
    return result;
}

function generateEncryptedParams(plainData) {
    // plainData: 明文的业务参数对象,如 {s: ‘周杰伦’, type: 1, limit: 30, offset: 0}
    const text = JSON.stringify(plainData);
    const aesKey = getRandomString(16); // 生成16位随机 AES 密钥
    
    const params = aesEncrypt(text, aesKey); // 加密业务数据
    const encSecKey = rsaEncrypt(aesKey); // 加密 AES 密钥
    
    return { params, encSecKey };
}

// 获取 csrf_token,通常来自 Cookie 中的 ‘__csrf’ 字段
// 在模拟请求时,需要先访问页面获取 Cookie,或从已有会话中提取
function getCsrfTokenFromCookie(cookieString) {
    const match = cookieString.match(/__csrf=([^;]+)/);
    return match ? match[1] : ‘’;
}

// 示例:模拟搜索请求
async function simulateSearch(keyword) {
    // 1. 先获取初始 Cookie 和 csrf_token (这里需要模拟一次页面请求)
    // const { cookie, csrf } = await fetchInitialPage();
    
    // 2. 构造明文参数
    const plainData = {
        s: keyword,
        type: 1, // 1: 单曲
        limit: 30,
        offset: 0
    };
    
    // 3. 生成加密参数
    const encrypted = generateEncryptedParams(plainData);
    
    // 4. 构造最终请求载荷
    const payload = {
        params: encrypted.params,
        encSecKey: encrypted.encSecKey,
        csrf_token: ‘你的csrf_token’, // 替换为实际获取的值
    };
    
    console.log(‘加密后的参数:’, payload);
    // 5. 使用 axios, node-fetch 等库发送 POST 请求
    // const response = await fetch(‘https://music.163.com/weapi/search/get’, {
    //     method: ‘POST’,
    //     headers: { ‘Content-Type’: ‘application/x-www-form-urlencoded’ },
    //     body: new URLSearchParams(payload).toString(),
    //     cookies: …
    // });
}

5. 常见问题、调试技巧与进阶思考

即使按照上述步骤还原了代码,在实际运行中也可能遇到各种问题。这里记录一些常见的坑和调试技巧。

5.1 常见问题排查表

问题现象 可能原因 排查方法
返回 -460 错误码 加密参数不正确,服务器解密失败。 1. 核对 PUB_KEY_N , PUB_KEY_E , AES_IV 是否与当前网页源码一致。
2. 重点检查 AES 密钥是否进行了反转 。在 rsaEncrypt 函数开始处打印 key reversedKey ,与浏览器中断点处的值对比。
3. 检查 params 加密前的明文是否与浏览器完全一致(包括空格、引号格式)。建议将浏览器中的明文直接复制到你的代码中测试。
返回 -2 错误码 通常意味着 csrf_token 无效或缺失。 1. 确认请求头中携带了正确的 Cookie
2. 确认 csrf_token 是从当前会话的 Cookie ( __csrf ) 中正确提取的。
3. 尝试先访问一次主页或登录页,获取有效的 Cookie 和 csrf_token 后再发起请求。
encSecKey 长度不是512位 RSA 加密结果未补零。 确保 rsaEncrypt 函数最后有补零到512个16进制字符(256字节)的逻辑。
params 加密结果与浏览器不一致 AES 加密模式、填充或 IV 错误。 1. 确认使用 aes-128-cbc PKCS7 填充。
2. 确认 IV Buffer.from(‘0102030405060708’, ‘utf8’) 还是 Buffer.from(‘0102030405060708’, ‘hex’) 必须和 JS 端保持一致 。通常 JS 里 IV 是字符串 ’0102030405060708’ ,在 createCipheriv 时需要转换为 Buffer 。用 ‘utf8’ 还是 ‘hex’ 编码,需要看 JS 源码如何处理。断点查看 CryptoJS.enc.Utf8.parse(iv) 或类似语句,可以确定。
请求成功但返回数据乱码或为空 请求头或响应解码问题。 1. 确保 POST 请求的 Content-Type application/x-www-form-urlencoded ,并且 payload 是 URLSearchParams 格式的字符串。
2. 网易云音乐可能对请求头有校验,如 Referer , User-Agent ,需要模拟浏览器设置。

5.2 高级调试技巧

  1. “抠代码”与本地补环境 :如果加密函数依赖了浏览器特有的对象(如 window document )或其他未定义的全局变量,直接复制到 Node.js 会报错。这时需要“补环境”,即在 Node.js 中模拟这些对象。更稳健的做法是只“抠出”最核心的加密函数(通常是纯数学计算),而用 Node.js 的 crypto 库替换掉原函数中识别出的 AES/RSA 部分。
  2. 使用 vm2 jsdom 执行原 JS :对于极其复杂或混淆严重的代码,可以尝试将整个加密函数所在的 JS 文件或代码段,在 Node.js 中用 vm2 沙箱或 jsdom 模拟的浏览器环境中执行。这需要处理大量的依赖和环境变量,难度较大,但能保证 100% 还原。
  3. 关注代码更新 :网易云音乐的加密逻辑并非一成不变。如果某天你的脚本突然全部失效,首先应检查核心加密函数是否还在原位置,其内部的常量( n , e , iv )和算法步骤是否有变化。重新抓包、断点、分析一遍是唯一的办法。

5.3 安全与伦理边界

最后必须强调,逆向工程技术是一把双刃剑。

  • 学习与研究 :本文的目的是为了学习前端安全机制、加密算法应用和 JavaScript 逆向技术,理解大型互联网应用如何保护其接口。
  • 尊重版权与服务条款 :切勿将技术用于大规模爬取受版权保护的音乐内容、干扰网易云音乐正常服务、进行恶意攻击或商业牟利。这违反其服务条款,也可能涉及法律风险。
  • 控制请求频率 :即使在个人学习测试时,也应控制请求频率,避免对对方服务器造成不必要的压力。

技术的乐趣在于探索和理解。通过这次对网易云音乐加密参数的逆向,我们不仅获得了一套可用的模拟请求方法,更重要的是,深入理解了前端加密的常见模式、非对称与对称加密的结合使用,以及如何系统性地进行 JS 逆向分析。这套方法论,完全可以迁移到分析其他具有类似前端加密策略的网站或应用上。当你再遇到 params encSecKey 时,相信你已能从容应对。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值