爱奇艺视频cmd5x解析算法的移植分析和实现Nodejs(2019-08)
目錄
- 愛奇藝視頻cmd5x解析算法的移植分析和實現(2019-08)。
- 什么是cmd5x算法
- 說明
- 大概
- 分析過程
- 首先
- 方案
- 關于調試
- 如何使用chromium的開發者工具對本地導入的代碼進行斷點調試
- 第一次測試
- 模擬環境測試(可選)
- 初看代碼結構
- 反混淆
- 測試反混淆結果
- 為什么這么做?
- 這樣的結果說明了什么?
- 暴力調試
- 怎么調試呢?
- 怎么處理呢?
- 隨便說說
- 最后
- 鏈接
愛奇藝視頻cmd5x解析算法的移植分析和實現(2019-08)。
什么是cmd5x算法
愛奇藝視頻的解析算法cmd5x是dash視頻接口的參數vf的生成算法,實際上這也是一個加了鹽的md5算法。
說明
這不是完整的分析視頻解析的過程,如果需要了解整一個大概過程可以參考2018版的愛奇藝視頻解析分析過程。下面文章將直接以cmd5x代碼為起點進行分析。
這是2019-08版本的cmd5x算法的分析處理思路。
由于算法太臃腫,所以下面的分析講通過貼出偽代碼來進行分析,如果需要完整的算法可以點這里。
這文章的分析不是對算法的簡化分析,而是為了在非瀏覽器端nodejs實現cmd5x算法,也就是從瀏覽器端移植到Nodejs。
大概
這一版本cmd5x算法的一些主要特點:
- 進行了方法名的混淆。
- 引入了瀏覽器端天生具有的而Nodejs這些運行時天生所不具有的document和window。
- 進行了非瀏覽器端JS運行時的識別(如Nodejs中的特有指令process和require的判斷)。
分析過程
首先
下面是cmd5x算法生成的偽代碼。
function(module, exports, __webpack_require__) {var _qda = [...]; // 混淆方法名!function(e, t) { !function(t) {for (; --t; )e.push(e.shift())}(++t)}(_qda, 115); // 混淆偏移var qdb = function(e, t){// 包含document, window的算法執行代碼。};function _qd_az() {// 包含document, window的初始化代碼。}_qd_az(); }執行
var r = e.url.replace(new RegExp("^(http|https)://" + t,"ig"), "");d && (r = r.replace("/3ea/420a8433732a6c99d1eae98fea69e55d", "")),n = s.a.cmd5x(r)方案
暴力破解:
- 既然cmd5x是一個加了鹽的md5算法,那么應該是可以使用遍歷所加的鹽來暴力破解,但是這計算量確實是非常不友好,這從來都是一個萬不得已的解決方法。
暴力調試。
- 所謂的暴力調試就是通過比較輸出循環結果來找出并以此來解決不匹配的點,以便于修正不匹配的問題,事實上這跟通過print來調試程序bug的本質是一致的,暴力調試是一個相對一個比較通用的解決方法。
下面將使用暴力調試來分析
關于調試
簡單的來說,這文章所做的一切就是為了實現將瀏覽器端的cmd5x代碼移植到nodejs中運行。
為了實現這一點,這篇文章里面我們將同時需要用到瀏覽器端的調試工具和nodejs的調試工具。
如何使用chromium的開發者工具對本地導入的代碼進行斷點調試
下面用幾張圖簡單說一下chromium開發者工具對注入的代碼的斷點調試:
注:文章中所說的調試通常是指的是瀏覽器端和nodejs分別使用以輸出數據以進行比較。
第一次測試
-
既然我們已經找到了cmd5x實現的算法,我們在進行第一次調試是否能夠正常使用,
-
我們的目標是在Nodejs上實現cmd5x,所以我們對找到的cmd5x代碼放到Nodejs里面運行一下看下代碼是否僥幸的運行正確。
-
為了在nodejs中運行,建立如下代碼。(偽代碼)
- var build_cmd5x = function(module, exports, __webpack_require__) {var _qda = [...]; // 混淆方法名!function(e, t) { !function(t) {for (; --t; )e.push(e.shift())}(++t)}(_qda, 115); // 混淆偏移var qdb = function(e, t){// 包含document, window的算法執行代碼。};function _qd_az() {// 包含document, window的初始化代碼。}_qd_az(); } var cmd5x_exports = {}; build_cmd5x(null, cmd5x_exports);```
-
放進nodejs執行發生如下錯誤。
- Ca = 60) : (b2 ? b5 = self[_qdb("0x29")][_qdb("0x2a")] : document[_q db("0x2b")] && (b5 = document[_qdb("0x2b")][_qdb("0x2c")]),^ReferenceError: document is not definedat _qd_az (C:\Users\admin\Desktop\8-12\origin_cmd5x.js:142:70)at build_cmd5x (C:\Users\admin\Desktop\8-12\origin_cmd5x.js:10717:5)at Object.<anonymous> (C:\Users\admin\Desktop\8-12\origin_cmd5x.js:10721:1)at Module._compile (internal/modules/cjs/loader.js:778:30)at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)at Module.load (internal/modules/cjs/loader.js:653:32)at tryModuleLoad (internal/modules/cjs/loader.js:593:12)at Function.Module._load (internal/modules/cjs/loader.js:585:3)at Function.Module.runMain (internal/modules/cjs/loader.js:831:12)at startup (internal/bootstrap/node.js:283:19)
-
模擬環境測試(可選)
-
到目前為止我們發現并不能簡單的扣代碼運行,其中的document, window是瀏覽器端先天具有而非瀏覽器端運行時如nodejs不先天具有的對象。 這個問題第一反應是先借助nodejs的第三方庫構建window 和 document對象(這里我們使用庫jsdom),之后當實現了目的后,如果有進一步的需求,我們可以再將庫jsdom的依賴去掉(實際上算法并不需要依賴document 和 window,或許可能僅僅依賴其中一些數據。一步一步排錯修改,這樣就能避免過多的變量造成調試的困難)。
-
要注意的是,jsdom所構造得到的window和document并不能完全構造在瀏覽器端運行的window和document,所以這之后將是要考慮的一個問題。
-
在build_cmd5x之前加入如下代碼來構建window 和 document。
- const jsdom = require('jsdom'); const { JSDOM } = jsdom;const dom = new JSDOM('', {url: "http://www.iqiyi.com/",referrer: "http://www.iqiyi.com/",contentType: "text/html",includeNodeLocations: true,storageQuota: 1000000 });global.window = dom.window; global.document = window.document;global.self = window; document.domain = "iqiyi.com";```
-
“安全”通過build_cmd5x,之后我們繼續測試執行cmd5x來看下輸出值是否是所需要的。
-
為了檢驗cmd5x的正確性,我們在瀏覽器端的運行結果cmd5x("/dash?tvid=2185399200&bid=300&vid=52ec66e4dc5263184c5b1b378ee6eb81&src=01010031010000000000&vt=0&rs=1&uid=&ori=pcw&ps=0&k_uid=be720e39e62fd602f95e957dc401891e&pt=0&d=0&s=&lid=&cf=&ct=&authKey=7ebb8c20728f94c13e74f3aa555f48e6&k_tag=1&ost=0&ppt=0&dfp=a0aadc0b7de24f4e3b97fe7d20230fbce8b3b1c4d0d9aa9904353e363ea4ece6ea&locale=zh_cn&prio=%7B%22ff%22%3A%22f4v%22%2C%22code%22%3A2%7D&pck=&k_err_retries=0&up=&qd_v=2&tm=1566399701976&qdy=a&qds=0&k_ft1=141287244169220&k_ft4=8196&k_ft5=1&bop=%7B%22version%22%3A%2210.0%22%2C%22dfp%22%3A%22a0aadc0b7de24f4e3b97fe7d20230fbce8b3b1c4d0d9aa9904353e363ea4ece6ea%22%7D&ut=0") = 8f2b8b04a29e5e8d0f10b8e7a7bc7668來進行首次檢驗運行的正確性。
- r = "/dash?tvid=2185399200&bid=300&vid=52ec66e4dc5263184c5b1b378ee6eb81&src=01010031010000000000&vt=0&rs=1&uid=&ori=pcw&ps=0&k_uid=be720e39e62fd602f95e957dc401891e&pt=0&d=0&s=&lid=&cf=&ct=&authKey=7ebb8c20728f94c13e74f3aa555f48e6&k_tag=1&ost=0&ppt=0&dfp=a0aadc0b7de24f4e3b97fe7d20230fbce8b3b1c4d0d9aa9904353e363ea4ece6ea&locale=zh_cn&prio=%7B%22ff%22%3A%22f4v%22%2C%22code%22%3A2%7D&pck=&k_err_retries=0&up=&qd_v=2&tm=1566399701976&qdy=a&qds=0&k_ft1=141287244169220&k_ft4=8196&k_ft5=1&bop=%7B%22version%22%3A%2210.0%22%2C%22dfp%22%3A%22a0aadc0b7de24f4e3b97fe7d20230fbce8b3b1c4d0d9aa9904353e363ea4ece6ea%22%7D&ut=0"; console.log(cmd5x_exports.cmd5x(r));
-
上面的代碼運行得到f48f5790c89d585630a083e3ce052442。很顯然這并不是算法得到的結果。
-
-
為什么一樣的代碼得到不一樣的結果,而不是報錯。這或許說明了存在著一個或多個“開關”——能夠使得在瀏覽器端與非瀏覽器端中進行切換算法,使得輸出的結果不一樣。
- 到目前為止我們不能僅通過宏觀的處理實現,所以后面需要分析到具體代碼了。但是分析代碼對于小型的代碼可以應付得來,但是對于幾萬行的代碼來說絕不是小事。
初看代碼結構
- function(module, exports, __webpack_require__) {var _qda = [...]; // 混淆方法名!function(e, t) { !function(t) {for (; --t; )e.push(e.shift())}(++t)}(_qda, 115); // 混淆偏移var qdb = function(e, t){// 包含document, window的算法執行代碼。};function _qd_az() {// 包含document, window的初始化代碼。}_qd_az(); }
-
第一次大概調試走一遍算法。然后會發現其中存在著很多的_qdb(0x?),看到這里我們調試走進去出來之后得到的是一個字符串length或者name這些常見的對象的屬性,可以猜測這是一種混淆的方法。
-
我們走進去_qdb()函數發現了它依賴于_qda,所以有理由的懷疑這是_qdb()是用來解析_qda變量的元素的。可以反復的給_qdb傳參來調用如 _qdb('0x0')來測試函數_qdb的輸出結果是否受調用次數的影響(當然這也可以通過粗略看_qdb代碼來知道這一點)。
-
結果就是_qdb()調用結果不受調用次數影響,所以我們可以將所有的混淆替換一下文本來進行簡單的反混淆。
反混淆
-
有很多方法可以實現文本的替換,這里我使用python實現一下批量替換。
-
首先我們先在腳本中輸出替換文本和被替換文本。注意到變量_qda的定義后面有一個自執行函數,是用來左移數組變量_qda的,所以我們需要在這之后運行。
- // 其中 _qda.length = 1312 var kv = []; for(var i=0; i<1312; i++){kv.push(i.toString(16) + '|' + _qdb('0x' + i.toString(16))); } console.log(kv.join('\n')); // 之后將輸出寫出文件```
-
保存輸出結果。
-
使用如下代碼得到的替換后的代碼。
- # 假設源文本讀入變量 origin_txt, # 替換文本讀入變量 replace_txt replace_list = [] for i in replace_txt.strip().split('\n'):replace_list.append(i.split('|'))retval = origin_txt for i in replace_list:retval = retval.replace('_qdb(0x%s)' % i[0], i[1].__repr__())# 寫出 retval 到文件,替換新的build_cmd5x
-
測試反混淆結果
- 為了保險起見,對于反混淆的結果后我們需要測試一下是否對算法結果有影響。
- 在瀏覽器端的環境進行運行一下來看下是否正確。
- 在瀏覽器任意打開一個愛奇藝視頻,進入開發者工具F12(這里使用百分瀏覽器),進入Console,將代碼放進去運行一下。然后看下結果。
- 除此之外還可以在其他網站運行這樣的代碼。下面我們在www.baidu.com和www.bilibili.com下運行了這段代碼。
- ,
為什么這么做?
- 在iqiyi視頻頁處運行這段代碼用意:
- 測試反混淆代碼是否正確,以便于后面工作的執行。
- 測試是否在其他地方進行算法的額外初始化工作。(其他地方進行額外初始化工作的意思是代碼中可能存在export到window或其他全局變量,然后在其他JS腳本進行調用初始化的函數。)
- 在其他網站頁如bilibili和baidu處運行這段代碼用意:
- 測試算法的運行結果是否與document和window里面的一些數據有關系。
這樣的結果說明了什么?
-
反混淆代碼是正確的。
-
沒有在其他地方的額外初始化工作。
-
算法的運行確實與document和window的數據有關系。
-
可以推測的是這種關系是一種數據判斷關系,算法本身并不需要這些數據。這我們可以通過在兩個不同的網站里面得到了一樣的結果知道這些。
暴力調試
怎么調試呢?
-
通過調試n = s.a.cmd5x(r)我們可以知道的是其函數所在就是:
- aF = function(e) {if ('mwlCr' !== 'UdOHx')return typeof ArrayBuffer === 'undefined' ? 'iloveiqiyi' : aU['ccall']('cmd5x', 'string', ['string'], [e]);b[na >> 2] = ma,ma = 0 | b[15],ma ? (w = ma + 4 | 0,b[na + 4 >> 2] = b[w >> 2],oa = w) : (b[na + 4 >> 2] = na,oa = 60),b[oa >> 2] = na }
-
必然的是會執行return typeof ArrayBuffer === 'undefined' ? 'iloveiqiyi' : aU['ccall']('cmd5x', 'string', ['string'], [e]);
- function c7(e, t, n, r, i) {var o = {};...return f = d,d = t === 'string' ? cM(f) : t === 'boolean' ? Boolean(f) : f,0 !== u && sg(u),d}
-
在build_cmd5x的時候會執行初始化工作_qd_az(),我們進去走一篇可以發現的是算法使用了ArrayBuffer。如下代碼:
- function dM() {'gNDMI' != 'gNDMI' ? document['title'] = title : (aU['HEAP8'] = dE = new Int8Array(dD),aU['HEAP16'] = dG = new Int16Array(dD),aU['HEAP32'] = dI = new Int32Array(dD),aU['HEAPU8'] = dF = new Uint8Array(dD),aU['HEAPU16'] = dH = new Uint16Array(dD),aU['HEAPU32'] = dJ = new Uint32Array(dD),aU['HEAPF32'] = dK = new Float32Array(dD),aU['HEAPF64'] = dL = new Float64Array(dD))}
-
我們可以看到Buffer是dD。所以dE, dG, dI, dF, dH, dJ, dK, dL是綁在dD的。所以我們可以選擇其中一個進行數據對比, 如dJ。
-
首先如果是幾千或者幾萬個數據當然你可以將每一個數據輸出進行比較。但是這么是幾十萬幾百萬,電腦可能就吃不消,所以這就不適合將所有數據輸出來比較。
-
由于未初始化數據都是0,所以我們可以僅僅輸出非0的數據,這樣就能大大減少比較的數據量。
-
如下函數用于輸出待比較的數據。
- function test_arraybuffer(arraydata){for(var i=0; i<arraydata.length; i++){if(data != 0){print_data.push([i, data].join(' '));}} }...console.log(print_data.join('\n'));
-
-
我們再回去看函數c7(), 比較一下在瀏覽器端和nodejs輸出的數據是否匹配。如果不匹配就是說明前面存在使他們不匹配的數據,這才是我們關注的對象。也就是說我們需要逐步縮小不匹配問題的所在地點,然后重點分析。
- function c7(e, t, n, r, i) {var o = {};o['string'] = function(e) {if ('dDZQD' !== 'HIUzl') {var t = 0;if (null != e && 0 !== e) {var n = 1 + (e.length << 2);d4(e, t = sf(n), n)}return t}b[ib + 4 >> 2] = ib,jb = 60},o['array'] = function(e) {var t = sf(e.length);return dh(e, t),t};/// 1號var a = c4(e), s = [], u = 0;// 2號if (r)for (var c = 0; c < r['length']; c++) {var l = o[n[c]];l ? (0 === u && (u = sh()),s[c] = l(r[c])) : s[c] = r[c]}// 3號var f, d = a['apply'](null, s);// 4號return f = d,d = t === 'string' ? cM(f) : t === 'boolean' ? Boolean(f) : f,0 !== u && sg(u),d
}
- 我們注意上面代碼的1~4號,我們在這些地方可以分別加上test_arraybuffer(dJ);,并將在瀏覽器端和nodejs端的輸出數據分別進行比較。如果出現不一致的問題就說明了之間有不匹配的點,而那些所謂的瀏覽器端和nodejs的識別開關就存在于其中。
-
下面給出在一號位在瀏覽器端和Nodejs端的結果的比較。
- - ,
-
然后我就不一一列出來比較結果,這里直接說結果就是識別開關就存在于var f, d = a['apply'](null, s);(因為在3號位數據一致,4號位數據不一致)。
-
我們進去var f, d = a['apply'](null, s);。
- function hn(e) {...// ArrayBuffer 數據一致。e: for (; ; )if ('TpEqY' === 'guQVq')b = Pe + 4 | 0,gL[Ae + 4 >> 2] = gL[b >> 2],qe = b;else {switch ((255 & x) << 24 >> 24) {...}Q = ht,z = pt,G = _t,H = gt,W = yt,V = bt,B = vt,F = mt,j = kt,N = wt,U = St,M = xt,C = Tt,R = Lt,D = Et,I = Pt,O = At,q = qt,A = Ot,P = It,E = Dt,L = Rt,T = Ct,x = Mt,S = Ut}... }```
-
同樣的方法,我們先比較ArrayBuffer的數據。之后我們會發現一直到e: for(;;)數據還是一致。
-
這又遇到了麻煩了。因為下面就是算法的最后一步,而這一步是一個無限循環,代碼又是一長串,我們不希望在每一種case都進行比較一下ArrayBuffer,第一這既繁瑣,第二這又無法得知是在第幾次循環后的case。
-
所以之后我們應該轉向考察其他的變量,找那些for循環里面用到的東西,如上面這些變量,同時計數一下,用于找出循環次數是否一樣。
- function hn(e) {...// 計數變量 counter;var counter = 0;e: for (; ; )if ('TpEqY' === 'guQVq')b = Pe + 4 | 0,gL[Ae + 4 >> 2] = gL[b >> 2],qe = b;else {// 如下代碼將用來輸出比較數據counter++; print_data.push([counter,lt,ct,ut,b,at,ot,it,rt,nt,tt,$e,Je,Ze,Xe,Ke,Ye,Qe,ze,Ge,He,We,Ve,Be,Fe,je,Ne,Ue,Me,Ce,Re,De,Ie,Oe,qe,Ae,Pe,Ee,Le,Te,xe,Se,we,ke,me,ve,be,ye,ge,_e,pe,he,de,fe,le,ce,ue,se,ae,oe,ie,re,ne,te,ee,$,J,Z,X,K,Y,Q,z,G,H,W,V,B,F,j,N,U,M,C,R,D,I,O,q,A,P,E,L,T,x].join(' '));// 第二部可以如下調試,for循環里面所經過的路徑print_data.push([counter, (255 & x) << 24 >> 24].join(' '))switch ((255 & x) << 24 >> 24) {...}Q = ht,z = pt,G = _t,H = gt,W = yt,V = bt,B = vt,F = mt,j = kt,N = wt,U = St,M = xt,C = Tt,R = Lt,D = Et,I = Pt,O = At,q = qt,A = Ot,P = It,E = Dt,L = Rt,T = Ct,x = Mt,S = Ut}... }```
-
當然也可以從for循環里面的的代碼發現,里面大都是數學運算,只有少少的幾條函數調用,就可以發現下面這幾條,或許就是非瀏覽器端識別的關鍵所在。
- // 第一條w = 0 | s1[1 & gL[16 + ((0 != (0 | gL[14]) & 1) + (0 == (0 | gL[15]) & 1) << 2) >> 2]]();// 第二條m = 0 | rb(0 | gL[(w = i + (z << 3) | 0) >> 2], 0 | gL[w + 4 >> 2], 0 | r6(0 | k, ((0 | k) < 0) << 31 >> 31 | 0, 1), 0 | h0());// 第三條yt = v = 6 + ((w = (0 | z) % 4 | 0) << 2) + ((0 | gX(w + -1 | 0, w)) / 2 | 0) | 0;// 第四條s3[1 & gL[16 + ((0 != (0 | gL[14]) & 1) + ((0 == (0 | y)) << 31 >> 31) + ((0 != (0 | y) & 1) << 1) << 2) >> 2]](S);// 第五條s2[1 & gL[16 + (((0 == (0 | gL[14]) & 1) << 1 | 4) << 2) >> 2]](33);// 第六條k = 0 | rb(0 | gL[(y = i + (z << 3) | 0) >> 2], 0 | gL[y + 4 >> 2], 0 | r6(0 | v, ((0 | v) < 0) << 31 >> 31 | 0, 1), 0 | h0());....
-
函數h0(), s1[...](), s2[...](), s3[...](), gX(), r6()…可以逐一去調試進去看一下。
-
通過一頓調試操作后,我們進入函數function eV(){}。
- function eV() {var counter = 0;for (var e = 27218; ; ) {counter ++;// 存儲執行序列進行數據對比,以便找到“判斷點”。print_data.push([conter, e].join(' '));switch (e) {case 52924:...}}}
-
然后再經過對比執行序列。
-
我們能找到如下用于檢測nodejs的process。
- case 59488:try {A = Object['prototype']['toString']['call'](C('process;')) === '[object process]'} catch (e) {}e += -13840;break;
- aD = function(aE) {return eval(aE) }
-
可以看到的這里面會嘗試運行代碼Object['prototype']['toString']['call'](C('process;')),也就是執行函數aD(), 然后執行返回eval('process'),
- 這一條函數如果是在瀏覽器端將會產生異常,然后就會被try catch()捕捉,然后就會給A賦值,之后就會影響到接下來的算法結果。
- 如果是在非瀏覽器端nodejs執行這條函數對象process,也就是說他不會產生異常。
-
上面所說的就是一些所謂的非瀏覽器端的識別開關。
-
如果有process對象,那么就會有不一樣的e,之后就會執行不一樣的流程。
怎么處理呢?
-
解決方法其實很簡單,只要將變量名改為一個在瀏覽器端和非瀏覽器端均產生異常或者均不產生異常的變量即可。這里可以直接將process改成process1, require改成require1就行了。
-
之后我們又找到了檢測require,document.domain, window.screen.clientWidth, window.screen.clientHeight。。。。
- case 40401:A ? e += -37e3 : 'CESxF' === 'PPJkW' ? t = C('require;') : e += 11717;break;
- case 6409:try {'NYIqF' === 'coDEu' ? (ba = g,ca = s) : t = C('require;')} catch (e) {}e += 52661;break;
- case 37096:A = C['clientHeight'],e += -23385;break;
-
。。。
-
后面都是使用同樣的方法,通過不斷的對比數據序列來暴力調試。不斷的修正,這就能得到最后的結果了。
-
經過了不斷的調試,不斷的對比序列,并且不斷的修改,就能得到完全吻合的序列。
-
這種流程其實本質上這就是和軟件的反匯編破解注冊是一樣的,只要在關鍵節點處進行修改跳轉就行了。
隨便說說
- 從process和require這些的檢測,我們知道它都是可以經過異常捕獲來進行瀏覽器端和非瀏覽器端的判斷,所以這就可以讓我們通過搜索被try catch的代碼來進行分析是否存在這樣的識別問題。
- 事實上,我們可以不經過反混淆就可以進行后面的所有操作,但是去掉混淆后會減輕很多調試負擔。
- 正如前面所說的,我們可以進一步調試去掉庫jsdom的依賴。但是這個并不是必須的。
最后
鏈接
-
Github: 點這里
-
Github博客:點這里
-
原cmd5x腳本:origin_cmd5x.js
-
移植后JS腳本: iqiyi_cmd5x.js
總結
以上是生活随笔為你收集整理的爱奇艺视频cmd5x解析算法的移植分析和实现Nodejs(2019-08)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu16 上安装 福昕PDF阅读
- 下一篇: Python——正则表达式语法与实践