某东签名算法 jsvmp 插桩法的纯算还原《笔记》
最新不是很忙,摸鱼整理一份 h5st 的签名算法 jsvmp 插桩法的纯算还原流程,焚诀爆出来了。
5.2.2 补环境应该已经不行了,env不对会校验。
只做逆向经验分享,请勿用于非法用途,不提供完整成品
只做逆向经验分享,请勿用于非法用途,不提供完整成品
只做逆向经验分享,请勿用于非法用途,不提供完整成品
只做逆向经验分享,请勿用于非法用途,不提供完整成品
只做逆向经验分享,请勿用于非法用途,不提供完整成品
可以先在网上搜一搜原来老版本的h5st的代码,核心逻辑没有发生变化,只是后面加了两段,并且从ob混淆切换到ob混淆+jsvmp混淆。
如果想跟做,请先自备一份老版本的h5st解密代码。
能直接搜的到的成品应该大部分都是之前从我手里流出去的,之前csdn还有人直接拿我在tg群发的算法卖钱,图片都没改
前言
之前在bilibili直播过,然后发了一个多小时的视频,但是收到了京东安全实验室的邮件,都删除了,巅峰的时候提供的api服务日调用量几千万。
去年十一月份删除主仓库,解散频道。但是陆续还有不少人问我相关的东西(本论坛、tg、某火免流论坛),其实我早就放弃了京东的羊毛,只是默默维护,这里统一发一下思路和经验。
安全性的本文章只做经验分享,不提供任何成品。

这里只是补充一下jsvmp的处理方式,现在不需要处理ob混淆了,个人认为不需要解释的太详细,因为主逻辑没变,我的代码网上飘的到处都是,找到关键点进行插桩就好了。
然后还需要熟悉一下基本的加密算法特征与调用方式,保持敏感性。
逆向是一门非常吃经验的活,保持敏感再加上自己的经验有时候可以很轻松的判断出来关键地方。
最后这个jsvmp入手可能有点麻烦,其实代码特征特别明显,熟悉了小版本更新可以在十分钟之内搞定。大版本包括tk版本换了,大概也就半小时多
初识
目标:aHR0cHM6Ly9qZC5jb20=
作用:对于接口请求加签,应付反爬,这里还有一些细节,后续再说

该签名目前最新已经包含10部分:
- 时间戳转换yyyyMMddhhmmssSSS格式字符串,5.1.8开始会添加一个时间偏移量
- fp指纹
- appId,和同为请求体里面的appid不是同一个东西
- token,分为动态tk和localTk
分为动态tk和本地tk,涉及MD5、SHA256、HmacSHA256- 动态tk是通过本地env环境信息、fp、h5st小版本请求request_algo,交换到的token,并且有携带的对应的加密key的对应算法
- localTk是为加载到缓存时候的一种兜底策略,对应生成参与sign的key是将token、fp、时间戳、appId、一个扩展字符串(每个版本不一样)解析出一个算法调用逻辑,循环调用MD5、SHA256、HmacSHA256生成key。
- 业务请求参数和key的签名
- 当前h5st大版本
- 原始时间戳,和第一部分是关联的
- env web环境信息加密,这里有坑,注意
- 4.7.4新增 第二个子签名,也有key的参与,这个不太清楚逻辑,前几个版本刚加的时候和业务请求参数还有关系,目前为固定字符串签名,不清楚是不是京东程序员手滑了
- 5.1.3 新增 stk(参与签名参数key)的base64
京东的虎符文档: https://yd-doc.jdcloud.com/docs/5-hufu/5-2-guanfang/waap.html
这里可以看看京东的文档,大概了解一下
拆解
这里先说一下需要逆向什么东西才能还原,这是事后总结了,放在前面可以节省时间,清晰思路。
- fp指纹:核心
- env环境信息和加密:核心
- localTk:原先版本是可选的,可以跳过直接使用动态tk,但是现在看交换动态tk也需要localTk了
- 魔改算法还原:从4.7.1开始进行魔改,似乎也是从这个时候开始赚到jsvmp,之前是纯ob混淆的。
算法入口
某东的任意网页,F12打开console,输入 window.ParamsSign,也有可能是window.ParamsSignLite,之前他们有区别,不带Lite是会请求动态tk,使用localTk兜底,带Lite的是纯localTk,但是现在好像都一样了,没什么区别了。

