/ 爬虫 / 920浏览

玩物得志H5列表页签名逆向总结

前言

网址:

aHR0cHM6Ly9oNS53YW53dWRlemhpLmNvbS9tYWxsLXdlYi9jYXRlZ29yeS9jbGFzc2lmeS8xNjE5MTYxNDM3NDMxP3RleHQ9JUU1JTkyJThDJUU3JTk0JUIwJUU3JThFJTg5JmNpZD0xMCZmYWNhZGVDYXRlZ29yeUlkPTEwJmlzU2hvd0F1Y3Rpb25GaXJzdD0xJl9fSGdXdHdZVT0xNjE5MTYxNDM3NDMxJnJ0cFJlZmVyPWJ3MC53MC4wLjAuMTYxOTE2MTQzNTQ4MiUyNEc3SiZzaGFyZVVzZXJJZD0xODc1OTU4OCZzaGFyZVRpbWU9MTU5MTY5MDY5OQ==

对方的要求:

kl_device_idkl_signkl_t这三个参数的加密方式,其实核心参数还有一个kl_trace_id,下面会讲到。

拿到站点,初步分析找到数据接口后,看了看请求头

image-20210423150924903

其中,kl_t明显是时间戳。

image-20210423150959504

还有这个请求携带的Payload

image-20210423200645181

我们使用全局搜索关键词kl_sign

格式化后,看到了这个比较显眼的对象

image-20210423155257039

与请求头里面的值一一对应。

image-20210423155251271

仔细看看,少了些什么,少了最重要的kl_sign

这个先放着不管,我们先解决其他三个参数,分别是kl_tkl_device_idkl_trace_id

kl_t

kl_t是由变量x赋值,网上找找即可找到x是如何被赋值的。

image-20210423155630054

很明显,这个就是时间戳。

源代码为:

parseInt(Date.now() / 1000)

kl_trace_id

使用同样的方法找到 kl_trace_id 是由某个方法计算而来。

image-20210423155909188

我们将断点打在此处,看看Object(Mn[xe("넕녰넞녻넉녨넜녹넬녹넰녴")])()是什么牛马

鼠标选中xe("넕녰넞녻넉녨넜녹넬녹넰녴")停滞一会儿,或者在console中输入此代码,可以看到对应的字符串是generateUUID

image-20210423160206725

xe函数就是一个混淆函数,用于将这些傻不拉几的字符,转成实际方法名。在这个站点中还有很多这样的函数。

所以,这个Mn.generateUUID方法就是生产一个uuid

在console中输入此方法名并点击,查看其代码

image-20210423160716538

generateUUID

稍微整理下,抠出此代码

function generateUUID () {
    var t = (new Date).getTime();
    return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (function(e) {
        var n = (t + 16 * Math.random()) % 16 | 0;
        return t = Math.floor(t / 16),
            ("x" === e ? n : 3 & n | 8).toString(16)
    }
                                                                   ))
}

所以,最终kl_trace_id可以这样得来。

var kl_trace_id = generateUUID()

注意

我们看了kl_trace_id的生成算法与时间戳有关,这也就是说,每次请求的trace_id都是变化的,这个也将作为后端验证的重要参数。

kl_device_id

kl_device_id翻译翻译,就是设备id。

这个请求头,并没有携带cookie。所以我有理由判断,这个id就是用户的唯一标识。

并且我查看了local storage

image-20210423161300095

image-20210423161315407

在多次请求中,kl_device_id都是不会改变的。

在我在对kl_device_id修改后,再次发送请求,依然可以请求成功。所以kl_device_id不参与后端的验证。

既然这个参数不参与验证,那么我只要随机构造与uuid格式相同的id即可。

也就是,我们可以通用上方的generateUUID函数。

一句话就是,kl_device_iduuid的格式即可。


UUID百度百科

kl_sign

好了,重头戏来了,这个签名才是最重要的一环。

在上面的赋值中,少了kl_sign,那么这个kl_sign在哪里呢?

image-20210423155257039

其他的值都在这个j对象内,我猜测,在后续的代码执行过程中,还会对这个j进行一些操作,以此到达kl_sign赋值的目的。

所以我们就瞅准这个j在干什么就行了。

就在下方,j被再次赋值了。

image-20210423162420693

断点跳到这里,看下Se(f[54])又是什么牛马

image-20210423162726371

哈,这不就是kl_sign吗,接着查看z

image-20210423162821474

这里传入了三个参数,在console中输出看看

image-20210423163034648

x就是刚刚赋值给kl_t的值。

H就是payload,请求参数

W是个Map类型,其中有三个值,kl_tract_idkl_device_id有方法可以生成,而kl_path是请求路径,是一个固定值。

所以这个参数可以马上就构造出来。

W = new Map();
W.set('kl_path', '/activitysearch/category/item')
W.set('kl_trace_id', generateUUID())
W.set('kl_device_id', generateUUID())

