Kingkong直播弹幕姬分析(二)

科技是第一生产力

Posted by 恋 on January 24, 2019

Kingkong直播

Stage 4

放弃了一条路,当然要找到其他的方案。对于网页,我们有一个基础操作叫F12。在这里的演示将使用Chrome而不是我平常使用的Firefox - 因为Chrome对娱乐(视频、Flash等)的支持比较好…

首先打开直播,等一会儿看连接。鼠标在时间轴上后来的地方随便拖一下即可选中。如果没问题的话,你应该能看到7个请求。当然也有可能不是,我这七个当中有一个是登陆以后才有的。

这里有三个?EIO=3&transport=websocket的请求,一个主播号+随机串.flv(十分显然就是直播的视频内容),一个unread?type=all,一个user?anchor_uid=主播号&psize=5&type=week,一个anchor?anchor_uid=主播号&psize=5&type=week。感觉前三个其中就有答案,但是十分悲伤,我对WebSocket毫无了解 - 也无法测试是否正确。

我扫了一眼,发现后面的三个看起来像是登陆后的啥操作,于是开了个新的小窗抓。这下只剩三个了:视频和两个WebSocket。点开就可以发现,其中有一个的地址和之前Aardio的非常相似:wss://cht-1.ws.kingkong.com.tw/chat_nsp/wss://ctl.ws.kingkong.com.tw/control_nsp/

感谢伟大的开发者工具,WebSocket的内容变得十分简单。ctl的那个内容全部都是42/control_nsp开头的玩意儿,目测没有也能正常用,所以先不管。认真分析42/chat_nsp开头的。看着名字,很明显这就是聊天弹幕!

仔细看看收到的内容。

第一行是服务器发来的:

0{sid: "长度20的base64 binary", upgrades: [], pingInterval: 50000, pingTimeout: 60000}

这个神奇的sid我没看出来到底是啥,但是反正是服务器发给我的。

接下来就是中规中矩的普通交流:

40/chat_nsp,

40

40/chat_nsp

42/chat_nsp,["authentication",{"live_id":"主播号+随机串","anchor_pfid":主播号,"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsaXZlX2lkIjoiMjc2Mjg4M0czOTA3N0ZyWWoiLCJwZmlkIjoiNzZmOGZmYzJlNWU0NGIwZGFiZWQxZmU1ZWYzZmMzZDEiLCJuYW1lIjoi6Kiq5a6iZmMzZDEiLCJhY2Nlc3NfdG9rZW4iOm51bGwsImx2IjoxLCJmcm9tIjoxLCJmcm9tX3NlcSI6MSwiY2hhbm5lbF9pZCI6MSwiY2xpZW50X3R5cGUiOiJ3ZWIifQ.tR8ylro0WMXIfmJ2nW3RoFwOmxwEVAH5UumwGWgzE_s","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsaXZlX2lkIjoiMjc2Mjg4M0czOTA3N0ZyWWoiLCJwZmlkIjoiNzZmOGZmYzJlNWU0NGIwZGFiZWQxZmU1ZWYzZmMzZDEiLCJuYW1lIjoi6Kiq5a6iZmMzZDEiLCJhY2Nlc3NfdG9rZW4iOm51bGwsImx2IjoxLCJmcm9tIjoxLCJmcm9tX3NlcSI6MSwiY2hhbm5lbF9pZCI6MSwiY2xpZW50X3R5cGUiOiJ3ZWIifQ.tR8ylro0WMXIfmJ2nW3RoFwOmxwEVAH5UumwGWgzE_s","from":"WEB","client_type":"web","r":0}]

42/chat_nsp,["authenticated",true]

然后就是弹幕了。

Stage 5

很明显,关键就是分析这段东西了。主播号和live_id很明显,不管。这个access_token和token一模一样,就是我们要分析的东西。根据Aardio的代码,这段东西是通过eval一段js获得的 - 看起来eval很不靠谱啊…

我把截图放到了dalao群里,然后有个大佬貌似认出了这是啥(emmmm 不懂,幸好我当时没有头铁去分析那个压缩的js… 那tm是个加密库)

那么说,现在可以靠谱地分析一下了。这是base64编码的字符串 - 并且里面有两个点。很明显,点肯定不是base64里的一部分 - 那么暂且当成分隔符分成三部分。

第一部分:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

第二部分:eyJsaXZlX2lkIjoiMjc2Mjg4M0czOTA3N0ZyWWoiLCJwZmlkIjoiNzZmOGZmYzJlNWU0NGIwZGFiZWQxZmU1ZWYzZmMzZDEiLCJuYW1lIjoi6Kiq5a6iZmMzZDEiLCJhY2Nlc3NfdG9rZW4iOm51bGwsImx2IjoxLCJmcm9tIjoxLCJmcm9tX3NlcSI6MSwiY2hhbm5lbF9pZCI6MSwiY2xpZW50X3R5cGUiOiJ3ZWIifQ

第三部分:tR8ylro0WMXIfmJ2nW3RoFwOmxwEVAH5UumwGWgzE_s

很稳,直接上.jpg

头铁又莽的我直接把这东西丢到了在线解码工具里。这些工具好用得一笔,十分方便地给出了我想要的结果… 唯一的问题是,省略了一点儿细节。第一部分的内容是

{"alg":"HS256","typ":"JWT"}

第二部分则为

{"live_id":"主播号+随机串","pfid":"76f8ffc2e5e44b0dabed1fe5ef3fc3d1","name":"訪客fc3d1","access_token":null,"lv":1,"from":1,"from_seq":1,"channel_id":1,"client_type":"web"}

