篇幅有限

完整内容及源码关注公众号:ReverseCode,发送

起因

最近对腾讯视频下手了,因为公众号中的视频来源都是腾讯视频,也就是说通过2分钟阅读本文,腾讯视频下整站视频都可以下载下来,也是大多数在线解析vip等视频网站底层实现逻辑,接下来就是建站接广告接流量接法院传单。。。。

分析

通过点击播放抓包发现不停的请求http://btrace.video.qq.com/kvcollect,点击发现没有发现,不过这些请求都是m3u8格式片段,我们大胆猜测这是m3u8,将视频切成无数ts小段,一段一段加载播放,可以根据网络状况自动切换视频清晰度,保障流畅播放。那么这种播放方式前端一定有请求索引文件保存ts文件,保存了网络url链接,这些链接顺序播放就完成了整个视频的播放。而我们数据抓取只需要关注这个文件,m3u8转为mp4格式,具体转换代码见https://github.com/OneJane/datautil

废话一堆,开始寻找m3u8请求,通过过滤筛选找到该请求后,http://58.216.106.14/omts.tc.qq.com/AjzDO1DrTOFhSuI4sQa-qkOmtUv8yq9UrejaLeSKpF2M/uwMROfz2r57BIaQXGdGnC2deOm7WRbkbfdWCxMUsemsF2Gfz/svp_50001/NO3RnkZVfa4hKoQCijd2VpXELo5sw-cgX_CMZmI7XeU8ZfKGTirnxL1xJGXvDq2mliBQiL2MqcB6egr3lk7nk3wyBP18yb1lGlcVFNCkQ82kNml8GgGg4BbokC6yjxDcIJIVugQ8OkIG6GOCUijW9a3QpotWcmbZTrCI5kfpcYxax9isGSZL7Q/szg_3772_50001_0bc3hqaciaaameaajrug6rqvcpgdeq6aajca.f304110.ts.m3u8?ver=4

这个请求除去域名外所有参数都加密了,随便找一段加密参数随便搜搜,找到了目标请求http://vd.l.qq.com/proxyhttp

通过json格式化vinfo参数可以判断url+pt即可拿到我们想要的m3u8文件

好戏开场了

参数做减法,去除adparamvinfoparam中的logintokenehost,重点关注guidflowidcKeytm就是时间戳,vid就是请求中的视频id

spsrt=1
charge=0
defaultfmt=auto
otype=ojson
guid=a66e56d20401a21c8a35e92ad94eebde
flowid=82a7dcecd17cede6b9dc02a39571c4bc_70201
platform=70201
sdtfrom=v1104
defnpayver=0
appVer=3.4.40
host=v.qq.com
refer=v.qq.com
sphttps=0
tm=1637204401
spwm=4
vid=s3306hychob
defn=
fhdswitch=0
show1080p=0
isHLS=1
dtype=3
sphls=1
spgzip=
dlver=
hdcp=0
spau=1
spaudio=15
defsrc=1
encryptVer=9.1
cKey=doItDoTdYRR79ZEItZs_lpJX5WFNi2CdS8kE1h7qVaqtHEZQ1c_X6m2O8hQenWPBG5hnGM2UODs52vPBr7VR-rE3OCFTLlH3-xN1QMZmGWCleJdQ62v6N6dvhRBy86U5pyEtRx0KHILNluNDEH6IC8EOljLQ2VfW2sTdospNPlD9535CNT9iSo3cLRH93ogtX_OJeYNVWrDYS8btjkFpGl3F3IxmISJc_8dRIBruTik-e4rt0isxZAXexKqWDJGxu2qxHq-QxHER_ek2fB1T6ywJriVO0ksOGo7_XQLdE-FshP9ARvdxQlEJPKWtziEF2xwGBgYGBgY0KhFT

打上xhr断点vd.l.qq.com/proxyhttp,在调用栈中r.requestPostCgi中参数已经生成,继续向上追溯

