[失败] 网易云音乐爬虫分析
網(wǎng)易云音樂js破解分析
大家好,我是W
最近在搞畢設相關的材料,所以很久沒有敲代碼和寫博客了。剛好,一個同學有個需求,要獲取網(wǎng)易云音樂的歌曲id和封面地址,然后用外鏈播放。相當于在他的系統(tǒng)里加一個小功能,錦上添花。所以來找到我,剛開始我覺得蠻簡單的,所以就應了。沒想到越是分析覺得越難搞,今天就來將整個過程寫下來。
失敗了,這篇文就當做是記錄吧,沒興趣的大家可以不看。
項目時間:2020年5月6日
網(wǎng)站分析
以為很簡單
如果是一般的圖書網(wǎng)站什么的,采集一本書的ID簡直是輕輕松松,所以我一開始覺得網(wǎng)易云應該也不難。
訪問網(wǎng)易云音樂首頁https://music.163.com/,打開elements隨便點進一個歌單,選擇一首歌,發(fā)現(xiàn)歌曲的id、名稱、時長、歌手、專輯全都有,好像不是什么大問題。
ok,直接請求一下試試。
import requestsurl = "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傳輸來的數(shù)據(jù),因為現(xiàn)在一般都是jax來傳數(shù)據(jù)的。在network里選擇xhr,一個個文件點擊看看有沒有頁面顯示的數(shù)據(jù),果然在一個文件里找到了。
那么直接請求文件的鏈接行不行呢?在headers里找到request url直接請求試試看。可是剛要嘗試,就發(fā)現(xiàn)url里沒有歌曲的信息啊,既沒有id,也沒有歌名,那就意味著這個鏈接這樣是沒有辦法定位歌曲的。
是不是帶了參數(shù)?選項卡往下翻,果然在下方找到了參數(shù):
看到這兩個參數(shù)我有一種不祥的預感,因為參數(shù)里有encSecKey,這明顯是一個加密后的參數(shù),不管了先復制一下參數(shù)請求試試看。
import requestsparams = {"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代碼里看看放數(shù)據(jù)的地方。
在上圖可以看到,只要是文字相關的地方都是類似這種${escape(x.user.nickname)},這好像struts的s標簽之類的東西,雖然不確定但是應該可以確定網(wǎng)易云的流程是:PC網(wǎng)頁接收到關鍵字春秋,點擊搜索后前段js運算params和encSecKey,然后向服務器發(fā)送請求,接下來服務器判斷參數(shù)是否正確,錯誤則返回空,正確則返回json串,然后由前端技術進行渲染。
ok,既然大概猜到數(shù)據(jù)流程了我就知道只需要破解了前端js加密的算法就可以請求到json串了,也就可以拿到數(shù)據(jù)了。
JS破解
定位JS文件
既然知道請求需要參數(shù),而且參數(shù)由js文件產生,那么我們只需要用參數(shù)去搜索JS文件就可以了。
選項卡勾選JS,然后ctrl+f查找encSecKey和params。可以看到有兩個文件。
但是看名字應該就可以分辨出來vipcashier.umd.js跟加密應該沒什么關系,所以我們重點看core_753952a5677cb6fdfb9d7d26a65b3ee7.js把。
補充
原來在找到json文件后可以直接定位文件,看下圖。
翻JS文件
直接點擊JS選項卡,然后找到core_753952a5677cb6fdfb9d7d26a65b3ee7.js,然后找到url直接請求,這樣就可以得到js文件了,當然還需要找個網(wǎng)站把它格式化一下。
整個文件格式化下來4w6千行,查一下encSecKey和params關鍵字吧。經過關鍵字查詢,params有37個match,而encSecKey只有3個match,顯然還是從少的入手。而且不管js的處理過程如何,最后肯定是返回一個類似字典的數(shù)據(jù),里面包含了encSecKey和params。
經過一通尋找,找到了這一段:
var bYf8X = window.asrsea(JSON.stringify(i9b), bqR9I(["流淚", "強"]), bqR9I(QM2x.md), bqR9I(["愛心", "女孩", "驚恐", "大笑"]));e9f.data = k9b.cy0x({params: bYf8X.encText,encSecKey: bYf8X.encSecKey})encSecKey和params都存在,那么e9f.data這個對象很大概率就是加密的最終結果。為了再印證一下,找到這個函數(shù)。
在從控制臺中查找是否返還的數(shù)據(jù)用到了這個函數(shù),
所以現(xiàn)在基本可以確定函數(shù)了。接下來擺在我面前的有兩條路,一個是徹底搞清楚js的流程,然后可以仿照流程寫一個加密算法來得到參數(shù);第二個是找找有什么漏可以鉆。
第一條,模擬js流程得到參數(shù)
其實不管哪一條還是要大致找找代碼的流程,就從最后的對象e9f.data開始找吧。這里只需要一步步的倒回去就可以找到對象的生產流程了。
1、 e9f.data的params由window.asrsea方法生成。
2、 window.asrsea需要四個參數(shù)組成,其中3個是寫死的,一個是JSON.stringify(i9b),所以會變的只有第一個參數(shù)。
// window.asrsea傳入四個參數(shù),其中后三個是寫死的,只不過由一個函數(shù)經過處理然后再丟給asrsea再處理 // window.asrsea的第一個參數(shù)是會變得,我們的重點應該在這里 var bYf8X = window.asrsea(JSON.stringify(i9b), bqR9I(["流淚", "強"]), bqR9I(QM2x.md), bqR9I(["愛心", "女孩", "驚恐", "大笑"]));3、 既然知道bqR9I這個函數(shù)就是對列表或者說數(shù)組進行加密處理,那就不用太管他了,我們把精力集中在JSON.stringify(i9b)上,先上網(wǎng)查一下這個函數(shù)時做什么的,好像不是網(wǎng)易自己寫的。
JSON.stringify()的作用是將 JavaScript 對象轉換為 JSON 字符串4、 ok,既然stringify是將js對象轉為json串,那i9b就是一個對象了,接下來看對象怎么生成的。在pycharm里查找,發(fā)現(xiàn)有400+個match,瞬間頭大,但是在函數(shù)的開頭發(fā)現(xiàn)其實這個對象好像一開始就定義了并且是一個空字典。意思是說全文其他地方的i9b無非就是命名重復,這好讓大家找起來麻煩頭痛而已。
5、 但是接下來就麻煩了,因為js加密破解的難點就在于目標會對js函數(shù)進行模糊處理,你根本不知道每一個函數(shù)的作用,要搞清楚估計要個小本子一一排查記下來每個函數(shù)的流程、作用、參數(shù)等等。這玩意兒就跟破譯戰(zhàn)時電報一樣,沒有密碼本很難搞。雖然知道下面的代碼就是一堆判斷,然后根據(jù)結果往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這個函數(shù),看看它是怎么做的,就當學習吧。全文查找window.asrsea發(fā)現(xiàn)match不多,很輕松就找到在文件頭部有一些abcd函數(shù)。
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函數(shù) 而d函數(shù)在上邊也有定義 window.ecnonasr = e7、 那就看d函數(shù),需要四個參數(shù)(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函數(shù)返回的值,且是固定值return h.encText = b(d, g), // d函數(shù)會返回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函數(shù),首先看a函數(shù)。a函數(shù)的參數(shù)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個數(shù)字或字母 }9、 看b函數(shù),在d函數(shù)調用處,傳過來的參數(shù)就是(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加密算法的結果,傳的參無非就是上面產的參數(shù),還有一個mode, f = CryptoJS.AES.encrypt(e, c, {iv: d,mode: CryptoJS.mode.CBC});return f.toString() }10、 看c函數(shù),(a, b, c)在d中的調用是c(i, e, f),而i又是16位的字符串,e是d函數(shù)的第二個參數(shù),也是一個固定字符串數(shù)組。f也是固定字符串數(shù)組。
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所需要的參數(shù),也就是我們最終的參數(shù)了。
function encryptedString(a, b) {// 定義了一堆變量,其中c是新數(shù)組,d=16,e=0for (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) }這個函數(shù)有點難度,我實在是找不到chunksize的意思,所以如果有朋友了解的話可以考慮模擬js的整套過程,畢竟只差這一步就可以完整模擬了,如果不行也可以試試直接拷貝這個函數(shù)調用js來完成。那么接下來我們考慮能不能取巧。
第二條,取巧
經過上面的分析,我們知道window.asrsea = d,并且經過翻d的源碼過后發(fā)現(xiàn),d函數(shù)直接返回了encText和encSecKey,那顯然這就是最后一步了,加上d函數(shù)的后三個參數(shù)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函數(shù)中顯然也調用了其他函數(shù),可是那又關我們什么事呢?我們只需要控制好d的參數(shù)格式,最終一定會返回我么正確的數(shù)據(jù)的。所以接下來的重心就需要偏移到JSON.stringify(i9b)到底是什么的問題上了,只要解決了這個問題我們就可以按照格式請求了。
百度可以知道JSON.stringify()的作用是將 JavaScript 對象轉換為 JSON 字符串,那顯然i9b就是js的對象,然后d函數(shù)的d參數(shù)就是一個json串。那么到底藏了什么參數(shù)呢?
失敗
當分析到這里的時候用fiddler來替換文件,發(fā)現(xiàn)怎么也找不到ajax文件,之前實驗的時候還是可以的,但是這次網(wǎng)易云音樂js文件名換了。我看著好像沒發(fā)現(xiàn)什么問題,但是打印log的時候怎么也找不到ajax了,可能還需要再搞搞。這篇就這樣吧,下次有空的時候再研究研究,第一次玩js破解就失敗了。。
總結
以上是生活随笔為你收集整理的[失败] 网易云音乐爬虫分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天猫精灵连接蓝牙摸索4 STM32单片机
- 下一篇: 位运算实现乘法运算