单击下面的代码进入算法主逻辑,然后右击override进行随时修改

文件内搜索:debug = ,修改成true,进行打印京东自动的调试日志

虽然现在jsvmp后,函数名都已经看不出来了,但是可以根据代码特征判断出来初始化函数。刷新页面后看到console打印出来大量日志即为成功。

算法流程
这里根据之前ob混淆的方法名称进行说明。
最简请求上下文(正常情况)
| 参数 | 说明 |
|---|---|
| appid | 唯一标识 |
| functionId | 接口标识 |
| body | 业务请求参数、签名阶段会先做 SHA256 哈希,原始明文不直接参与签名 |
| h5st | 安全签名结果 |
- __checkParams 按键名字典序排序,并逐项调用 isSafeParamValue 过滤空值、非法字符、过滤非_stk内的参数;若全部被丢弃直接抛错。并且做数据格式转换
- __requestDeps 获取token,这里只讨论localTk,动态tk请求接口不讨论。涉及到 token 的种子,随机取数长度异常总长度等信息。
- __collect 环境参数加密,提取当前页面的信息和环境信息,有时候会开验证,验证里面的小参,有时候不会
- __makeSign 加密流程
- __genDefaultKey 生成参与签名的key
- __genSign 通过请求体的参数与key进行签名
- __genSignDefault 签名2,现在是每个版本不同的字符串与key进行签名
- signStkStr stk base64,stk是参与签名的参数key , 拼接字符串
- __genSignParams 拼接完成的h5st
老版本的相关代码可以在github上面搜一下,应该还有不少之前fork的,注释是让当时的gpt3.5写的,应该还挺详细的,配合上面的流程应该可以很清晰的过一遍。
jsvmp插桩实战
以一个的md5算法为例,京东自己魔改了,魔改的地方进行了vmp处理。其他的逻辑都是这样,一通百通.


这里截图不太好截图,可以对比一下原始的代码
https://github.com/brix/crypto-js/blob/develop/src/core.js#L537
https://github.com/brix/crypto-js/blob/develop/src/core.js#L653
https://github.com/brix/crypto-js/blob/develop/src/md5.js
可以看到md5加了_eData和_seData,其他是一致的,_eData搜一下,能看到是覆盖的BufferedBlockAlgorithm的_eData。_seData覆盖的是Hasher的,这样看几本上能大概猜到思路。
BufferedBlockAlgorithm的append也被vmp化了,这个append大概应该魔改了调用了_eData,但是md5有自己的逻辑,和其他的hash算法魔改的还不一样,他自己又重写了_eData和_seData。可以验证一下。
先写一个snippet,进行稳定的调用MD5算法
var test = new window.ParamsSign({'appId': "b5216", 'debug': false})
console.log(test._algos.MD5('123123').toString())
然后进行插桩,插桩之前得大概了解一下他这个vmp代码,非常简单:

