1.开门见山,机票数据都需要一个blackbox的数据,这个就是由无感相关生成的。直接搜索Blackbox就可以找到最后赋值的地方,然后下断点刷新,往上跟调用栈就可以找到回调函数14916_16.png

//oooooQ()函数生成的值就是blackbox
function OOO0OO() {
    if (O0QQ0Q) return;
    O0QQ0Q = true, O00Q0Q(Qoo0oO["success"]) && Qoo0oO["success"](oooooQ());
}

这个函数是走下面的else的,因为status为255,在前面发完profile.json这个包之后设置了status=255,这个tokens就是tokenId.

function oooooQ() {
    oOOo00["blackBox"] = {}, oOOo00["blackBox"]["v"] = Qoo0oO["version"], oOOo00["blackBox"]["os"] = 3;
    if (Qoo0oO["status"] % 255) {
        if (window["navigator"] && window["navigator"]["cookieEnabled"]) {
            if (!Qoo0oO["strictMode"] && o0O00Q && oooQO0 && o0O00Q["length"] === 16 && oooQO0["length"] === 16 && localStorage && localStorage[o0O00Q] && localStorage[oooQO0] && new window["Date"]()["getTime"]() - Number(localStorage[oooQO0]) <= 86400000) {
                _fmOpt["blackBoxType"] = 1;
                var OQoOo = Q00000(localStorage[o0O00Q]);
                if (Qoo0oO["collectBehavior"] && Qoo0oO["behaviorUrl"] && window["FormData"]) {
                    window["_fmBehaviorBlackbox"] = OQoOo;
                }
                return OQoOo;
            }
        }
        O00Q0O(oOOo00["deviceInfo"]), oOOo00["blackBox"]["e"] = Qoo0oO["status"], _fmOpt["blackBoxType"] = 2, oOOo00["blackBox"]["d"] = window["JSON"]["stringify"](oOOo00["deviceInfo"]);
    } else {
//这部分就是通过profile.json接口返回的token生成blackbox, 用到Q00000方法
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        if (Qoo0oO["tokens"]) {
            oOOo00["blackBox"] = Qoo0oO["tokens"], _fmOpt["blackBoxType"] = 1, setTimeout(function () {
                try {
                    if (window["localStorage"] && o0O00Q && oooQO0 && o0O00Q["length"] === 16 && oooQO0["length"] === 16) {
                        localStorage[oooQO0] = new window["Date"]()["getTime"](), localStorage[o0O00Q] = oOOo00["blackBox"];
                    }
                } catch (error) {
                }
            }, 0);
            var QOQ0O = Q00000(oOOo00["blackBox"]); // 这个地方是真正的blackbox
            if (Qoo0oO["collectBehavior"] && Qoo0oO["behaviorUrl"] && window["FormData"]) {
                window["_fmBehaviorBlackbox"] = QOQ0O;
            }
            return QOQ0O;
        }
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        O00Q0O(oOOo00["deviceInfo"]), oOOo00["blackBox"]["msg"] = "no token returned", oOOo00["blackBox"]["e"] = Qoo0oO["status"], oOOo00["blackBox"]["d"] = window["JSON"]["stringify"](oOOo00["deviceInfo"]), _fmOpt["blackBoxType"] = 3;
    }
    if (Qoo0oO["collectBehavior"] && Qoo0oO["behaviorUrl"] && window["FormData"]) {
        window["_fmBehaviorBlackbox"] = "tdfp" + OoQooO(window["JSON"]["stringify"](oOOo00["blackBox"]), 1);
    }
    return "tdfp" + OoQooO(window["JSON"]["stringify"](oOOo00["blackBox"]), 1);
}

Q00000函数如下,这个方法固定的可以直接用,用tokenid生成blackbox:

