[失败] 网易云音乐爬虫分析

网易云音乐js破解分析

大家好,我是W

最近在搞毕设相关的材料,所以很久没有敲代码和写博客了。刚好,一个同学有个需求,要获取网易云音乐的歌曲id和封面地址,然后用外链播放。相当于在他的系统里加一个小功能,锦上添花。所以来找到我,刚开始我觉得蛮简单的,所以就应了。没想到越是分析觉得越难搞,今天就来将整个过程写下来。

失败了,这篇文就当做是记录吧,没兴趣的大家可以不看。

项目时间:2020年5月6日

网站分析

以为很简单

如果是一般的图书网站什么的,采集一本书的ID简直是轻轻松松,所以我一开始觉得网易云应该也不难。

访问网易云音乐首页https://music.163.com/,打开elements随便点进一个歌单,选择一首歌,发现歌曲的id、名称、时长、歌手、专辑全都有,好像不是什么大问题。

ok,直接请求一下试试。

import requests

url = "https://music.163.com/#/search/m/?id=4924225474&s=%E6%98%A5%E7%A7%8B&type=1"

headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
}

response = requests.get(url=url, headers=headers)
print(response.status_code)
print(response.text)
print(response.content)

好像这就请求成功了,找找id在哪里先!在浏览器找到需要id的位置,可以看到每首歌的id格式都藏在href里,直接搜索href="/song?id=188550"就可以了。

在这里插入图片描述

怎么会没找到呢?直接右键查看源码看看,还是没有。

在这里插入图片描述

尝试ajax

再回头查看network里,看看是不是ajax传输来的数据,因为现在一般都是jax来传数据的。在network里选择xhr,一个个文件点击看看有没有页面显示的数据,果然在一个文件里找到了。

在这里插入图片描述

那么直接请求文件的链接行不行呢?在headers里找到request url直接请求试试看。可是刚要尝试,就发现url里没有歌曲的信息啊,既没有id,也没有歌名,那就意味着这个链接这样是没有办法定位歌曲的。

是不是带了参数?选项卡往下翻,果然在下方找到了参数:

在这里插入图片描述

看到这两个参数我有一种不祥的预感,因为参数里有encSecKey,这明显是一个加密后的参数,不管了先复制一下参数请求试试看。

import requests

params = {
    "params": "Kw1PmEZYdbHSrLLG43T24+mzCQ+3FYS1ociCIw4pZFH8pf0kbyKgf7t+nR9d2jkjEr8t6QkMycPA/Uuywj5nN5FyPpSofDP/JyxEXHUZ16XNUtNE01Q+a5lDmNW30xzoOoNhB8tnGaPwO2kdDJmRa/TIJM6AEpH+zCcZRPHvxgcajiUzAoxool1iwvcEY57v",
    "encSecKey": "862bcb583e6ba29abf5ec8793b2fda98ded0ce2c375d2f0bfac324de2738f9ba628c056191a5671f56e398ea07ac3e73b0c09357734bdd8ea31b68bc42a8ba3aacc4f338f0e8369a372841efb9ccea846a064ac2df8c8bcb47b5f015b6c6980ecdc5b2da659076429ddf6aafe240d33918b81a33e633a43527e9037486dd82d9",
}

url = "https://music.163.com/weapi/cloudsearch/get/web?csrf_token="

headers = {
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
}

response = requests.get(url=url, params=params, headers=headers)
print(response.status_code)
print(response.text)
print(response.content)

居然可以请求成功,可是一看控制台,什么都没有打印,看来是被服务器拦截了。直到这里我基本可以确定是js加密和渲染了。为了进一步验证,我要在html代码里看看放数据的地方。

在这里插入图片描述

在上图可以看到,只要是文字相关的地方都是类似这种${escape(x.user.nickname)},这好像struts的s标签之类的东西,虽然不确定但是应该可以确定网易云的流程是:PC网页接收到关键字春秋,点击搜索后前段js运算paramsencSecKey,然后向服务器发送请求,接下来服务器判断参数是否正确,错误则返回空,正确则返回json串,然后由前端技术进行渲染。

ok,既然大概猜到数据流程了我就知道只需要破解了前端js加密的算法就可以请求到json串了,也就可以拿到数据了。

JS破解

定位JS文件

既然知道请求需要参数,而且参数由js文件产生,那么我们只需要用参数去搜索JS文件就可以了。

选项卡勾选JS,然后ctrl+f查找encSecKeyparams。可以看到有两个文件。

在这里插入图片描述

在这里插入图片描述

但是看名字应该就可以分辨出来vipcashier.umd.js跟加密应该没什么关系,所以我们重点看core_753952a5677cb6fdfb9d7d26a65b3ee7.js把。

补充

原来在找到json文件后可以直接定位文件,看下图。

在这里插入图片描述

在这里插入图片描述

翻JS文件

直接点击JS选项卡,然后找到core_753952a5677cb6fdfb9d7d26a65b3ee7.js,然后找到url直接请求,这样就可以得到js文件了,当然还需要找个网站把它格式化一下。