执行流程:
┌────────────────────────────────────────────────────────┐
│ VM执行流程图 │
└────────────────────────────────────────────────────────┘
初始化阶段:
┌─────────┐
│ 初始化 │
│ q = 129 │ ← 指令指针(PC)
│ m = [] │ ← 操作数栈
└────┬────┘
│
▼
┌─────────────────────────────────────────┐
│ 主循环: for(;;) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 读取指令: m[q++] │
│ (从_2a765数组中读取操作码) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ switch(操作码) { │
│ case 9: ... // 操作 │
│ case 16: ... // 操作 │
│ case 55: ... // 返回 │
│ } │
└─────────────────────────────────────────┘
│
├──► 栈操作 (push/pop)
├──► 变量赋值
├──► 函数调用
├──► 条件跳转
└──► 返回值
case 9: // ADD指令
a = y.pop();
y[y.length - 1] += a;
栈变化: [a, b] → [a+b]
case 16: // PUSH指令 压栈操作
y.push(_$uu);
栈变化: [] → [_$uu]
case 61: // POP指令 出栈操作
y.pop();
栈变化: [a, b, c] → [a, b]
case 46: // CALL指令
if (y[y.length - 2] != null) {
y[y.length - 3] = h.call(y[y.length - 3], y[y.length - 2], y[y.length - 1]);
y.length -= 2;
}
栈变化: [obj, method, arg1] → [result]
case 74: // JUMP指令 跳转到指定位置
q += m[q];
控制流: q=100 → q=150
case 74: // BRANCH指令 条件转换
if (y[y.length - 1])
++q;
--y.length;
else
q += m[q];
比较简单的是,我们其实只需要关心 类似于 add、call这种,其他的对于push,pop几本上都是在攒参数,真正的逻辑代码应该都不在这里。那么我可完全可以在方法入口和return出口进行下断,发生值变动的操作进行下日志断点,最后观察入参、执行逻辑、出参进行还原逻辑,这就是vmp的插桩纯算,不过感觉这种操作对于程序员来说应该不是问题吧,会看日志都会反向思考的。
右键下在关键地方下日志断点后执行snippet

观察日志:

棕色的框,发现多调用了一个方法,内容多了j|/3n8,这个也许就是vmp想要隐藏的,但是我们还需要重新确认一下。直接定位到魔改内容,点击调用的日志跳转到js代码。
跳转到MD5的_eData,按照上面流程重新处理:

似乎还有判断是否以 envCollect 结尾的逻辑,但是实际上没遇到过,所以我没有进行处理,有兴趣可以修改变量值进行走到相关逻辑进行还原。
这里似乎好像还是不能确定j|/3n8是不是固定字符串还是动态算出来的,上面怎么说的,参数都是push攒出来的,断一下push相关的代码判断可以代码进行调试。


这说明就是固定字符串了。
这下MD5就魔改的参数就处理好了。
是吗?等等等等,我记得我们的snippet脚本是传入的 1231234,但是append入参是1231234iM4A,所以前面还有一步漏了,重新在append入口下段
还好,调用链很短,基本上就是上一层了。


这里Hasher.finalize也魔改了,会判断参数是否是string类型,调用_seData进行进一步魔改,进入该方法继续处理。

啊嘞,空方法,还是判断是否以envCollect结尾,然后调用了另一个方法,添加的后缀,又进入了一个新方法,继续

