前言
网址:
aHR0cHM6Ly9oNS53YW53dWRlemhpLmNvbS9tYWxsLXdlYi9jYXRlZ29yeS9jbGFzc2lmeS8xNjE5MTYxNDM3NDMxP3RleHQ9JUU1JTkyJThDJUU3JTk0JUIwJUU3JThFJTg5JmNpZD0xMCZmYWNhZGVDYXRlZ29yeUlkPTEwJmlzU2hvd0F1Y3Rpb25GaXJzdD0xJl9fSGdXdHdZVT0xNjE5MTYxNDM3NDMxJnJ0cFJlZmVyPWJ3MC53MC4wLjAuMTYxOTE2MTQzNTQ4MiUyNEc3SiZzaGFyZVVzZXJJZD0xODc1OTU4OCZzaGFyZVRpbWU9MTU5MTY5MDY5OQ==
对方的要求:
kl_device_id
、kl_sign
、kl_t
这三个参数的加密方式,其实核心参数还有一个kl_trace_id
,下面会讲到。
拿到站点,初步分析找到数据接口后,看了看请求头
其中,kl_t
明显是时间戳。
还有这个请求携带的Payload
我们使用全局搜索关键词kl_sign
格式化后,看到了这个比较显眼的对象
与请求头里面的值一一对应。
仔细看看,少了些什么,少了最重要的kl_sign
这个先放着不管,我们先解决其他三个参数,分别是kl_t
和 kl_device_id
和kl_trace_id
kl_t
kl_t
是由变量x
赋值,网上找找即可找到x
是如何被赋值的。
很明显,这个就是时间戳。
源代码为:
parseInt(Date.now() / 1000)
kl_trace_id
使用同样的方法找到 kl_trace_id
是由某个方法计算而来。
我们将断点打在此处,看看Object(Mn[xe("넕녰넞녻넉녨넜녹넬녹넰녴")])()
是什么牛马
鼠标选中xe("넕녰넞녻넉녨넜녹넬녹넰녴")
停滞一会儿,或者在console
中输入此代码,可以看到对应的字符串是generateUUID
xe
函数就是一个混淆函数,用于将这些傻不拉几的字符,转成实际方法名。在这个站点中还有很多这样的函数。
所以,这个Mn.generateUUID
方法就是生产一个uuid
。
在console中输入此方法名并点击,查看其代码
稍微整理下,抠出此代码
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
在多次请求中,kl_device_id
都是不会改变的。
在我在对kl_device_id
修改后,再次发送请求,依然可以请求成功。所以kl_device_id
不参与后端的验证。
既然这个参数不参与验证,那么我只要随机构造与uuid
格式相同的id即可。
也就是,我们可以通用上方的generateUUID
函数。
一句话就是,kl_device_id
是uuid
的格式即可。
kl_sign
好了,重头戏来了,这个签名才是最重要的一环。
在上面的赋值中,少了kl_sign
,那么这个kl_sign
在哪里呢?
其他的值都在这个j
对象内,我猜测,在后续的代码执行过程中,还会对这个j
进行一些操作,以此到达kl_sign
赋值的目的。
所以我们就瞅准这个j
在干什么就行了。
就在下方,j
被再次赋值了。
断点跳到这里,看下Se(f[54])
又是什么牛马
哈,这不就是kl_sign
吗,接着查看z
这里传入了三个参数,在console
中输出看看
x
就是刚刚赋值给kl_t
的值。
H
就是payload
,请求参数
W
是个Map
类型,其中有三个值,kl_tract_id
与kl_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
那么,现在的问题则是转换成了,寻找m
、T
、O
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)
这里用了两种加密算法,分别是MD5
和HmacSHA256
这个代码中的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×tamp=1619167676"
"f68d2658fc92f6a5c911074030c18469"
最后得到就是签名
a9326b90979e3a690323615bd59f82ec22f83c611dc1c141d3f641bbb2f46381
然后拼接字符串即完成kl_sign
的实现
画一张图,看下签名生成的过程。
总结
分析完这个站点可知,这个站点的后端校验逻辑主要与时间戳、时间戳所生成的trace_id
以及请求携带的Payload
有关。
最后,我们需要使用这个签名,要么用纯Python实现,要么起一个Nodejs服务。
起nodejs服务是比较快的,扣出关键代码,放在nodejs里去跑就行了。
用Pyhon的写的话也不难,主要理清楚这个站点的加密流程就可以实现了。