整个文件格式化下来4w6千行,查一下encSecKeyparams关键字吧。经过关键字查询,params有37个match,而encSecKey只有3个match,显然还是从少的入手。而且不管js的处理过程如何,最后肯定是返回一个类似字典的数据,里面包含了encSecKeyparams

经过一通寻找,找到了这一段:

var bYf8X = window.asrsea(JSON.stringify(i9b), bqR9I(["流泪", "强"]), bqR9I(QM2x.md), bqR9I(["爱心", "女孩", "惊恐", "大笑"]));
        e9f.data = k9b.cy0x({
            params: bYf8X.encText,
            encSecKey: bYf8X.encSecKey
        })

encSecKeyparams都存在,那么e9f.data这个对象很大概率就是加密的最终结果。为了再印证一下,找到这个函数。

在这里插入图片描述

在从控制台中查找是否返还的数据用到了这个函数,

在这里插入图片描述

所以现在基本可以确定函数了。接下来摆在我面前的有两条路,一个是彻底搞清楚js的流程,然后可以仿照流程写一个加密算法来得到参数;第二个是找找有什么漏可以钻。

第一条,模拟js流程得到参数

其实不管哪一条还是要大致找找代码的流程,就从最后的对象e9f.data开始找吧。这里只需要一步步的倒回去就可以找到对象的生产流程了。

1、 e9f.dataparamswindow.asrsea方法生成。

2、 window.asrsea需要四个参数组成,其中3个是写死的,一个是JSON.stringify(i9b),所以会变的只有第一个参数。

// window.asrsea传入四个参数,其中后三个是写死的,只不过由一个函数经过处理然后再丢给asrsea再处理
// window.asrsea的第一个参数是会变得,我们的重点应该在这里
var bYf8X = window.asrsea(JSON.stringify(i9b), bqR9I(["流泪", "强"]), bqR9I(QM2x.md), bqR9I(["爱心", "女孩", "惊恐", "大笑"]));

3、 既然知道bqR9I这个函数就是对列表或者说数组进行加密处理,那就不用太管他了,我们把精力集中在JSON.stringify(i9b)上,先上网查一下这个函数时做什么的,好像不是网易自己写的。

JSON.stringify()的作用是将 JavaScript 对象转换为 JSON 字符串

4、 ok,既然stringify是将js对象转为json串,那i9b就是一个对象了,接下来看对象怎么生成的。在pycharm里查找,发现有400+个match,瞬间头大,但是在函数的开头发现其实这个对象好像一开始就定义了并且是一个空字典。意思是说全文其他地方的i9b无非就是命名重复,这好让大家找起来麻烦头痛而已。

在这里插入图片描述

5、 但是接下来就麻烦了,因为js加密破解的难点就在于目标会对js函数进行模糊处理,你根本不知道每一个函数的作用,要搞清楚估计要个小本子一一排查记下来每个函数的流程、作用、参数等等。这玩意儿就跟破译战时电报一样,没有密码本很难搞。虽然知道下面的代码就是一堆判断,然后根据结果往i9b里塞东西,但是涉及的东西很多。