现在,进入方法Fn[Se(s[66])].sign

呵呵,全是混淆,什么妖魔鬼怪,给爷爬。

看最后一句return返回的oe函数里的值

执行oe(d, t, u, h, e, n),这里一堆参数,t e n 我们已经知道,所以这里看下d, y, h就行

简单的分析,这三个值来源于window.ENCRYPT_KEY,这里就不赘述了,全局变量很容易就找到的。并且这个是固定值。

oe函数最终的返回数据如下:

return ee[Jt(s[57])][$t(yt + n + "REDAE" + r + i + o)] + "=" + ee[te("d?RVg7GYZ[MZ%")].ALGORITHM + u[58] + ee[Jt("涐淹涞淰涠淁涳淒涿淺涔淡涌淿")][wt + "ER_SIGNEDHEADERS"] + "=" + m[Jt("涷淘涋淿涍淤涊" + p)]() + te("") + ee[$t(d + "raPngiS")][xt + "IONHEADER_" + y] + $t(u[59]) + T + $t("/") + ee[h[67]]["AUTHORIZATION" + g + St] + a[63] + O

我们断点调试,反混淆解出来就是这样的。

return ee.SignParamEnums.AUTHORIZATIONHEADER_ALGORITHM + "=" + ee.SignParamEnums.ALGORITHM + '/' + ee.SignParamEnums.AUTHORIZATIONHEADER_SIGNEDHEADERS + "=" + m.toString() + '/' + ee.SignParamEnums.AUTHORIZATIONHEADER_CREDENTIAL + '=' + T + '/' + ee.SignParamEnums.AUTHORIZATIONHEADER_SIGNATURE + '=' + O

先看看这个ee是什么,这是一个对象。

构造如下:

const ee = {
    "SignCodeEnums": {
        "PASS": "0000",
        "SIGN_PARAMS_ERROR": "0001",
        "properties": {
            '0000': {
                'message': '签名通过'
            },
            '0001': {
                'message': "SignParams参数未传或者为空"
            }
        }
    },
    "SignParamEnums": {
        "ALGORITHM": "WWDZ-HMAC-SHA256",
        "AUTHORIZATIONHEADER_ALGORITHM": "Algorithm",
        "AUTHORIZATIONHEADER_CREDENTIAL": "Credential",
        "AUTHORIZATIONHEADER_SIGNATURE": "Signature",
        "AUTHORIZATIONHEADER_SIGNEDHEADERS": "SignedHeaders",
        "SIGNEDHEADERSAUTHORIZATION": "Authorization",
        "SIGNEDHEADERSPARAMS": "Signed-Headers",
        "SIGNEDPARAM_APIPATH": "apiPath",
        "SIGNEDPARAM_APPVERSION": "appVersion",
        "SIGNEDPARAM_DEVICETOKEN": "deviceToken",
        "SIGNEDPARAM_END": "wwdz_request",
        "SIGNEDPARAM_METHOD": "method",
        "SIGNEDPARAM_PAYLOAD": "payload",
        "SIGNEDPARAM_SID": "sId",
        "SIGNEDPARAM_SIGNVERSION": "signVersion",
        "SIGNEDPARAM_TIMESTAMP": "timestamp",
        "SIGNSALT": "e680d60e7e6bd5931cb46d30c91d6d0d", // 疑似固定值
        "SIGNSECRETKEY": "bbcc71f7b26a82ea97196366558a8ef0" // 疑似固定值
    }
}

所以这个return语句再次变换为这样

return "Algorithm=WWDZ-HMAC-SHA256/SignedHeaders=" + m + "/Credential=" + T + "/Signature=" + O

那么,现在的问题则是转换成了,寻找mTO

m

m这个参数不用解,因为他是一个固定值

以下是正常请求的kl_sign

Algorithm=WWDZ-HMAC-SHA256/SignedHeaders=kl_path;kl_trace_id;kl_device_id/Credential=1619164848_wwdz_request/Signature=d8e4cbd8c4b7aba69e5fe0ba966304c3e6355024c8e8c0496886ed1f0a974898

对比一下可知,m = kl_path;kl_trace_id;kl_device_id

T

T就在return上面一行,代码如下:

T = t[te('C?X]D"TUG')] + "_" + ee[b + "Enums"][a[62]]

反混淆后

T = t.timestamp + "_" + ee.SignParamEnums.SIGNEDPARAM_END

T = t.timestamp + "_" + "wwdz_request"

t.timestamp,这个很明显就是我们传进来的时间戳。

为了O的生成也会有t的参与,所以这里解释下t怎么来的,去看下t什么长什么样的。

这个t是就是new ne(t, e, n,r, i)而来,这里就不赘诉了,直接看t是什么样子的,更方便。

这就是一个对象。