function Q00000(OQoOo) {
    {
        if (OQoOo["length"] !== 23) {
            return OQoOo;
        }
        var ooQ00 = "";
        var o0O00 = ["ghijklmnopqrstuv"["charAt"]("0123456789abcdef"["indexOf"](OQoOo["substring"](0, 1))), OQoOo["substring"](1, 4), OQoOo["substring"](4, 14), OQoOo["substring"](14, 22), OQoOo["substring"](22, 23)];
        var oOoQo = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
        var OoQoQ = [];
        var OOo00 = 0;
        var OOoOo = 75;
        while (OOoOo) {
            switch (OOoOo) {
                case 77:
                    OOo00++;
                    OOoOo = 75;
                    break;
                case 76:
                    OoQoQ = [oOoQo[parseInt(window["Math"]["random"]() * 62)], oOoQo[parseInt(window["Math"]["random"]() * 62)], oOoQo[parseInt(window["Math"]["random"]() * 62)]];
                    if (o0QOOO["length"] > 1000 || o0QOOO["indexOf"]("" + OoQoQ[0] + OoQoQ[1] + OoQoQ[2]) === -1) {
                        OOo00 = 1000, o0QOOO["push"]("" + OoQoQ[0] + OoQoQ[1] + OoQoQ[2]), ooQ00 = "" + o0O00[0] + o0O00[1] + OoQoQ[0] + o0O00[2] + OoQoQ[1] + o0O00[3] + OoQoQ[2] + o0O00[4];
                    }
                    OOoOo = 77;
                    break;
                case 75:
                    OOoOo = OOo00 < 1000 ? 76 : 0;
                    break;
            }
        }
        if (ooQ00["length"] !== 26) {
            ooQ00 = "" + o0O00[0] + o0O00[1] + OoQoQ[0] + o0O00[2] + OoQoQ[1] + o0O00[3] + OoQoQ[2] + o0O00[4];
        }
        return ooQ00;
    }
}

2.接下来看怎么通过profile.json发包获取tokenid,继续向上跟调用栈。其实这里就是拼接网址发包拿到数据回调处理,把tokenId赋值给tokens

function OOo0oQ() {
    {
        Qoo0oO["status"] = 4, QoooO0(Qoo0oO["fpHost"] + Qoo0oO["jsonUrl"] , function (OQoOo) {    // 发包成功回调函数
            {
                var OOoOo = OQoOo["result"];
                var ooQ00 = OOoOo === undefined ? {} : OOoOo;
                Qoo0oO["timer"] && clearTimeout(Qoo0oO["timer"]);
                if (!ooQ00["tokenId"]) {
                    Qoo0oO["status"] = 200;
                } else {
                    var OoQoQ = function QOQ0O() {
                        {
                            var Q0oooQ = document["createElement"]("iframe");
                            Q0oooQ["sandbox"] = "allow-scripts";
                            var OOo00 = Qoo0oO["iUrl"];
                            if (OOo00 && OOo00[OOo00["length"] - 1] !== "/") {
                                OOo00 += "/";
                            }
                            Q0oooQ["src"] = OOo00 + "i.html", Q0oooQ["width"] = 0, Q0oooQ["height"] = 0, (Q0oooQ["frameElement"] || Q0oooQ)["style"]["cssText"] = "position:absolute !important; z-index:-9999 !important; visibility:hidden !important;border:0 !important";
                            var QOQ0O = function OOo00(OQoOo) {
                                if (OQoOo["data"] === "i init ok" && Q0oooQ["contentWindow"] && Q0oooQ["contentWindow"]["postMessage"]) {
                                    var QOQ0O = Q0O00O["get"](O00o0Q, 255, 2);
                                    Qoo0oO["pxy"] = QOQ0O || "-", Q0oooQ["contentWindow"]["postMessage"](window["JSON"]["stringify"](Qoo0oO), "*");
                                }
                            };
                            if (window["addEventListener"]) {
                                window["addEventListener"]("message", QOQ0O);
                            } else if (window["attachEvent"]) {
                                window["attachEvent"]("onmessage", QOQ0O);
                            }
                            document["body"] && document["body"]["appendChild"](Q0oooQ);
                        }
                    };
                    oOOQOo = ooQ00["xxid"];
                    try {
                        if (ooQ00["c"]) {
                            if (ooQ00["c"]["factor"] !== undefined) {
                                if (window["navigator"] && window["navigator"]["cookieEnabled"]) {
                                    localStorage["_TDfactor"] = ooQ00["c"]["factor"];
                                }
                                Q000oo["factor"] = ooQ00["c"]["factor"];
                            } else {
                                if (window["navigator"] && window["navigator"]["cookieEnabled"]) {
                                    localStorage["_TDfactor"] = 0;
                                }
                                Q000oo["factor"] = 0;
                            }
                            if (ooQ00["c"]["op"] !== undefined) {
                                if (window["navigator"] && window["navigator"]["cookieEnabled"]) {
                                    localStorage["_TDopnum"] = ooQ00["c"]["op"];
                                }
                                Q000oo["op"] = ooQ00["c"]["op"];
                            } else {
                                if (window["navigator"] && window["navigator"]["cookieEnabled"]) {
                                    localStorage["_TDopnum"] = 0;
                                }
                                Q000oo["op"] = 0;
                            }
                            if (window["navigator"] && window["navigator"]["cookieEnabled"]) {
                                localStorage["_TDctimestamp"] = new window["Date"]()["getTime"]();
                            }
                        }
                    } catch (OOQ0QQ) {
                    }
                    if (oOOQOo) {
                        Q0O00O["set"](OQQo0o, oOOQOo);
                    }
                    //tokenId赋值
                    Qoo0oO["tokens"] = ooQ00["tokenId"], Qoo0oO["_xid"] = ooQ00["xdid"];
                    if (Qoo0oO["_xid"]) {
                        Q0O00O["set"](OoOQQQ, Qoo0oO["_xid"]);
                    }
                    if (!Qoo0oO["noIframe"] && OQQ0oO()) {
                        OoQoQ();
                    }
                    //修改状态
                    Qoo0oO["status"] = 255;
                }
               //这个是最上面那个调用success函数
                OOO0OO();
            }
        }, oOOo00["deviceInfo"], function () {
            OOO0OO();
        });
        if (Qoo0oO["partnerSendSwitch"]) {
            try {
                QoooO0(Qoo0oO["partnerFpUrl"], null, oOOo00["deviceInfo"]);
            } catch (e2788) {
                oO0o0O(e2788);
            }
        }
        oOOo00["pageInfo"] = {};
        if (Qoo0oO["detectSwitch"]) {
            oOOo00["pageInfo"]["partnerCode"] = _fmOpt["partner"], oOOo00["pageInfo"]["token_id"] = _fmOpt["token"], oOOo00["pageInfo"]["appName"] = _fmOpt["appName"], oOOo00["pageInfo"]["paramz"] = o0oOo0(), QoooO0(Qoo0oO["fpHost"] + Qoo0oO["detectUrl"], null, oOOo00["pageInfo"]);
        }
        if (Qoo0oO["partnerSendSwitch"]) {
            try {
                QoooO0(Qoo0oO["partnerDetectUrl"], null, oOOo00["pageInfo"]);
            } catch (e2788) {
                oO0o0O(e2788);
            }
        }
    }
}