v9m.bg0x = function(Z0x, e9f) { 
    var i9b = {} // 空字典
      , e9f = NEJ.X({}, e9f)
      , mr3x = Z0x.indexOf("?");

	// 经过一堆判断来往i9b里塞东西
    if (window.GEnc && /(^|\.com)\/api/.test(Z0x) && !(e9f.headers && e9f.headers[es1x.Av7o] == es1x.Ha9R) && !e9f.noEnc) {
        if (mr3x != -1) {
            i9b = k9b.gW2x(Z0x.substring(mr3x + 1));
            Z0x = Z0x.substring(0, mr3x)
        }
        if (e9f.query) {
            i9b = NEJ.X(i9b, k9b.fU1x(e9f.query) ? k9b.gW2x(e9f.query) : e9f.query)
        }
        if (e9f.data) {
            i9b = NEJ.X(i9b, k9b.fU1x(e9f.data) ? k9b.gW2x(e9f.data) : e9f.data)
        }
        i9b["csrf_token"] = v9m.gM2x("__csrf");
        Z0x = Z0x.replace("api", "weapi");
        e9f.method = "post";
        delete e9f.query;

6、 先不管他了,先看看window.asrsea这个函数,看看它是怎么做的,就当学习吧。全文查找window.asrsea发现match不多,很轻松就找到在文件头部有一些abcd函数。

 function d(d, e, f, g) {
    var h = {}
      , i = a(16);
    return h.encText = b(d, g),
    h.encText = b(h.encText, i),
    h.encSecKey = c(i, e, f),
    h
}
function e(a, b, d, e) {
    var f = {};
    return f.encText = c(a + e, b, d),
    f
}
window.asrsea = d, // 所以window.asrsea == d函数 而d函数在上边也有定义
window.ecnonasr = e

7、 那就看d函数,需要四个参数(d, e, f, g),显然就对应了下面的(JSON.stringify(i9b), bqR9I(["流泪", "强"]), bqR9I(QM2x.md), bqR9I(["爱心", "女孩", "惊恐", "大笑"])),后三个是死的,第一个是活的。

function d(d, e, f, g) {  //对应(JSON.stringify(i9b), bqR9I(["流泪", "强"]), bqR9I(QM2x.md), bqR9I(["爱心", "女孩", "惊恐", "大笑"]))
    var h = {} //空字典
      , i = a(16); // i是a函数返回的值,且是固定值
    return h.encText = b(d, g), // d函数会返回h,前面的操作都是对h字典里的两个字段进行加密(逗号运算符)
    h.encText = b(h.encText, i),
    h.encSecKey = c(i, e, f),
    h
}

// 所以h = {"encText":b(b(d, g), i),"encSecKey":c(i, e, f)}

8、 所以还需要看abc函数,首先看a函数。a函数的参数a固定为16(起码在d调用时)

function a(a) {
	// deb先固定为一个串 c为空串
    var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
	// 这个for从上边的串中随机出16字符出来,既然是随机的 那就没什么好讲的了
    for (d = 0; a > d; d += 1)
        e = Math.random() * b.length,
        e = Math.floor(e),
        c += b.charAt(e);
    return c // 最终返还16个数字或字母
}

9、 看b函数,在d函数调用处,传过来的参数就是(JSON.stringify(i9b),bqR9I(["爱心", "女孩", "惊恐", "大笑"])),其中b是固定的,a是变化的。

function b(a, b) {
	// c、d变量都不用看 都是直接调用 并且传参都是固定的 返回的值也固定
    var c = CryptoJS.enc.Utf8.parse(b)
      , d = CryptoJS.enc.Utf8.parse("0102030405060708")
		// 这里可以看到加密a的算法 返回来的值付给了e
      , e = CryptoJS.enc.Utf8.parse(a)
		// f也是调用AES加密算法的结果,传的参无非就是上面产的参数,还有一个mode
      , f = CryptoJS.AES.encrypt(e, c, {
        iv: d,
        mode: CryptoJS.mode.CBC
    });
    return f.toString()
}

10、 看c函数,(a, b, c)在d中的调用是c(i, e, f),而i又是16位的字符串,e是d函数的第二个参数,也是一个固定字符串数组。f也是固定字符串数组。

function c(a, b, c) {
    var d, e;
    return setMaxDigits(131), // 在new RSA对象之前需要先调用该方法,用途是设置key的长度
    d = new RSAKeyPair(b,"",c), // new 加密对象
    e = encryptedString(d, a) // 调用自己写的加密算法来加密,然后返回e
}

11、 可以看到这里又来了一个encryptedString,显然调用完这个算法后就可以直接返回给d所需要的参数,也就是我们最终的参数了。

function encryptedString(a, b) {
	// 定义了一堆变量,其中c是新数组,d=16,e=0
    for (var f, g, h, i, j, k, l, c = new Array, d = b.length, e = 0; d > e; )
		// 然后c从index=0开始到15,逐一存放b串的unicode编码
        c[e] = b.charCodeAt(e),
        e++;

    for (; 0 != c.length % a.chunkSize; )
        c[e++] = 0;
    for (f = c.length,
    g = "",
    e = 0; f > e; e += a.chunkSize) {
        for (j = new BigInt,
        h = 0,
        i = e; i < e + a.chunkSize; ++h)
            j.digits[h] = c[i++],
            j.digits[h] += c[i++] << 8;
        k = a.barrett.powMod(j, a.e),
        l = 16 == a.radix ? biToHex(k) : biToString(k, a.radix),
        g += l + " "
    }
    return g.substring(0, g.length - 1)
}

这个函数有点难度,我实在是找不到chunksize的意思,所以如果有朋友了解的话可以考虑模拟js的整套过程,毕竟只差这一步就可以完整模拟了,如果不行也可以试试直接拷贝这个函数调用js来完成。那么接下来我们考虑能不能取巧。

第二条,取巧

经过上面的分析,我们知道window.asrsea = d,并且经过翻d的源码过后发现,d函数直接返回了encTextencSecKey,那显然这就是最后一步了,加上d函数的后三个参数e, f, g都是固定的字符串列表,变化的只有一个d。

function d(d, e, f, g) {
    var h = {}
      , i = a(16);
    return h.encText = b(d, g),
    h.encText = b(h.encText, i),
    h.encSecKey = c(i, e, f),
    h
}

在d函数中显然也调用了其他函数,可是那又关我们什么事呢?我们只需要控制好d的参数格式,最终一定会返回我么正确的数据的。所以接下来的重心就需要偏移到JSON.stringify(i9b)到底是什么的问题上了,只要解决了这个问题我们就可以按照格式请求了。

百度可以知道JSON.stringify()的作用是将 JavaScript 对象转换为 JSON 字符串,那显然i9b就是js的对象,然后d函数的d参数就是一个json串。那么到底藏了什么参数呢?

失败

当分析到这里的时候用fiddler来替换文件,发现怎么也找不到ajax文件,之前实验的时候还是可以的,但是这次网易云音乐js文件名换了。我看着好像没发现什么问题,但是打印log的时候怎么也找不到ajax了,可能还需要再搞搞。这篇就这样吧,下次有空的时候再研究研究,第一次玩js破解就失败了。。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值