t = {
    "appVersion": "3.1.2",
    "payload": "xxxx",
    "sId": "300100",
    "signVersion": "1.0.0",
    "timestamp": 1619167676
}

根据实际情况,动态构造即可。

O

O = function(t, e) {
    var n,
        r = "od",
        i = "s",
        o = "s",
        p = "SIGNEDPARAM_T",
        d = "ums",
        g = "SIGNEDPARAM_S",
        v = "d",
        m = new Map,
        b = at.MD5(ee.SignParamEnums.SIGNSECRETKEY + ee.SignParamEnums.SIGNSALT + t.signVersion).toString();
    var y = it(e)

    var S, _ = !0, k = false;

    // 这个循环给m这个map设值
    try {
        for (y.s(); !(n = y.n())["done"];) {
            var _ = n["value"];  // ["kl_path", "/activitysearch/category/item"]
            m.set(_[0], _[1])
        }
    } catch (t) {
        y.e(t)
    } finally {
        y.f()
    }

    return m.set(ee.SignParamEnums.SIGNEDPARAM_SID, t.sId),
        m.set(ee.SignParamEnums.SIGNEDPARAM_TIMESTAMP, t.timestamp),
        m.set(ee.SignParamEnums.SIGNEDPARAM_APPVERSION, t.appVersion),
        m.set(ee.SignParamEnums.SIGNEDPARAM_SIGNVERSION, t.signVersion),
        // 对payload进行加密
        m.set(ee.SignParamEnums.SIGNEDPARAM_PAYLOAD, function (t, e) {
            return at.HmacSHA256(t, e).toString()
        }(t.payload, b)),
        // 上方都是为m设置,下方方法将会返回实际被加密的值
        function (t, e) {
            var n = st.getParamStr(t);
            return n = n.toUpperCase(),
                at.HmacSHA256(n, e).toString()
        }(m, b)
}(t, e)

这里用了两种加密算法,分别是MD5HmacSHA256

这个代码中的at用就是JS中的加密包CryptoJS

MD5加密基于 KEY + SALT + 版本号,这三个值是固定的。

可以看到,在return这一步,全是在对m设置值,其中对payload进行HmacSHA256加密。

参数t在上面有讲,而参数e其实就是最初的W,看下值就知道了

W = new Map();
W.set('kl_path', '/activitysearch/category/item')
W.set('kl_trace_id', generateUUID())
W.set('kl_device_id', generateUUID())

最终,剩下最后一个函数了

        function (t, e) {
            var n = st.(t);
            return n = n.toUpperCase(),
                at.HmacSHA256(n, e).toString()
        }(m, b)

其中st.getParamStr代码如下:

const st = {
    "getParamStr": function (t) {
        var e = "", n = [];
        t.forEach((function (t, e, r) {
                n.push(e)
            }
        )),
            n.sort();
        for (var r = !1, i = 0; i < n.length; i++) {
            var s = n[i];
            if ("sign" !== s) {
                var u = String(t.get(s));
                if (u === undefined || null == u || u === "")
                    continue;
                r ? e += "&" + s + "=" + u : (e += s + "=" + u, r = !0)
            }
        }
        return String(e)
    }
}

那么最终就是这两个字符串做HmacSHA256加密

"appVersion=3.1.2&kl_device_id=12889e77-47a4-4eeb-b03c-8efb1edf03db&kl_path=/log/api/logcenter/postCommonUelLog&kl_trace_id=58c66def-4fdc-4742-93e4-7277f4fe1489&payload=e630e82682565c08a8f2239b3406516bd6b65fff23170f88a4aa3f1994c2d189&sId=300100&signVersion=1.0.0&timestamp=1619167676"

"f68d2658fc92f6a5c911074030c18469"

最后得到就是签名

a9326b90979e3a690323615bd59f82ec22f83c611dc1c141d3f641bbb2f46381

然后拼接字符串即完成kl_sign的实现

画一张图,看下签名生成的过程。

总结

分析完这个站点可知,这个站点的后端校验逻辑主要与时间戳、时间戳所生成的trace_id以及请求携带的Payload有关。

最后,我们需要使用这个签名,要么用纯Python实现,要么起一个Nodejs服务。

起nodejs服务是比较快的,扣出关键代码,放在nodejs里去跑就行了。

用Pyhon的写的话也不难,主要理清楚这个站点的加密流程就可以实现了。

小试牛刀-利用AST平坦化一段瑞数代码
小试牛刀-利用AST平坦化一段瑞数代码
JavaScript AST抽象语法树常见节点及结构
【瑞数】维普期刊搜索接口逆向总结_2_获取Cookie
【瑞数】维普期刊JS逆向4000字详细流程_1_获取接口签名
【瑞数】维普期刊JS逆向4000字详细流程_1_获取接口签名
CSS字体加密反反爬通用方法
CSS字体加密反反爬通用方法
Python使用Protobuf&&如何赋值&&如何正反序列化