目测发现,pfid的后五位就是访客的具体编号。虽然不知道是怎么生成的,但是暂时还行吧。

那么问题来了,第三部分。

首先 这里面有个下划线… 我完全不知道这下划线是啥操作,但是假装它是+或/吧。看起来区别不是很大,基本上就只是一个字节。

b5 1f 32 96 ba 34 58 c5 c8 7e 62 76 9d 6d d1 a0 5c 0e 9b 1c 04 54 01 f9 52 e9 b0 19 68 33 13 fb (如果是+的话,最后一个字节改为eb)

这玩意儿解完是binary没法看,我也猜不出来,那自然就又要换条路了。

根据第一部分解出来的东西,我们可以找到一个关键词:HS256。我相信这玩意儿不是凭空出现的,也不是711那样来的,所以就去搜了一下。

Stage 6

在Kingkong/js/template/live.js里,我们成功搜到了HS256。它大概长这样:

DETAIL@ph

代码 点我展开
var o = function() {
    function t(e) {
        var i = e.liveId
          , a = e.liveKey
          , o = e.relColor
          , s = e.pfid
          , r = e.name
          , l = e.accessToken
          , c = e.gradeId
          , d = e.gradeLv
          , u = e.limit
          , h = e.lv
          , f = e.from
          , g = e.fromSeq
          , m = e.channelId
          , v = e.anchor
          , p = e.follow
          , y = e.sex
          , _ = e.balance
          , b = e.sun
          , k = e.balanceNoble
          , w = e.mute
          , $ = e.relation
          , I = e.avatar
          , T = e.signed
          , C = e.nobleLevel
          , S = n(e, ["liveId", "liveKey", "relColor", "pfid", "name", "accessToken", "gradeId", "gradeLv", "limit", "lv", "from", "fromSeq", "channelId", "anchor", "follow", "sex", "balance", "sun", "balanceNoble", "mute", "relation", "avatar", "signed", "nobleLevel"]);
        !function(t, e) {
            if (!(t instanceof e))
                throw new TypeError("Cannot call a class as a function")
        }(this, t),
        this.liveId = i,
        this.liveKey = a,
        this.pfidName = "localPFID",
        this.accessToken = l,
        this.pfid = s || S.uid || this._getPFID(),
        this.name = r || S.nickname || this._getName(this.pfid),
        this.relColor = o || "#ffffff",
        this.gradeId = c || S.grade_id || 1,
        this.gradeLv = d || S.grade_lvl || 1,
        this.lv = h || 1,
        this.from = f || 1,
        this.fromSeq = g || 1,
        this.channelId = m || 1,
        this.anchor = v || !1,
        this.admin = function(t, e) {
            return (t & 1 << e) == 1 << e
        }($, 1),
        this.follow = p || 0,
        this.sex = y || 0,
        this.balance = _ || 0,
        this.sun = b || 0,
        this.balanceNoble = k || S.balance_noble || 0,
        this.mute = w || 0,
        this.relation = $,
        this.avatar = I || 0,
        this.limit = u,
        this.signed = T,
        this.nobleLevel = C || S.nlv || 0,
        this.nobleHornNum = S.noble_horn_cnt || 0,
        this.nobleState = S.nst || 0,
        this.token = this.getToken()
    }
    return function(t, e, i) {
        e && a(t.prototype, e),
        i && a(t, i)
    }(t, [{
        key: "_getPFID",
        value: function() {
            return localStorage && localStorage[this.pfidName] ? localStorage[this.pfidName] : this._makePFID()
        }
    }, {
        key: "_makePFID",
        value: function() {
            var t = UUID.generate().toString().replace(/-/g, "");
            return t = t.substring(0, t.length),
            localStorage && (localStorage[this.pfidName] = t),
            t
        }
    }, {
        key: "_getName",
        value: function(t) {
            var e;
            return t && (e = "訪客" + t.toString().substring(t.length - 5, t.length)),
            e
        }
    }, {
        key: "getToken",
        value: function() {
            var t = {};
            t.live_id = this.liveId,
            t.pfid = this.pfid,
            t.name = this.name,
            t.access_token = this.accessToken,
            t.lv = this.lv,
            t.from = this.from,
            t.from_seq = this.fromSeq,
            t.channel_id = this.channelId,
            t.client_type = "web";
            function e(t) {
                var e = CryptoJS.enc.Base64.stringify(t);
                return e = (e = (e = e.replace(/=+$/, "")).replace(/\+/g, "-")).replace(/\//g, "_")
            }
            var i = e(CryptoJS.enc.Utf8.parse(JSON.stringify({
                alg: "HS256",
                typ: "JWT"
            }))) + "." + e(CryptoJS.enc.Utf8.parse(JSON.stringify(t)))
              , n = this.liveKey
              , a = CryptoJS.HmacSHA256(i, n);
            return i + "." + (a = e(a))
        }
    }]),
    t
}()

DETAILEND@ph

好的,十分成功地找到了整套相关代码。那么逻辑暂时也明确了:

第一行是base64(utf8({"alg":"HS256","typ":"JWT"}))

第二行是base64(utf8({live_id,pfid,name,access_token,lv,from,from_seq,channel_id,client_type}))

pfid的值只是个客户端随机生成的值UUID.generate().toString().replace(/-/g, "")

而第三部分是base64(HMACSHA256(一二行, key=liveKey))

上面调用的base64会把所有的+替换成-,把/替换成_

那么,暂时先这样吧… 我去找个WebSocket库试试先…