qcc headers 哈希逆向

dev 2022-11-29 21:35:12 #网络安全 #前端开发 #逆向 2941

今天受一老师朋友所托,帮其看了下企查查高级搜索页请求的哈希校验,页面地址:

YUhSMGNITTZMeTkzZDNjdWNXTmpMbU52YlM5M1pXSXZjMlZoY21Ob0wyRmtkbUZ1WTJVPQ==

先随便交互触发两个请求看看构造,以 Node.js fetch 格式显示如下:

fetch("https://www.qcc.com/api/search/searchCount", {
  headers: {
    b3e1a8edd33330140573: "e4b1fd60aa53b5a7686492e38b0e7d52b7b2a44db8fa85ba73c2d1b9093bcb9e61a20c5a01fbf931709dbaee324fc524806f2c26fa1dbc435d0c56547c7eeeb5",
    "x-pid": "aa22357a6b17df78bc1b6e1ea32cb8b7",
    cookie: "QCCSESSID=3f5f0d761ed9eee3f696bd9d2a; qcc_did=82697083-6a9e-4ab7-9059-d7df6f670e61; UM_distinctid=18474def4aaadc-08393462a90e57-18525635-1ea000-18474def4abc1b; acw_tc=db90651616696334990788317ef781a882b454ed444237917218746ed5; CNZZDATA1254842228=241614328-1668405992-https%253A%252F%252Fwww.baidu.com%252F%7C1669631222",
  },
  body: "{\"count\":true,\"filter\":\"{\\\"i\\\":[\\\"A\\\"]}\"}",
  method: "POST"
});

fetch("https://www.qcc.com/api/search/searchCount", {
  headers: {
    d6c819c35478cea0a8d9: "67ef5dc179bae97c4543bf419a3965081119e12b36a04b8063ad22438b61f241212b92d1c3830cd7217b92a56cc2aee123fd6370d2db1a7b98a476b27f9596f0",
    "x-pid": "aa22357a6b17df78bc1b6e1ea32cb8b7",
    cookie: "QCCSESSID=3f5f0d761ed9eee3f696bd9d2a; qcc_did=82697083-6a9e-4ab7-9059-d7df6f670e61; UM_distinctid=18474def4aaadc-08393462a90e57-18525635-1ea000-18474def4abc1b; acw_tc=db90651616696334990788317ef781a882b454ed444237917218746ed5; CNZZDATA1254842228=241614328-1668405992-https%253A%252F%252Fwww.baidu.com%252F%7C1669631222",
  },
  body: "{\"count\":true,\"filter\":\"{\\\"i\\\":[\\\"A\\\"],\\\"r\\\":[{\\\"pr\\\":\\\"CQ\\\"}]}\"}",
  method: "POST"
});

可以发现随着请求而变化的数据仅有 headers 里的一对键值,且后台也仅对该变动键值做了二次校验。由于其看起来很像 Hash,索性就叫 hashKey:hashValue。

通过网络请求调用堆栈并没有发现相关的赋值代码,于是便想从代码搜索入手。例如此场景在 js 中的赋值语句通常为 headers[key] = value,搜索 headers[ 即可。

遂查看其网页源代码,发现 JavaScript 脚本文件都集中在路径 //qcc-static.qichacha.com/qcc/pc-web/prod-22.11.515/ 中,搜索后发现此处最为可疑:

u_1_638602e3aa6e8_1234x1744.jpeg

在此断点调试看一下:

u_1_6386037211451_1230x1346.jpeg

很明显可以发现,其临时变量 i 就是 hashKey,l 则是 hashValue:

// 源代码
var i = (0,a.default)(t, e.data),
    l = (0,r.default)(t, e.data, (0,s.default)());
e.headers[i] = l;

// 其中
t = '/api/search/searchcount';
e.data = {
    count: true,
    filter: "{\"i\":[\"A\"],\"r\":[{\"pr\":\"GD\"},{\"pr\":\"CQ\"}]}"
};
(0,s.default)() = window.tid;

由此,我们只需要查看 a.default 与 r.default 的函数即可知道其哈希值的生成方法。

首先是生成 hashKey 的函数 a.default,通过多次断点,发现其调用堆栈如下:

// 第一层 a.default
function() {
    // t=请求URL,e=请求对象,n=请求对象json
    var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}
      , t = (arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "/").toLowerCase()
      , n = JSON.stringify(e).toLowerCase();
    // o.default 加密函数在最后提到
    return (0, o.default)(t + n, (0, a.default)(t))
    .toLowerCase().substr(8, 20)
};
// 传入加密的两个字符分别为
// t + n = /api/search/searchcount{"count":true,"filter":"{\"i\":[\"a\"],\"r\":[{\"pr\":\"gd\"},{\"pr\":\"cq\"}]}"}
// (0, a.default)(t) = iLAgiklLN8QiklLN8Q86Lv4iLAgiklLN8QiklLN8Q86Lv4

// 第二层 a.default
function() {
    for (var e = (arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "/").toLowerCase(), t = e + e, n = "", i = 0; i < t.length; ++i) {
        var a = t[i].charCodeAt() % o.default.n;
        // 并非最后的加密函数 o.default
        n += o.default.codes[a]
    }
    return n
};
// return "iLAgiklLN8QiklLN8Q86Lv4iLAgiklLN8QiklLN8Q86Lv4"

// 第二层 a.default 中的 o.default
o.default = {
    "n": 20,
    "codes": {
        "0": "W",
        "1": "l",
        "2": "k",
        "3": "B",
        "4": "Q",
        "5": "g",
        "6": "f",
        "7": "i",
        "8": "i",
        "9": "r",
        "10": "v",
        "11": "6",
        "12": "A",
        "13": "K",
        "14": "N",
        "15": "k",
        "16": "4",
        "17": "L",
        "18": "1",
        "19": "8"
    }
};

其次是 r.default:

// 第一层 r.default
function() {
    // n=请求URL,e=请求对象,t=tid,i=请求对象json
    var e = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}
      , t = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : ""
      , n = (arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : "/").toLowerCase()
      , i = JSON.stringify(e).toLowerCase();
    // o.default 加密函数在最后提到
    // a.default 即为刚提到的第二层函数
    return (0, o.default)(n + "pathString" + i + t, (0, a.default)(n))
};

// 传入加密的两个字符分别为
// /api/search/searchcountpathString{"count":true,"filter":"{\"i\":[\"a\"],\"r\":[{\"pr\":\"gd\"},{\"pr\":\"cq\"}]}"}8b4ebc1e4a1b8c21235f34bf9db8f1a8
// iLAgiklLN8QiklLN8Q86Lv4iLAgiklLN8QiklLN8Q86Lv4

// 则 return 返回值为
// 9396c85dc08d8c5be05644a07dda6539a2c2ec4b742f0b412eb58d71b3d41574e01a1d2b0982861253aa9a14aa08bafb59dd615ea3c36a9716a203620c5bfec1

其中,两个函数都调用了同一个 o.default,其堆栈如下:

// 第一层 o.default
function(e, t) {
    return (0, o.default)(e, t).toString()
};

// 第二层 o.default
return function(e, n) {
    // h 对象就是一个库,使用的 HMAC SHA512 加密算法
    // input:[e],key:[n]
    // 在线加密:https://www.idcd.com/tool/encrypt/hmac
    return new h.HMAC.init(t,n).finalize(e)
}

至此,顺带做个 python 的解构版本:GitHub 地址

声明

文中所有内容仅供学习交流,严禁用于商业或非法用途,否则由此产生的一切后果均与作者无关。若有侵权,请与小蔗联系,将在第一时间处理。