可以看到就是方法加了后缀,这个日志有点长,截图接不下来,我贴在下面
入参: 123123
-4243 '+' 1697 '--->' -2546
-2546 '+' 2551 '--->' 5
6370 '+' 5143 '--->' 11513
11513 '+' -11491 '--->' 22
调用: ƒ (_$j, _$L) {
_$j = _$j - (-0xb94 + -0x5e * -0x55 + -0x1295);
var _$z = _$G[_$j];
if (a0d2b23b.FqOFnt === undefined) {
var _$B = function(_$w) {
… 462
调用结果: utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv
6 '/' 5 '--->' 1.2
调用: ƒ floor() { [native code] } Math {abs: ƒ, acos: ƒ, acosh: ƒ, asin: ƒ, asinh: ƒ, …} 1.2
调用结果: 1
-8909 '+' 3838 '--->' -5071
-5071 '+' 5071 '--->' 0
0 '<' 5 '--->' true
-5749 '+' -9900 '--->' -15649
-15649 '+' 15649 '--->' 0
0 '*' 1 '--->' 0
-322 '+' 2199 '--->' 1877
1877 '+' -1876 '--->' 1
5 '-' 1 '--->' 4
0 '===' 4 '--->' false
-3925 '+' -6564 '--->' -10489
-10489 '+' 10489 '--->' 0
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 1
调用结果: true
0 '+' 0 '--->' 0
0 '<' 6 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123 0
调用结果: 49
0 '+' 49 '--->' 49
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 1
调用结果: false
49 '*' 22 '--->' 1078
1078 '%' 64 '--->' 54
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 54
调用结果: 4
调用: ƒ push() { [native code] } [] 4
调用结果: 1
1 '<' 5 '--->' true
-5749 '+' -9900 '--->' -15649
-15649 '+' 15649 '--->' 0
1 '*' 1 '--->' 1
-322 '+' 2199 '--->' 1877
1877 '+' -1876 '--->' 1
5 '-' 1 '--->' 4
1 '===' 4 '--->' false
-3925 '+' -6564 '--->' -10489
-10489 '+' 10489 '--->' 0
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 1
调用结果: true
1 '+' 0 '--->' 1
1 '<' 6 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123 1
调用结果: 50
0 '+' 50 '--->' 50
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 1
调用结果: false
50 '*' 22 '--->' 1100
1100 '%' 64 '--->' 12
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 12
调用结果: i
调用: ƒ push() { [native code] } ['4'] i
调用结果: 2
2 '<' 5 '--->' true
-5749 '+' -9900 '--->' -15649
-15649 '+' 15649 '--->' 0
2 '*' 1 '--->' 2
-322 '+' 2199 '--->' 1877
1877 '+' -1876 '--->' 1
5 '-' 1 '--->' 4
2 '===' 4 '--->' false
-3925 '+' -6564 '--->' -10489
-10489 '+' 10489 '--->' 0
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 1
调用结果: true
2 '+' 0 '--->' 2
2 '<' 6 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123 2
调用结果: 51
0 '+' 51 '--->' 51
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 1
调用结果: false
51 '*' 22 '--->' 1122
1122 '%' 64 '--->' 34
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 34
调用结果: M
调用: ƒ push() { [native code] } (2) ['4', 'i'] M
调用结果: 3
3 '<' 5 '--->' true
-5749 '+' -9900 '--->' -15649
-15649 '+' 15649 '--->' 0
3 '*' 1 '--->' 3
-322 '+' 2199 '--->' 1877
1877 '+' -1876 '--->' 1
5 '-' 1 '--->' 4
3 '===' 4 '--->' false
-3925 '+' -6564 '--->' -10489
-10489 '+' 10489 '--->' 0
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 1
调用结果: true
3 '+' 0 '--->' 3
3 '<' 6 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123 3
调用结果: 49
0 '+' 49 '--->' 49
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 1
调用结果: false
49 '*' 22 '--->' 1078
1078 '%' 64 '--->' 54
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 54
调用结果: 4
调用: ƒ push() { [native code] } (3) ['4', 'i', 'M'] 4
调用结果: 4
4 '<' 5 '--->' true
-5749 '+' -9900 '--->' -15649
-15649 '+' 15649 '--->' 0
4 '*' 1 '--->' 4
-322 '+' 2199 '--->' 1877
1877 '+' -1876 '--->' 1
5 '-' 1 '--->' 4
4 '===' 4 '--->' true
6 '%' 5 '--->' 1
1 '+' 1 '--->' 2
-3925 '+' -6564 '--->' -10489
-10489 '+' 10489 '--->' 0
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 2
调用结果: true
4 '+' 0 '--->' 4
4 '<' 6 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123 4
调用结果: 50
0 '+' 50 '--->' 50
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 2
调用结果: true
4 '+' 1 '--->' 5
5 '<' 6 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123 5
调用结果: 51
50 '+' 51 '--->' 101
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 2 2
调用结果: false
101 '*' 22 '--->' 2222
2222 '%' 64 '--->' 46
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 46
调用结果: A
调用: ƒ push() { [native code] } (4) ['4', 'i', 'M', '4'] A
调用结果: 5
5 '<' 5 '--->' false
调用: ƒ join() { [native code] } (5) ['4', 'i', 'M', '4', 'A']
调用结果: 4iM4A
123123 + 4iM4A ---> 1231234iM4A
a[0]
'1231234iM4A'
此处静止一分钟,考考大家能分出来是什么逻辑吗?
相信以大家的编码能力仔细点还是能分辨出来大概逻辑的。
首先看头和结尾都有判断是否 < 5的逻辑判断,最终生成的后缀也是5位,合理判断这是一个循环,搜索一下。


结合开头的有一个6 / 5也很可以,5大概可以判断也是for循环,这个6是什么,巧了这不是,入参是123123,长度正好是6,不确定,放行重新构造一个别的字符串看看是不是入参字符串的长度(只有字符串才会进入这个方法,如果是 WordArray 类型是不会进入的)
现在可以大概写一个初始代码
function transformMessage(plainText, options) {
// segments 是循环次数
var segments = options.segments;
var segmentLength = Math.floor(plainText.length / segments);
for (var i = 0; i < segments; i++) {
}
var checkString = transformedSegments.join('');
return plainText + checkString;
}
然后再看for循环里面是什么,结合5个for循环的日志进行还原。

可以看到代码流程相似度主要分为两块:
首先都是 一个数字 * 1,这个数字还是递增的,应该是for循环的当前位置。
5 - 上面*的结果,判断是否等于4,如果不是4是一个逻辑,是4是另外一个逻辑
这里的1合理怀疑是上面 入参长度 / 循环步数 floor的值,可以用控制变量法,多尝试几个入参就判断出来。
这个5-和是否等于4,似乎是判断是最后一次循环,根据前四个的代码可以判断里面似乎还有一层循环,根据 入参长度 / 循环步数 floor的值 进行处理,如果入参不是5的倍数,最后一个循环处理的数据应该长度不够5,应该是在兼容这里

棕色的框里面应该也是一个循环
还是那句话,先进行合理的猜测,然后进行控制入参去验证两次,就可以判断出来逻辑了
然后整理这一块逻辑,重新补全代码
function transformMessage(plainText, options) {
// segments 是循环次数
var segments = options.segments;
var segmentLength = Math.floor(plainText.length / segments);
for (var i = 0; i < segments; i++) {
// 计算当前段的起始和结束位置
var startIndex = i * segmentLength;
var endIndex;
if (i === segments - 1) {
// TODO
} else {
endIndex = startIndex + segmentLength;
// TODO
}
}
var checkString = transformedSegments.join('');
return plainText + checkString;
}
先看 else 分支的,因为日志稍微短
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 1
调用结果: true
0 '+' 0 '--->' 0
0 '<' 6 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123 0
调用结果: 49
0 '+' 49 '--->' 49
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 1
调用结果: false
49 '*' 22 '--->' 1078
1078 '%' 64 '--->' 54
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 54
调用结果: 4
调用: ƒ push() { [native code] } [] 4
调用结果: 1
看着逻辑是 循环之前的入参长度 / 5,然后将去到当前入参idx字符的ASCII码,然后 * 22 % 64,然后取自定义映射表utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv idx上的字符,但是这里面 < 6,以及循环就一次,这里看不太清楚逻辑,重新写snippet,将入参控制长一点看看。
入参: 123123123123123123
-4243 '+' 1697 '--->' -2546
-2546 '+' 2551 '--->' 5
6370 '+' 5143 '--->' 11513
11513 '+' -11491 '--->' 22
调用: ƒ (_$j, _$L) {
_$j = _$j - (-0xb94 + -0x5e * -0x55 + -0x1295);
var _$z = _$G[_$j];
if (a0d2b23b.FqOFnt === undefined) {
var _$B = function(_$w) {
… 462
调用结果: utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv
18 '/' 5 '--->' 3.6
调用: ƒ floor() { [native code] } Math {abs: ƒ, acos: ƒ, acosh: ƒ, asin: ƒ, asinh: ƒ, …} 3.6
调用结果: 3
-8909 '+' 3838 '--->' -5071
-5071 '+' 5071 '--->' 0
0 '<' 5 '--->' true
-5749 '+' -9900 '--->' -15649
-15649 '+' 15649 '--->' 0
0 '*' 3 '--->' 0
-322 '+' 2199 '--->' 1877
1877 '+' -1876 '--->' 1
5 '-' 1 '--->' 4
0 '===' 4 '--->' false
-3925 '+' -6564 '--->' -10489
-10489 '+' 10489 '--->' 0
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 3
调用结果: true
0 '+' 0 '--->' 0
0 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 0
调用结果: 49
0 '+' 49 '--->' 49
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 3
调用结果: true
0 '+' 1 '--->' 1
1 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 1
调用结果: 50
49 '+' 50 '--->' 99
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 2 3
调用结果: true
0 '+' 2 '--->' 2
2 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 2
调用结果: 51
99 '+' 51 '--->' 150
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 3 3
调用结果: false
150 '*' 22 '--->' 3300
3300 '%' 64 '--->' 36
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 36
调用结果: K
调用: ƒ push() { [native code] } [] K
调用结果: 1
入参是123123123123123123后能看清楚一点逻辑了,他是循环之前的入参长度 / 5,然后将每次循环当前入参idx字符的ASCII码相加 * 22 % 64,再去自定义映射表里面取到字符串。
但是 这里还是有 < 18(入参长度) 不知道什么含义!!!
联想之前的可能最后一次循环有特殊操作,这个18有可能是给他准备的,前面的都分析完了,直接看最后一次循环的逻辑
4 '<' 5 '--->' true
-5749 '+' -9900 '--->' -15649
-15649 '+' 15649 '--->' 0
4 '*' 3 '--->' 12
-322 '+' 2199 '--->' 1877
1877 '+' -1876 '--->' 1
5 '-' 1 '--->' 4
4 '===' 4 '--->' true
18 '%' 5 '--->' 3
3 '+' 3 '--->' 6
-3925 '+' -6564 '--->' -10489
-10489 '+' 10489 '--->' 0
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 0 6
调用结果: true
12 '+' 0 '--->' 12
12 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 12
调用结果: 49
0 '+' 49 '--->' 49
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 1 6
调用结果: true
12 '+' 1 '--->' 13
13 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 13
调用结果: 50
49 '+' 50 '--->' 99
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 2 6
调用结果: true
12 '+' 2 '--->' 14
14 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 14
调用结果: 51
99 '+' 51 '--->' 150
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 3 6
调用结果: true
12 '+' 3 '--->' 15
15 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 15
调用结果: 49
150 '+' 49 '--->' 199
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 4 6
调用结果: true
12 '+' 4 '--->' 16
16 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 16
调用结果: 50
199 '+' 50 '--->' 249
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 5 6
调用结果: true
12 '+' 5 '--->' 17
17 '<' 18 '--->' true
调用: ƒ charCodeAt() { [native code] } 123123123123123123 17
调用结果: 51
249 '+' 51 '--->' 300
调用: ƒ (_$uj, _$uL) {
return _$uj < _$uL;
} {xPZwm: 'function', SSOed: ƒ, gIscO: ƒ, OXhhk: ƒ, lioaD: ƒ, …} 6 6
调用结果: false
300 '*' 22 '--->' 6600
6600 '%' 64 '--->' 8
调用: ƒ charAt() { [native code] } utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv 8
调用结果: m
调用: ƒ push() { [native code] } (4) ['K', 'K', 'K', 'K'] m
调用结果: 5
这次大概看清楚什么逻辑了,因为之前我有一个地方分析错了,不知道发现了没
如果入参不是5的倍数,最后一个循环处理的数据应该长度不够5,应该是在兼容这里
实际上因为 / 5进行floor,最后一次循环是大于了5,是保证能全部取到,但是总体逻辑发现还是一样的,循环内当前idx在入参取到对应字符的ASCII相加,最后*22/64在自定义映射表里面取值。
那么最终的还原代码就是
function transformMessage(plainText, options) {
var map = options.map;
var segments = options.segments;
var multiplier = options.multiplier;
// 计算每段的长度
var segmentLength = Math.floor(plainText.length / segments);
// 存储转换后的校验字符
var transformedSegments = [];
// 循环处理每一段
for (var i = 0; i < segments; i++) {
// 计算当前段的起始和结束位置
var startIndex = i * segmentLength;
var endIndex;
if (i === segments - 1) {
// 最后一段处理到文本末尾
endIndex = plainText.length;
} else {
endIndex = startIndex + segmentLength;
}
// 提取当前段的文本
var segment = plainText.slice(startIndex, endIndex);
// 计算当前段所有字符的 ASCII 码总和
var segmentSum = 0;
for (var j = 0; j < segment.length; j++) {
segmentSum = segmentSum + segment.charCodeAt(j);
}
// 根据总和生成校验字符
var index = (segmentSum * multiplier) % 64;
var checkChar = map.charAt(index);
// 添加到结果数组
transformedSegments.push(checkChar);
}
// 将所有校验字符连接成字符串
var checkString = transformedSegments.join('');
// 返回原文本 + 校验字符串
return plainText + checkString;
}
var result = transformMessage("Hello World", {
map: "utsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA-_9876543210zyxwv",
segments: 5,
multiplier: 22
});
console.log(result);
功能是:
- 将输入文本分成若干段(segments)
- 计算每段中所有字符的 ASCII 码总和
- 用这个总和生成一个校验字符
- 将所有校验字符附加到原文本后面
最后还原的md5算法如下:
/**
* File: customAlgorithm.ts
* Description: 京东加强算法
* Author: zhx47
*/
import * as CryptoJS from 'crypto-js';
import { Injectable } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import { isNullOrUndefined } from '../../utils/baseUtils';
import { TransformMessageOptions } from './type';
interface Format {
stringify(cipherParams: CryptoJS.lib.CipherParams): string;
parse(str: string): CryptoJS.lib.CipherParams;
}
interface CipherOption {
iv?: CryptoJS.lib.WordArray | undefined;
format?: Format | undefined;
[key: string]: any;
}
@Injectable()
export class CustomAlgorithm {
constructor(protected readonly clsService: ClsService) {}
MD5(message: CryptoJS.lib.WordArray | string): CryptoJS.lib.WordArray {
return CryptoJS.MD5(this.addSalt(message));
}
addSalt(message: CryptoJS.lib.WordArray | string): CryptoJS.lib.WordArray | string {
if (typeof message === 'string') {
const transformMessageOptions = this.clsService.get('h5stContext.customAlgorithm')?.transformMessageOptions;
if (transformMessageOptions) {
message = this.transformMessage(message, transformMessageOptions);
}
const salt = this.clsService.get('h5stContext.customAlgorithm')?.salt ?? '';
return message + salt;
}
return message;
}
transformMessage(plainText: string, options: TransformMessageOptions) {
const { map, segments, multiplier } = options;
const transformSegments = () => {
const segmentLength = Math.floor(plainText.length / segments);
return Array.from({ length: segments }, (_, i) => {
const startIndex = i * segmentLength;
const endIndex = i === segments - 1 ? plainText.length : startIndex + segmentLength;
const segmentSum = plainText
.slice(startIndex, endIndex)
.split('')
.reduce((sum, char) => sum + char.charCodeAt(0), 0);
return map.charAt((segmentSum * multiplier) % 64);
});
};
const transformedSegments = transformSegments();
return plainText + transformedSegments.join('');
}
}
相应的位置已经使用变量进行替换,因为每一个版本都不太一样。
代码就分析到这,因为所有的逻辑都一样,主流程基本上没什么变化,只有localTk升级到05版本了,其余都是这个逻辑
最后贡献一点京东的网页api的验证心得
- jstoken:就是请求参数里面的x-api-eid-token参数,不知道什么时候开始校验了,不过这个参数是eval加密,可以直接f12在vm里面看到明文,也是一段设备指纹,加上活动标志请求https://jra.jd.com/jsTk.do动态下发的。
- tls验证:不知道什么时候开始越来越多的接口加了tls指纹验证
- 自定义请求头:x-referer-page、x-rp-client这种别漏了
- cookie验证:有的接口似乎会验证jda、jdb那些东西,反正就是cookie里面有时候也会验证一部分值
- python下params对于queryString处理似乎和浏览器、node有一点不同,可能会异常。
- h5st里面的env京东有时候会解密进行强校验,应对方法是通过提供官方生成的h5st,脚本生成h5st以官方的解密为准,只替换部分信息。(现在双十一 5.2.2 就开了)
最后最后的提醒
此文章只作为学习交流,京东现在没什么好撸的了,动不动掉线,env不对直接踢下线。
部分接口除了踢下线还要拉黑你。
即使这个签名生成的与官方百分百一致,还是可以很轻松的查到你,毕竟你的cookie、埋点都不对,附上一份官方之前发的调查报告。