追溯到requestNewGetinfoImpl,该方法中p.requestPostCgi发起请求参数已经封装完成,手动调用this.getInfoConfig("vinfoad", i)时cKey及guidflowid都已经加密完成。

进入getInfoConfig

在如下这段代码中guid不存在时,guid: this.context.dataset.guid将通过l.getUserId(this.context.config)

this.context.dataset.guid || (this.context.config.guid = l.getUserId(this.context.config),
this.context.dataset.guid = this.context.config.guid);

由于l = o(436),进入436: function(e, t, o)找到了getUserId,当从本地或者内存中取不到时调用d.createGUID(),也就是随机32位字符串。

getUserId: function(e) {var t = e.guid || d.getData(i.localStorageKey.userId);return e.guid || !d.browser.pcqqlive && !d.browser.macqqlive || (t = (t = d.getPcClientGuid()) || d.getData(i.localStorageKey.userId)),t || (t = d.createGUID(),d.setData(i.localStorageKey.userId, t)),t
},
createGUID: function(e) {e = e || 32;for (var t = "", o = 1; o <= e; o++) {t += Math.floor(16 * Math.random()).toString(16)}return t
},

至于flowid跟踪代码得知是通过随机32位字符串_70201

updatePlayerId: function() {this.context.dataset.playerId = l.createGUID(),this.context.dataset.flowid = this.context.dataset.playerId + "_" + this.context.dataset.platform
},

在执行m(s)前s中没有cKey的值,执行后cKey生成

cKey逆向

可以将整个js拷贝出来放到vs code中分析。

修改如下

var createGUID = function(e) {e = e || 32;for (var t = "", o = 1; o <= e; o++) {t += Math.floor(16 * Math.random()).toString(16)}return t
}
// n函数存在走 (e.encryptVer = "9.1",n(e.platform, e.appVer, e.vids || e.vid, "", e.guid, e.tm))
function m(e) {// var t = n ? (e.encryptVer = "9.1",// n(e.platform, e.appVer, e.vids || e.vid, "", e.guid, e.tm)) : (e.encryptVer = "8.1",// a(e.vids || e.vid, e.tm, e.appVer, e.guid, e.platform));var t = n("70201", "3.4.40", "s3306hychob", "", createGUID(), Date.parse(new Date()).toString().substr(0,10))return t
}

进入n函数

n = r.cwrap("getckey", "string", ["number", "string", "string", "string", "string", "number"]),
h.cwrap = function(e, t, o, i) {return function() {return u(e, t, o, arguments)}
}

进入u函数,其中w函数就是判断存在否则异常报错,Ge根据值是否存在校验跑错,没有操作逻辑。

// function w(e, t) {
//     e || Ge("Assertion failed: " + t)
// }
// function Ge(t) {
//     h.onAbort && h.onAbort(t),
//     t = void 0 !== t ? (r(t),
//     y(t),
//     JSON.stringify(t)) : "",
//     v = !0,
//     0;
//     var o = "abort(" + t + ") at " + P();
//     throw $e && $e.forEach(function(e) {
//         o = e(o, t)
//     }),
//     o
//     return t
// }
function w(e, t) {return e
}
function u(e, t, o, i, n) {var r, a, s = (w(a = h["_" + (r = e)], "Cannot call unknown function " + r + ", make sure it is exported"),a), c = [], d = 0;if (w("array" !== t, 'Return type should not be "array".'),i)for (var l = 0; l < i.length; l++) {var u = x[o[l]];u ? (0 === d && (d = je()),c[l] = u(i[l])) : c[l] = i[l]}var f, p = s.apply(null, c);return f = p,p = "string" === t ? T(f) : "boolean" === t ? Boolean(f) : f,0 !== d && He(d),p
}

打印出h["_" + (r = e)],进入后调用了wasm中的函数,即0005098e中的函数,截止到目前为止如果不手动新建wasm对象,逆向cKey将无法继续进行下去

wasm

搜索wasm请求,下载wasm文件,该文件在web环境中作为体积小且加载快的二进制格式指令集合,我们不关心底层编译实现,直接通过api调用完成逆向分析。