QoooO0是发包函数,O0ooOO["src"] = OoQOO就是profile.json的网址,这里可以看到网址的拼接过程:

  • v:随js返回版本号

  • idf:时间戳加密

  • w:v加密

  • ct:时间差加密

  • h:前面所有参数拼接的网址hash

function QoooO0(OQoOo, OOo0QO, OoQoQ, QOQ0Q0) {
    {
        var QQoOQ0 = QOOo0Q();
        if (QQoOQ0 && QQoOQ0 <= 8) {
            OQoOo = OQoOo["replace"](/\/web.+\/profile\.json/, "/web3_7/profile.json");
        }
        var OoOo0o = false;
        var O0ooOO = document["createElement"]("script");
        var Oo00Q0 = {};
        var O00o00 = QoQQ00(OOo0QO, Oo00Q0, QOQ0Q0);
        var OoQOO = OQoOo;
        var Q0OQO = [];
        OoQoQ["v"] = Qoo0oO["version"], OoQoQ["idf"] = Qoo0oO["timestamp"], OoQoQ["w"] = QQ0QQQ(Qoo0oO["version"]), OoQoQ["ct"] = QQ0QQQ(new window["Date"]()["getTime"]() - Qoo0oO["jsDownloadedTime"]);
        if (!QQoOQ0 || QQoOQ0 > 8) {
            var QO0QO = new OQOooQ();
            QO0QO["setPublicKey"](QQQQQ0), OoQoQ["idf"] = QO0QO["encrypt"](Qoo0oO["timestamp"]);
        }
        for (var QQQOO in OoQoQ || {}) {
            Q0OQO["push"](QQQOO + "=" + encodeURIComponent(OoQoQ[QQQOO]));
        }
        Q0OQO["push"]("_callback=" + O00o00), OoQOO += OoQOO["indexOf"]("?") > 0 ? "&" : "?", OoQOO += Q0OQO["join"]("&"), OoQOO += "&h=" + OOQQ0O["hash128"](OoQOO["replace"](OQoOo, "")), O0ooOO["id"] = O00o00, O0ooOO["onload"] = function oO0oO() {
            if (!OoOo0o && (!this["readyState"] || this["readyState"] === "loaded" || this["readyState"] === "complete")) {
                OoOo0o = true, O0ooOO["onload"] = null, O0ooOO["onreadystatechange"] = null, Oo00Q0["t"] && clearTimeout(Oo00Q0["t"]);
                if (OOo0QO) {
                    var OQoOo = O00o00;
                    if (window[OQoOo]) {
                        if (QQoOQ0 && QQoOQ0 <= 8 && Qoo0oO["status"] === 4) {
                            O00Q0Q(QOQ0Q0) && QOQ0Q0();
                        }
                        Qoo0oO["status"] = 203;
                    }
                }
            }
        }, O0ooOO["onreadystatechange"] = O0ooOO["onload"], O0ooOO["onerror"] = function oOO0O() {
            if (OOo0QO) {
                Qoo0oO["status"] = 202, Oo00Q0["t"] && clearTimeout(Oo00Q0["t"]);
            }
            O00Q0Q(QOQ0Q0) && QOQ0Q0();
        }, O0ooOO["src"] = OoQOO, setTimeout(function () {
            OQOoo0["insertBefore"](O0ooOO, OQOoo0["firstChild"]);
        }, 0);
    }
}
  • e的生成函数:

