以下所需要的知识不需要精通,只需要了解和知道基本的语法即可。
这一文章将不使用Fiddler来抓包,而是全程使用chromium核的开发者工具进行分析。
一说到chromium,可能大家第一反应就是使用谷歌浏览器。但是很遗憾,在这里不使用谷歌浏览器,因为谷歌浏览器所用到的cKey是8.1版本的。(我这里的浏览器版本是74.0.3729.131, 以后的版本可能会使用9.1)
除了谷歌浏览器外还有很多使用chromium核的浏览器,360浏览器(未知),搜狗浏览器(ckey8.1),,我这里将使用百分浏览器(ckey9.1),并且在腾讯视频解析里面他使用到的是cKey9.1版本。这正是我们所要分析的对象。
下面将以https://v.qq.com/x/cover/bzfkv5se8qaqel2/j002024w2wg.html 为分析例子。
F12打开开发者工具。
打开链接:https://v.qq.com/x/cover/bzfkv5se8qaqel2/j002024w2wg.html
。
切换Network查看抓包。
搜索proxyhttp,找到两项 https://vd.l.qq.com/proxyhttp
。(这里搜proyhttp是因为前面省略了从视频到解析链接的寻找过程,如果不知道怎么做,就先看上文的前提建议)
既然知道了请求视频的链接是proxyhttp,那么在proxyhttp发送前中断如何?
Sources
页面,在XHR/fetch Breakpoints
的+
进行添加条件断点 proxyhttp
,意思就是在包含proxyhttp字串的请求链接时进行中断。Call Stack
,可以看到调用函数的右边表明了被调用函数所在的JS链接。https://vd.l.qq.com/proxyhttp
之前肯定先需要先收集所要发送的data,所以必然这将调用到获取data的函数,而获取部分必然会与加密部分有联系,所以可以通过这样的方式来找到加密部分。proxyhttp
来定位到目标链接(注意这不是一定的),但是由于在爱奇艺分析过程中使用了这一方法,我在这里用一下别的方法来解决。)tvx.core.js
是用来对发送请求的。所以大概可以估计这文件就是对请求函数的集合,既然已经到了发送的地步了,那么data肯定是已经获取完成了。pecker.js
,点击他,然后往下滚看到Scope
项,看到e,f两项就是要发送的请求的所有数据,展开发现data中cKey已经存在,所以这里Call Stack
往上走(往上一层调用走)。e.requestPostCgi
位于htmlframe.......
(关于Call Stack看图1.3),粗看函数名似乎就是提交data的获取。将其作为重点深找一下。e.requestPostCgi
后往下滚看到Scope
,下图,本地变量c
就是要提交的data,图1.7的中间红框部分就是本地变量c
的获取,发现vinfoparam
是由62455行
生成的数据。f.param(b.vinfoparam)
,发现该函数传入了参数b.vinfoparam
,鼠标停在该参数出现了数据cKey。所以可以断定重点在于b.vinfoparam
,而不是函数f.param
。b.vinfoparam
中的变量b是调用e.requestPostCgi
时传入的参数(位于62446
)c
。{ vinfoparam: g, adparam: e, domain: v, method: w}
vinfoparam: g
,往前找g的生成代码。看【图1.8】的62742
进入函数f.getInfoConfig
。却没有发现cKey
的踪迹,既然我们无法直接知道,不如放个断点走一走。getInfoConfig
的调试中。e(h)
。【图1.14】【图1.15】a.cKey = b || ""
这就是cKey生成的地方。就是变量b
,也就是f ? (a.encryptVer = "9.1", b = f(a.platform, a.appVer, a.vids || a.vid, "", a.guid, a.tm)) : (a.encryptVer = "8.1", b = i(a.vids || a.vid, a.tm, a.appVer, a.guid, a.platform)), a.cKey = b || ""
f()
参数控制的。但这不是我们的重点,既然我们分析的是9.1版本,那么进入函数f()
。function i(a, b, c, d, e) { // 注意下面的函数k(a, b)是函数f(a)里面的k函数,为了方便起见直接在合起来写了 function k(a, b) { if (0 === b || !a) return ""; for (var c, d = 0, e = 0; ; ) { if (g(a + e < db), c = Ga[a + e >> 0], d |= c, 0 == c && !b) break; if (e++, b && e == b) break } b || (b = e); var f = ""; if (d < 128) { for (var h, i = 1024; b > 0; ) h = String.fromCharCode.apply(String, Ga.subarray(a, a + Math.min(b, i))), f = f ? f + h : h, a += i, b -= i; return f } return m(a) } function f(a) { return "string" === b ? k(a) : "boolean" === b ? Boolean(a) : a } var i = h(a) , j = [] , l = 0; if (g("array" !== b, 'Return type should not be "array".'), d) for (var m = 0; m < d.length; m++) { var n = $a[c[m]]; n ? (0 === l && (l = Ub()), j[m] = n(d[m])) : j[m] = d[m] } var o = i.apply(null, j); return o = f(o), 0 !== l && Tb(l), o }
o
,那么我们重点关注他,走到o
,64084行
,进去,【图2.3】看到ua._getckey
,可以知道看来是找对地方了。ua._getckey = function() { return g(ib, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"), g(!jb, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"), ua.asm._getckey.apply(null, arguments)}
ua.asm._getckey.apply(null, arguments)
,???wocao这是什么鬼【图2.4】native code
,这说明了这不是JS的原生代码,可能是其他语言实现的方法。WebAssembly
,这是一种JS的一种可以理解成是交叉编程的一种方式,目的是为了提高JS运行效率,这是由C或者其他编程语言生成的代码,生成*.wasm然后交给WebAssembly加载处理运行。wasm-0005098e-29
,你点进去查看就看反汇编到具体的指令。wasm
文件。Sources
页面搜索wasm
就能找到加载的wasm文件。arguments
,虽然我们得到了wasm,但是我们还是需要知道参数arguments
才能实现算法。arguments
就是前面【图2.3】传递的参数j
,我们要得到j
。Ub()
和n()
,而n()
是由var n = $a[c[m]];
提供的。所以我们F5刷新下页面在【图2.2】重新断点。为的就是单步执行,找所需。n
,一种是undefined
和stringToC: function(a) { var b = 0; if (null !== a && void 0 !== a && 0 !== a) { var c = (a.length << 2) + 1; b = Sb(c), o(a, b, c) } return b}
Sb()
, o()
, n()
,其中包括了循环中的Ub
还有f()
函数中的k()
然后你能整理出来Ub = function() { return g(ib, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"), g(!jb, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"), ua.asm.stackSave.apply(null, arguments)}Sb = function() { return g(ib, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"), g(!jb, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"), ua.asm.stackAlloc.apply(null, arguments) }function o(a, b, c) { return g("number" == typeof c, "stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!"), n(a, Ga, b, c)}function n(a, b, c, d) { if (!(d > 0)) return 0; for (var e = c, f = c + d - 1, g = 0; g < a.length; ++g) { var h = a.charCodeAt(g); if (h >= 55296 && h <= 57343) { var i = a.charCodeAt(++g); h = 65536 + ((1023 & h) << 10) | 1023 & i } if (h <= 127) { if (c >= f) break; b[c++] = h } else if (h <= 2047) { if (c + 1 >= f) break; b[c++] = 192 | h >> 6, b[c++] = 128 | 63 & h } else if (h <= 65535) { if (c + 2 >= f) break; b[c++] = 224 | h >> 12, b[c++] = 128 | h >> 6 & 63, b[c++] = 128 | 63 & h } else if (h <= 2097151) { if (c + 3 >= f) break; b[c++] = 240 | h >> 18, b[c++] = 128 | h >> 12 & 63, b[c++] = 128 | h >> 6 & 63, b[c++] = 128 | 63 & h } else if (h <= 67108863) { if (c + 4 >= f) break; b[c++] = 248 | h >> 24, b[c++] = 128 | h >> 18 & 63, b[c++] = 128 | h >> 12 & 63, b[c++] = 128 | h >> 6 & 63, b[c++] = 128 | 63 & h } else { if (c + 5 >= f) break; b[c++] = 252 | h >> 30, b[c++] = 128 | h >> 24 & 63, b[c++] = 128 | h >> 18 & 63, b[c++] = 128 | h >> 12 & 63, b[c++] = 128 | h >> 6 & 63, b[c++] = 128 | 63 & h } } return b[c] = 0, c - e}
o(a, b, c)
调用了方法n(a, Ga, b, c)
,其中a, b,c
我们都知道,但是Ga
是什么东西?Ga
,所以,我们找到他了,看【图2.9】Ga
的缘由,那么把所有对于给Ga
赋值的东西联系起来。// 只要知道ArrayBuffer的都知道这将导致下面的 Fa,Ha, Ja, Ga, Ia, Ka, La, Ma绑在了Ea下// 也就是说Ea是数据,而Fa,Ha, Ja, Ga, Ia, Ka, La, Ma就是描述这个数据的方式,所有的改变只是对Ea操作。所以对其中一个改变都会改变我们的目标Gafunction w() { Fa = new Int8Array(Ea), Ha = new Int16Array(Ea), Ja = new Int32Array(Ea), Ga = new Uint8Array(Ea), Ia = new Uint16Array(Ea), Ka = new Uint32Array(Ea), La = new Float32Array(Ea), Ma = new Float64Array(Ea);}function d(a) { var b = Oa; return Oa = Oa + a + 15 & -16, b}function e(a, b) { b || (b = Da); var c = a = Math.ceil(a / b) * b; return c}var Da = 16;var Ea, Fa, Ga, Ha, Ia, Ja, Ka, La, Ma, Na, Oa, Pa, Qa, Ra, Sa, Ta, Ua, Va = { "f64-rem": function(a, b) { return a % b }, "debugger": function() {}}, Wa = (new Array(0), 1024) ;Na = Oa = Qa = Ra = Sa = Ta = Ua = 0, Pa = !1;var cb = 5242880 , db = 16777216, ab = 65536;var wasmMemory = new WebAssembly.Memory({ initial: db / ab, maximum: db / ab});Ea = wasmMemory.buffer;w();Ja[0] = 1668509029;Ha[1] = 25459;var eb = [] , fb = [] , gb = [] , hb = [] , ib = !1 , jb = !1;Na = Wa, Oa = Na + 6928, fb.push();Oa += 16;Ua = d(4),Qa = Ra = e(Oa),Sa = Qa + cb,Ta = e(Sa),Ja[Ua >> 2] = Ta,Pa = !0;
Ga
的初始化。for (var m = 0; m < d.length; m++) { var n = $a[c[m]]; n ? (0 === l && (l = Ub()), j[m] = n(d[m])) : j[m] = d[m]}
var o = i.apply(null, j); return o = f(o), 0 !== l && Tb(l), o
i.apply(null, j);
,他的代码位于wasm中。var ub = ua.asm(ua.asmGlobalArg, ua.asmLibraryArg, Ea)var Cb = ub._getckey;ub._getckey = function() { return g(ib, "you need to wait for the runtime to be ready (e.g. wait for main() to be called)"), g(!jb, "the runtime was exited (use NO_EXIT_RUNTIME to keep it alive after main() exits)"), Cb.apply(null, arguments)}
ub
也就是ua.asm(ua.asmGlobalArg, ua.asmLibraryArg, Ea)
。ua.asm = function(a, b, c) { if (!b.table) { var d = ua.wasmTableSize; void 0 === d && (d = 1024); var f = ua.wasmMaxTableSize; "object" == typeof WebAssembly && "function" == typeof WebAssembly.Table ? void 0 !== f ? b.table = new WebAssembly.Table({ initial: d, maximum: f, element: "anyfunc" }) : b.table = new WebAssembly.Table({ initial: d, element: "anyfunc" }) : b.table = new Array(d), ua.wasmTable = b.table } b.memoryBase || (b.memoryBase = ua.STATIC_BASE), b.tableBase || (b.tableBase = 0); var h; return h = e(a, b, c), g(h, "no binaryen method succeeded. consider enabling more options, like interpreting, if you want that: http://kripken.github.io/emscripten-site/docs/compiling/WebAssembly.html#binaryen-methods"), h}
a,b,c
,所以再回到ua.asm(ua.asmGlobalArg, ua.asmLibraryArg, Ea)
ua.asmGlobalArg, ua.asmLibraryArg, Ea
,而其中Ea
我们已经在前面说过了,跟Ga
有关系。ua.wasmTableSize = 99, ua.wasmMaxTableSize = 99, ua.asmGlobalArg = {},ua.asmLibraryArg = { abort: sa, assert: g, enlargeMemory: B, getTotalMemory: C, abortOnCannotGrowMemory: A, abortStackOverflow: z, nullFunc_ii: ca, nullFunc_iiii: da, nullFunc_v: ea, nullFunc_vi: fa, nullFunc_viiii: ga, nullFunc_viiiii: ha, nullFunc_viiiiii: ia, invoke_ii: ja, invoke_iiii: ka, invoke_v: la, invoke_vi: ma, invoke_viiii: na, invoke_viiiii: oa, invoke_viiiiii: pa, __ZSt18uncaught_exceptionv: Q, ___cxa_find_matching_catch: S, ___gxx_personality_v0: T, ___lock: U, ___resumeException: R, ___setErrNo: ba, ___syscall140: V, ___syscall146: X, ___syscall54: Y, ___syscall6: Z, ___unlock: $, _abort: _, _emscripten_memcpy_big: aa, _get_unicode_str: P, flush_NO_FILESYSTEM: W, DYNAMICTOP_PTR: Ua, tempDoublePtr: rb, STACKTOP: Ra, STACK_MAX: Sa};var ub = ua.asm(ua.asmGlobalArg, ua.asmLibraryArg, Ea)
P
,也就是_get_unicode_str: P,
中的P
,对应如下function P() { function a(a) { return a ? a.length > 48 ? a.substr(0, 48) : a : "" } function b() { var b = document.URL , c = window.navigator.userAgent.toLowerCase() , d = ""; document.referrer.length > 0 && (d = document.referrer); try { 0 == d.length && opener.location.href.length > 0 && (d = opener.location.href) } catch (e) {} var f = window.navigator.appCodeName , g = window.navigator.appName , h = window.navigator.platform; return b = a(b), d = a(d), c = a(c), b + "|" + c + "|" + d + "|" + f + "|" + g + "|" + h } var c = b() , d = p(c) + 1 , e = Pb(d); return o(c, e, d + 1), e}
_getckey()
的时候,他会call 20
,也就是wasm文件中的编号20的函数
,但是你仔细看【图2.5】会发现缺少缺少了20号
函数,这是因为他会上面在链接接口的时候链接了函数P()
,而函数P()
就是20号
函数。var fun_ = function(){};wasm_env = { abort: fun_, assert: fun_, enlargeMemory: fun_, getTotalMemory: C, abortOnCannotGrowMemory: fun_, abortStackOverflow: fun_, nullFunc_ii: fun_, nullFunc_iiii: fun_, nullFunc_v: fun_, nullFunc_vi: fun_, nullFunc_viiii: fun_, nullFunc_viiiii: fun_, nullFunc_viiiiii: fun_, invoke_ii: fun_, invoke_iiii: fun_, invoke_v: fun_, invoke_vi: fun_, invoke_viiii: fun_, invoke_viiiii: fun_, invoke_viiiiii: fun_, __ZSt18uncaught_exceptionv: fun_, ___cxa_find_matching_catch: fun_, ___gxx_personality_v0: fun_, ___lock: fun_, ___resumeException: fun_, ___setErrNo: fun_, ___syscall140: fun_, ___syscall146: fun_, ___syscall54: fun_, ___syscall6: fun_, ___unlock: fun_, _abort: fun_, _emscripten_memcpy_big: fun_, _get_unicode_str: P, // function 20( ) => P( ) flush_NO_FILESYSTEM: fun_, DYNAMICTOP_PTR: 7968, //Ua tempDoublePtr: 7952, //rb STACKTOP: 7984, //Ra STACK_MAX: 5250864, //Sa memoryBase: 1024, tableBase: 0, memory: wasmMemory, table: new WebAssembly.Table({ initial: 99, maximum: 99, element: "anyfunc" })};var importObject = { 'env': wasm_env, 'asm2wasm': { "f64-rem": function(a, b) { return a % b }, "debugger": function() {} }, 'global': { NaN: NaN, Infinity: 1 / 0 }, "global.Math": Math, // "parent": {};};
联系客服