const fs = require('fs');
var wasm_data = fs.readFileSync('./ckey.wasm')
var buffer = new Uint8Array(wasm_data);
var wasmobject = new WebAssembly.Instance(new WebAssembly.Module(buffer));

报错:WebAssembly.Instance(): Imports argument must be present and must be an object

var wasm_env = {
};
var wasmobject = new WebAssembly.Instance(new WebAssembly.Module(buffer), wasm_env);

报错:Import #0 module="env" error: module is not an object or function

重点关注445: function(Ke, e, t)return WebAssembly.instantiate(e, c)

var s, c = {global: null,env: null,asm2wasm: g,parent: h
};
var g = {"f64-rem": function(e, t) {return e % t},debugger: function() {}
};

// 由于h的实现太过复杂,目前只用{}替代
var wasm_env = {global: {},env: {},asm2wasm: {"f64-rem": function(e, t) {return e % t},debugger: function() {}},parent: {}
};

报错:Import #0 module="env" function="enlargeMemory" error: function import requires a callable。在h.asmLibraryArg中查看环境变量信息,由于Ge函数只校验参数抛错,所以直接用空函数代替

enlargeMemory: K
function K() {G()
}
function G() {Ge("Cannot enlarge memory arrays. Either (1) compile with  -s TOTAL_MEMORY=X  with X higher than the current value " + Q + ", (2) compile with  -s ALLOW_MEMORY_GROWTH=1  which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with  -s ABORTING_MALLOC=0 ")
}

添加环境变量中的env参数enlargeMemory

var fun_ = function () { };
var wasm_env = {global: {},env: {enlargeMemory: fun_,},asm2wasm: {"f64-rem": function(e, t) {return e % t},debugger: function() {}},parent: {}
};

报错:Import #1 module="env" function="getTotalMemory" error: function import requires a callable,以上同理,以空函数代替

var wasm_env = {global: {},env: {abort: fun_,assert: fun_,enlargeMemory: fun_,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_,flush_NO_FILESYSTEM: fun_,},asm2wasm: {"f64-rem": function(e, t) {return e % t},debugger: function() {}},parent: {}
};

报错:Import #1 module="env" function="getTotalMemory" error: function import requires a callable

var Q = 16777216
getTotalMemory: function () { return Q },

报错:Import #20 module="env" function="_get_unicode_str" error: function import requires a callable

_get_unicode_str: function () {  function a(e) {return e ? 48 < e.length ? e.substr(0, 48) : e : ""}var e = function () {var e = document.URL, t = window.navigator.userAgent.toLowerCase(), o = "";0 < document.referrer.length && (o = document.referrer);try {0 == o.length && 0 < opener.location.href.length && (o = opener.location.href)} catch (e) { }var i = window.navigator.appCodeName, n = window.navigator.appName, r = window.navigator.platform, e = a(e), o = a(o);return e + "|" + (t = a(t)) + "|" + o + "|" + i + "|" + n + "|" + r}(), t = q(e) + 1, o =Ve(t);// 5250872; //_malloc(t);console.log('---',t, o)return S(e, o, t + 1),o
},

报错:Import #21 module="env" function="memoryBase" error: global import must be a number or WebAssembly.Global object

报错:Import #22 module="env" function="tableBase" error: global import must be a number or WebAssembly.Global object

memoryBase: 1024,
tableBase: 0,

报错:Import #23 module="env" function="DYNAMICTOP_PTR" error: global import must be a number or WebAssembly.Global object

报错:Import #24 module="env" function="tempDoublePtr" error: global import must be a number or WebAssembly.Global object

报错:Import #25 module="env" function="STACKTOP" error: global import must be a number or WebAssembly.Global object

报错:Import #26 module="env" function="STACK_MAX" error: global import must be a number or WebAssembly.Global object

DYNAMICTOP_PTR: 7968,
tempDoublePtr: 7952,
tempDoublePtr: 7952,
STACK_MAX: 5250864,