function oO00Q0() {
    {
        var ooQ00 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        var OOoOo = "";
        for (var OoQoQ = 0, OOo00 = ooQ00["length"]; OoQoQ < 127; OoQoQ++) {
            OOoOo += ooQ00["charAt"](window["Math"]["floor"](window["Math"]["random"]() * OOo00));
        }
        var QOQ0O = OOoOo["split"]("");
        QOQ0O["splice"](window["Math"]["floor"](window["Math"]["random"]() * (ooQ00["length"] - 1)), 0, "\\");
        return QOQ0O["join"]("");
    }
}
  • i,j,k,l,o(也有可能是a,b,c,d之类的,是因为有两个版本js)都是环境检测相关的,然后通过加密生成,生成位置如下:

OOoOoo["forEach"](function (OQoOo, QOQ0O) {
                var OoOoQo = [];
                var QQoooO = QOQ0O > 3 ? QOQ0O + 2 : QOQ0O;
                var Q0OOoo = new window["Date"]()["getTime"]()["toString"](32);
                OQoOo["forEach"](function (OQoOo, QOQ0O) {
                    var OoQoQ = void 0;
                    if (OQoOo["z"]) {
                        var OOo00 = {};
                        OOo00["task"] = typeof OQoOo["y"] === "function" ? OQoOo["y"]() : OQoOo["y"], OOo00["index"] = QQoooO, OOo00["tIndex"] = QOQ0O, OOo00["values"] = OoOoQo, OOo00["type"] = OQ0QQQ["indexOf"](OQoOo["m"]), OOo00["now"] = Q0OOoo, oOQ0oo["push"](OOo00), OoOoQo["push"]("-");
                        return;
                    }
                    if (OQoOo["w"]) {
                        var OOoOo = {};
                        OOoOo["task"] = OQoOo["y"], OOoOo["index"] = QQoooO, OOoOo["tIndex"] = QOQ0O, OOoOo["values"] = OoOoQo, OOoOo["type"] = OQ0QQQ["indexOf"](OQoOo["m"]), OOoOo["now"] = Q0OOoo, QOQ00Q["push"](OOoOo), OoOoQo["push"]("-");
                        return;
                    }
                    OoQoQ = oQO0OO[OQoOo["x"]](OQoOo); 
                    OoOoQo["push"](Q0ooQQ(OoQoQ, OQ0QQQ["indexOf"](OQoOo["m"])));   //执行检测环境函数
             }), window[OO0Q0O](oOOo00["deviceInfo"], window["String"]["fromCharCode"](97 + QQoooO), QQ0QQQ, OoOoQo, Q0OOoo); //环境检测完拿到的结果进行加密,并存入deviceInfo

            })

最后我建议是补环境,原因是有两套js随机返回,一套是abcd参数AES加密,一套是ijkl参数3DES加密,而且base64_map的字符串也有两套,v参数也随js变动,扣代码挺麻烦的。我补了大概500行左右的环境,还是很稳定的,检测并不严格。另外放一份还原后的代码供参考,额外说一句,补环境的时候注意Promise封装的对象,因为Promise.all这个方法对失败的情况并不会报错,所以这个异步函数的地方有可能没补对没结果,但是程序又执行完了。因为src不是直接函数调用返回,建议通过Hook document.creatElement方法返回的对象src set方法拿值。

decode_tongdun.js