报错:Import #27 module="global" function="NaN" error: global import must be a number or WebAssembly.Global object

global: {NaN: NaN,Infinity: 1 / 0
}

报错:Import #29 module="env" function="memory" error: memory import must be a WebAssembly.Memory object

搜索WebAssembly.Memory

var Q = 16777216, j = 65536;
var wasmMemory = new WebAssembly.Memory({  initial: Q / j,maximum: Q / j
})

报错:Import #30 module="env" function="table" error: table import requires a WebAssembly.Table

table: new WebAssembly.Table({initial: 99,maximum: 99,element: "anyfunc"
}),

初始化好wasm后,开始处理function u(e, t, o, i, n)

// h["_" + (r = e)] = wasm._getckey
// je = wasm.stackSave
// He = wasm.stackRestore
// Fe = wasm.stackAlloc
// Ve = wasm._malloc  修改_get_unicode_str中的Ve
function _getckey() {return wasmobject.exports._getckey.apply(null, arguments)
}
function stackSave() {   return wasmobject.exports.stackSave.apply(null, arguments)
}
function stackRestore() {return wasmobject.exports.stackRestore.apply(null, arguments)
}
function stackAlloc() {   return wasmobject.exports.stackAlloc.apply(null, arguments)
}
function _malloc() {   return wasmobject.exports._malloc.apply(null, arguments)
}// 函数引用完成n函数
function S(e, t, o) {  // o(a, b, c)return w("number" == typeof o, "stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!"),E(e, C, t, o)
}
function T(e, t) {if (0 === t || !e)return "";for (var o, i = 0, n = 0; w(e + n < Q),i |= o = C[e + n >> 0],(0 != o || t) && (n++,!t || n != t); );t = t || n;var r = "";if (i < 128) {for (var a; 0 < t; )a = String.fromCharCode.apply(String, C.subarray(e, e + Math.min(t, 1024))),r = r ? r + a : a,e += 1024,t -= 1024;return r}return _(C, e)
}
var l = {stackSave: function() {stackSave()},stackRestore: function() {stackRestore()},arrayToC: function(e) {var t, o, i = stackAlloc(e.length); // Fe(e.length);return o = i,w(0 <= (t = e).length, "writeArrayToMemory array must have a length (should be an array or typed array)"),R.set(t, o),i},stringToC: function(e) {var t, o = 0;return null != e && 0 !== e && (t = 1 + (e.length << 2),S(e, o = stackAlloc(t), t)), //Fe(e.length);o}
}
var x = {string: l.stringToC,array: l.arrayToC
}
function n(...args) {var e = "getckey"var t = "string"var o = ["number", "string", "string", "string", "string", "number"]var i = argsvar n = undefinedvar r, a, s = (w(a = _getckey, "Cannot call unknown function " + r + ", make sure it is exported"),a), c = [], d = 0;// var r = "getckey", a = _getckey, s = _getckey, c = [], d = 0;// if (w("array" !== t, 'Return type should not be "array".'),// i)for (var l = 0; l < i.length; l++) {var u = x[o[l]];console.log("uuuu",u)u ? (0 === d && (d = stackSave()),  // je()c[l] = u(i[l])) : c[l] = i[l]}var f, p = s.apply(null, c);return f = p,p = "string" === t ? T(f) : "boolean" === t ? Boolean(f) : f,0 !== d && stackRestore(d), // He(d)p
}

报错:TypeError: Cannot set property '7984' of undefined 说明在内存操作的时候有部分变量我们没有注意到,回到445: function(Ke, e, t)中,抽出部分值操作

function X() {R = new Int8Array(k),O = new Int16Array(k),I = new Int32Array(k),C = new Uint8Array(k),M = new Uint32Array(k)
}
X()

报错:document is not definedwindow is not definedCannot read property 'userAgent' of undefined...

var document = {URL: "",referrer: ""
}
var window = {document: document,navigator: {userAgent: "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36",appCodeName: "Mozilla",appName: "Netscape",platform: "Win32"},
};

node ckey_blog.js

主动调用

由于公众号和正常腾讯视频中的部分参数不一致,修改function m(e)

function getCKey(plateform,appVer,vid) {// var t = n ? (e.encryptVer = "9.1",// n(e.platform, e.appVer, e.vids || e.vid, "", e.guid, e.tm)) : (e.encryptVer = "8.1",// a(e.vids || e.vid, e.tm, e.appVer, e.guid, e.platform));var t = n(plateform, appVer, vid, "", createGUID(), Date.parse(new Date()).toString().substr(0, 10))return t
}

ckey.py

import execjs
import re
import requests
import jsonfrom m3u8 import M3u8Downloadwith open("ckey_blog.js", "r",encoding="utf-8") as f:js_code = f.read()# target_url = "http://v.qq.com/txp/iframe/player.html?origin=https%3A%2F%2Fmp.weixin.qq.com&chid=17&vid=s3306hychob&autoplay=false&full=true&show1080p=false&isDebugIframe=false"
target_url = "https://v.qq.com/x/cover/bzfkv5se8qaqel2/j002024w2wg.html"
vinfoparam = "spsrt=1&charge=0&defaultfmt=auto&otype=ojson&guid={}&flowid={}&platform={}&sdtfrom={}&defnpayver=0&appVer={}&host=v.qq.com&sphttps=0&tm=1637237951&spwm=4&vid={}&defn=&fhdswitch=0&show1080p=0&isHLS=1&dtype=3&sphls=1&spgzip=&dlver=&hdcp=0&spau=1&spaudio=15&defsrc=1&encryptVer=9.1&cKey={}"
data = {}
data["buid"] = "vinfoad"
guid = execjs.compile(js_code).call('createGUID')
# 区分腾讯视频还是公众号视频
if "mp.weixin.qq.com" in target_url:vid = re.compile(r"&vid=(.*?)&").findall(target_url)[0] # ?非贪婪plateform = "70201"flowid = execjs.compile(js_code).call('createGUID') + "_" + plateformsdtfrom = "v1104"appVer = "3.4.40"ckey = execjs.compile(js_code).call('getCKey',plateform,appVer,vid)
else:vid = target_url.split("/")[-1].split(".")[0]plateform = "10201"flowid = execjs.compile(js_code).call('createGUID') + "_" + plateformsdtfrom = "v1010"appVer = "3.5.57"ckey = execjs.compile(js_code).call('getCKey', plateform, appVer, vid)data["vinfoparam"] = vinfoparam.format(guid,flowid,plateform,sdtfrom,appVer,vid,ckey)
result = requests.post('http://vd.l.qq.com/proxyhttp', data=json.dumps(data)).json()
# print(result)
if result.get("errCode") == 0:url_data = json.loads(result.get("vinfo"))["vl"]["vi"][0]["ul"]["ui"][0]url = url_data["url"]+url_data["hls"]["pt"]print(url)M3u8Download(url,"test1",max_workers=64,num_retries=10,)

总结

针对wasm二进制方式加密的js逆向,类似安卓的so逆向,可以选择硬肛分析汇编代码,当然也可以选择Unidbg主动调用,本文利用js的WebAssembly实例化wasm并完成调用分析cKey,完成腾讯系视频的下载,至于会员视频分析logintoken参数,下次一定,下次一定。。。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

JS逆向之x讯视频wasm的ckey分析相关推荐

  1. JS逆向——一个新的视频爬虫

    仅限技术交流和学习记录,严禁用于任何商业用途,否则后果自负,侵删 个人觉得坑还挺多,但难度不算大的一篇js逆向. 来吧,先分析. 起初解析pc网页端,感觉有点难度,然后就转到移动网页端了,其实是一模一 ...

  2. js特效之腾讯视频的图片轮播

    今天搞了一天的javascript,准备做一个特效图片轮播,现在晚上十二点,中午十二点我开始搞的,到了现在还没有搞好还差一个鼠标移进移出的暂定和播放,其实这是一个很简单的特效,就是从刚开始的css的布 ...

  3. python爬虫JS逆向:X咕视频密码与指纹加密分析

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:煌金的咸鱼 PS:如有需要Python学习资料的小伙伴可以加点击下方 ...

  4. js逆向之腾讯漫画《附源码》

    想看漫画但是没有VIP,想要爬取付费漫画:这是不可能滴 搜索到的程序要么是通过自动化,要么是代码有点老旧:互联网总是更新发展的嘛. 自动化实在是太慢了,一个520章的漫画得下载一天一夜这不符合搞爬虫的 ...

  5. gta python解指纹_python爬虫JS逆向:X咕视频密码与指纹加密分析

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:煌金的咸鱼 正文 先来看看今天的受害者: aHR0cDovL3d3d ...

  6. 逆向超时代加密视频无密码还原提取分析

    [免责说明] 本贴仅用于交流经验之用,只可用于个人研究试验,不得用于商业用途. 请在下24小时后删除,若产生不良后果,使用者应自已承担.与本人无关. 文章未经作者同意,禁止转载!如有侵权可联系simo ...

  7. [JS逆向案例]诸葛找房Cookie之acw_sc__v2分析

    目录 文章目录 目录 声明 逆向目标 抓包分析 参数定位 混淆还原 编写还原文件 执行还原命令 查看还原结果 参数分析 最终效果 部分代码 完整代码 声明 本文章中所有内容仅供学习交流,抓包内容.敏感 ...

  8. JS逆向-新榜数据nonce和xyz参数分析

    分析网站: https://www.newrank.cn/public/info/list.html?period=week&type=data 这次我们要分析的是该网站的接口中的nonce和 ...

  9. Python爬虫进阶--js逆向-某天下与某某二手房密码加密分析

    X天下密码加密分析 本次的受害者: aHR0cHM6Ly9wYXNzcG9ydC5mYW5nLmNvbS8= 分析 通过输入错误密码抓包查看加密字段.如下图: 直接通过检索pwd:定位加密位置如下图: ...

最新文章

  1. 【每日一题】 牛客 密码强度等级
  2. C语言递归方式实现冒泡排序(bubble排序)算法(附完整源码)
  3. ie6 下最佳 PNG透明方案【转】
  4. 如何revert一个merged branch上所有的改动
  5. tinyxml库使用实例
  6. 外卖侠cps V5.6版本小程序源码_支持多种CPS收益和流量主收益
  7. 通过银行卡号获取银行名称和银行图标的ICON
  8. 计算机导论dos实验报告,计算机导论实验报告-DOS常用命令的使用.doc
  9. Python海龟库write方法中形参font用法的记录
  10. 【教程】NEC e-Border Client的设置图文教程(中文版)
  11. IDEA导入项目不显示项目结构src解决
  12. 互联网日报 | 贾跃亭宣布破产重组完成;小米发布首款OLED电视;湖南迎来首家本土航空公司...
  13. SUBTYPE正规化数据类型
  14. 听说想当黑客的都玩过这个Monyer游戏
  15. 20dbm是多少mw
  16. 进程的基本概念及操作
  17. unpack python_python数据处理之 ddt,@data, @unpack
  18. Linux 安装 JDK + Tomcat + Mysql
  19. matlab regress
  20. 网络资源的定义--URI,URL,URN

热门文章

  1. Codeforces#232 A
  2. 支付软件Venmo的101亿美元是怎么赚来的?
  3. MATLAB模拟伽尔顿板实验
  4. 商业地产招商的十大误区(转)
  5. Fzu软工第一次作业-准备篇
  6. 30+行业头部企业相聚杭城,创邻科技“Graph+X”生态合作伙伴大会成功举办
  7. 人大金仓共建“深圳市教育技术信创实验室”,加速推进教育数字化转型
  8. 概率及常用概率分布的实现——计算机视觉修炼之路(零)
  9. 什么是AMF?AMF0和AMF3
  10. jQuery 将本地时间转换成 UTC 时间,计算时差,将UTC时间转换成 本地 时间