网页逆向-某东h5st参数5.1版本逆向

分析过程

首先尝试根据函数调用堆栈定位到发送请求的函数,但是失败了,往上回溯找不到加密参数的位置

全局搜索h5st字符串,找到如下函数,猜测这里就是h5st字段生成的地方

1
2
3
window.PSign.sign(colorParamSign).then(function(signedParams){\
var h5stURI = encodeURI(signedParams.h5st)
...

打下断点,跟函数

字段值如下

1
h5st=20250504202618580%3Bidiz9xpwzjh0jpp1%3Bf06cc%3Btk03wa5e41bff18nmGjvyIveP3rEVlMk31VQEz5lDra3ZFG40f-5pG8yXkN2cf0dIFEcpypmwoETXYTNV9-sQjMrbPZ5%3B2d4a0f77008b3ace8125976bebb48cbc%3B5.1%3B1746361577580%3Bt6HsMqrS_FoiGFIQ3tnQ1GXUKJImOGLm_VImOuMsCWrm0mMTLhImOuMsCmciKZYi1qrg5mYV9OLV9abi3urg9iLiMZbW1arh7urh1msm0msSo94VMZ4RMusmk_MmLZbhIlLh5Wbg5ioV_Ori9SLW2irV6uri8eoi3KriMdLmOGLm7pIRAp4WMusmk_siOGLm6aHWMusmk_Mm82ciAaLRPZoTFV4X5OImOGLm4lsmOGujM68cUZJi8O7ZeBYZMuMgM64TK1YW8lsmOGujMm7iAJ4ZMuMgMWoSMusmk_cPOuMs8uMgMqbi5lImOusmOGuj1uMgMubi5lImOusmOGuj26sm0mMi9aHWMusmOuMsCmcV1uZTFlbdohKTLlsm0mcT-dITNlHmOusmOGuj_uMgMObRMlsmOusmk_siOGLm3aHWMusmOuMsCe7iOGLm4aHWMusmOuMsCurm0mch5lImOusmOGuj_uMgMebRMlsmOusmk_Mh7uMgMibRMlsmOusmk_Mm42ciAuLmOGLm9aHWMusmOuMsCurm0m8U3lsmOusmk_chOGLm79ImOusmOGuj_uMgM_ImOusmOGuj_uMgMe4RMusmOuMsztMgMeITJdnQJlsmOGujxtsmkmsi1qYh_SLWNlYh9SbhPdIUMuMgMmrSMusmOuMsztMgMunSMusmk_Mm6WrQOCrh42YUXt8g_2si9usZgt8S3xoVAJ4ZMuMgMqYR7lsmOG_Q%3B75737cfab33731064d877d6c1bb429ed&x-api-eid-token=jdd03TYEPNXYFGXH3X6ZLFNXLYEPVXRSPWODQE2IKRPNBRQLZGKPPPHES5HHLPC42GPF3IWH4SJUFJLBEPDUNAWD5I53FZEAAAAMWTNBNCJQAAAAADDQXHJPZY35FTIX

根据函数代码可知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var getColorDataSignAjax = function(functionId,paramJson,callback,errCallback){
var colorApi = SEARCH.colorApiDomain && SEARCH.colorApiDomain.length>0 ? SEARCH.colorApiDomain : "//api.m.jd.com"
var jda = readCookie('__jda')
var djd = readCookie('ipLoc-djd')
var area = (djd!=null && djd.length>0)?djd.split('.')[0].replace(new RegExp("-", 'g'),'_'):"";
var colorParam = {
appid:'search-pc-java',
functionId: functionId,
client: 'pc',
clientVersion: '1.0.0',
t: new Date().getTime(),
body: JSON.stringify(paramJson)
}

try{
var colorParamSign = JSON.parse(JSON.stringify(colorParam))
colorParamSign['body'] = SHA256(colorParam.body);
colorParam['loginType'] = '3';
colorParam['uuid'] = jda;
colorParam['area'] = area;
var that = this;
window.PSign.sign(colorParamSign).then(function(signedParams){
//...

这里的functionId是固定值"mzhprice_getCustomRealPriceInfoForColor"paramJson的值如下

1
2
{area: '1_2810_55526_0', skuPriceInfoRequestList: Array(2)}
area: "1_2810_55526_0"

paramJsonarea值往上跟函数堆栈,在window.PSign.sign(colorParamSign)这里下断点,可以找到paramJson的生成位置

1
2
3
4
var djd = readCookie('ipLoc-djd')
var area = (djd!=null && djd.length>0)?djd.split('.')[0].replace(new RegExp("-", 'g'),'_'):"";
var param = {}
param["area"] = area;

所以生成h5st字段值的函数在这里window.PSign.sign(colorParamSign).then(function(signedParams)...,这段代码的意思当 sign() 这个Promise异步操作完成之后,把它的结果(resolved 值)作为参数 signedParams 传入这个 function 里继续处理

先扣部分代码,在本地实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const Crypto = require('crypto-js')

var paramJson = {
"area": "1_2810_55526_0",
"skuPriceInfoRequestList": [
{
"skuId": "12786499"
},
{
"skuId": "13068044"
}
]
}
var colorParam = {
appid:'search-pc-java',
functionId: 'mzhprice_getCustomRealPriceInfoForColor',
client: 'pc',
clientVersion: '1.0.0',
t: new Date().getTime(),
body: JSON.stringify(paramJson)
}

var colorParamSign = JSON.parse(JSON.stringify(colorParam));
console.log(colorParam.body)
colorParamSign['body'] = Crypto.SHA256(colorParam.body).toString();
console.log(colorParamSign['body'])
colorParam['loginType'] = '3';
colorParam['uuid'] = '143920055.17433350384561093666262.1743335038.1746449175.1746491535.14';
colorParam['area'] = '1_2810_55526_0';
console.log(colorParamSign)

然后,根据window.PSign.sign(colorParamSign).then(function(signedParams)...可以知道,window.PSign肯定是在某个地方被赋值给全局window,现在要找到这个赋值的地方

现在可以初步进行下加密,看结果对不对的上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
var CryptoJS = ...;
// 上面的CryptoJs是从网页扣出来的
window.SHA256 = function (s) {
return CryptoJS.SHA256(s).toString();
};
// 这里不用var声明,改成window比较保险
window.ParamsSign = function(){...}();
(function(){
window.PSign = new ParamsSign({
appId: "f06cc",
debug: false,
preRequest: false,
onSign: function (data) {
if (data && data.code && data.code != 200) {
console.log(JSON.stringify(data))
}
},
onRequestTokenRemotely: function (data) {
if (data && data.code && data.code != 0) {
console.log(JSON.stringify(data))
}
},
onRequestToken: function (data) {
if (data && data.code && data.code != 0) {
console.log(JSON.stringify(data))
}
}
});
})();

let paramJson = {
"area": "1_2810_55526_0",
"skuPriceInfoRequestList": [
{
"skuId": "12786499"
},
{
"skuId": "13068044"
}
]
}
let colorParam = {
appid:'search-pc-java',
functionId: 'mzhprice_getCustomRealPriceInfoForColor',
client: 'pc',
clientVersion: '1.0.0',
t: 1746519673698,
body: JSON.stringify(paramJson)
}

let colorParamSign = JSON.parse(JSON.stringify(colorParam));
colorParamSign['body'] = SHA256(colorParam.body);
colorParam['loginType'] = '3';
colorParam['uuid'] = '143920055.17433350384561093666262.1743335038.1746491535.1746519652.15';
colorParam['area'] = '1_2810_55526_0';
console.log(colorParamSign)

window.PSign.sign(colorParamSign).then(function(signedParams){
console.log(encodeURI(signedParams.h5st));
})

但是加密结果能运行,但是长度不对,是有环境没补好

分析日志

1
{obj|get:[Document_getElementsByTagName_head] -> prop:[0] -> result:[undefined]} 

说明缺少head标签,应该在userVar.js中创建head标签,再后面就是常规的补环境了,这里没什么好赘述的

重点来了,本地跑的结果长度和网页生成的结果不一样,本地的短了,需要分析代码,首先就是加密函数sdnmd这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
_$tu.prototype._$sdnmd = function(_$tO) {
'use strict';
var s = _3ggyn;
var o = _2o5yn;
var gB, _$te, _$tl, _$tP, _$tf;
var t = [];
var w = 4963;
var l, d;
l37: for (; ; ) {
switch (o[w++]) {
case 5:
t.push(_$tP);
break;
case 7:
if (t.pop())
++w;
else
w += o[w];
break;
case 8:
_$tl = t[t.length - 1];
break;
case 11:
t.push(tV);
break;
case 14:
_$tf = t[t.length - 1];
break;
case 16:
t.push(t[t.length - 1]);
t[t.length - 2] = t[t.length - 2][_1wdyn[351 + o[w++]]];
break;
case 20:
t[t.length - 5] = s.call(t[t.length - 5], t[t.length - 4], t[t.length - 3], t[t.length - 2], t[t.length - 1]);
t.length -= 4;
break;
case 23:
t.push(_$tl);
break;
case 29:
if (t[t.length - 1] != null) {
t[t.length - 2] = s.call(t[t.length - 2], t[t.length - 1]);
} else {
l = t[t.length - 2];
t[t.length - 2] = l();
}
t.length--;
break;
case 35:
t.push(_$TM);
break;
case 37:
t.push(_1wdyn[351 + o[w++]]);
break;
case 41:
//...
}
}
}

我开始想的是,会不会是两个代码流程平坦化走的顺序不一样?由于流程太复杂,直接在控制台进行hook

1
2
3
4
5
6
7
8
o = new Proxy(o, {
get(target, prop, receiver) {
if (!isNaN(prop)) {
console.log('case index (w++):', prop);
}
return Reflect.get(target, prop, receiver);
}
});

上面的hook代码可以在o数组被访问时,打印索引,但是遗憾的是两个代码的索引值相同,也就是函数执行流程并没有区别,注意这里来打印的是o[w++]m的值,由于case块是switch(o[w++]),所以case后面的还需要进行取值操作,比如打印出来w是999,case后面的值要计算o[999] = 111,所以case走的是111

然后就想,那一定是密钥不一样,简单单步跟一下发现在某个地方,网页代码和本地代码的值出现了区别,长度不一样

也就是在这里s.call(t[t.length - 2], t[t.length - 1]),调用函数的时候出现了区别,断点打在这里,跟进到函数中,这种方法当然是可以的,但是我使用的是另一种方法,可以看到,在出现问题的这步函数执行完毕之后,this的_token就应该被赋值了,但是模拟的代码中并没有被赋值

可以hook this实例的_token属性什么时候被修改,如果修改,就断下来并打印堆栈,这里要注意的是,this是_$tu的实例,不是_$tu对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var ParamsSign = function() {
...
function _$tu() {
var g0 = tV
, _$tO = arguments.length > -0x1ebd + 0x1fe1 + -0x4 * 0x49 && void (0x1cd3 + 0x3 * -0x7f + 0x2 * -0xdab) !== arguments[0x7be + -0xef * -0x21 + -0x268d] ? arguments[0xa50 + 0x14e2 + -0xf2 * 0x21] : {};
this._token = '',
this._defaultToken = '',
this._isNormal = !(0x1 * -0x2a1 + -0x2 * -0x5f3 + 0x2 * -0x4a2),
this._appId = '',
this._defaultAlgorithm = {
'local_key_1': _$T2,
'local_key_2': _$Tp,
'local_key_3': _$Td
},
this._algos = {
'MD5': _$T2,
'SHA256': _$Tp,
'HmacSHA256': _$Td,
'HmacMD5': _$Tx
}
...

}
return _$tu.prototype._$icg = {}
...
}

这里_token属性是被赋值给this实例的,不是在原型上,也不在对象上,所以尝试去hook对象或者原型都是hook不到的

也就是说

1
2
3
4
5
6
function A() {
this.a = 123;
}
console.log(Object.getOwnPropertyDescriptor(A, 'a')); // undefined
const obj = new A();
console.log(Object.getOwnPropertyDescriptor(obj, 'a')); // 有 descriptor!

想 hook 的其实是 this._token,而不是 _$tu._token,并且这个属性也没有定义在原型上,只挂在实例上

1
2
3
4
const obj = new _$tu();
console.log(obj.hasOwnProperty('_token')); // true
console.log(_$tu.prototype.hasOwnProperty('_token')); // false
console.log('_token' in obj); // true

两种hook的思路,第一种是在new实例之前,把属性定义在原型上,这样就算之后去this.xxx = aaa也会触发原型上的set/get方法,这种不好实现,第二种就是hook实例,因为这里在ParamSign函数中已经把实例赋值给全局了也就是window.PSign,直接hook实例就行

1
2
3
4
5
6
7
8
9
10
11
Object.defineProperty(window.PSign, '_token', {
configurable: true,
enumerable: true,
get() { return this.___token; },
set(v) {
console.log('[HOOK 单个实例] _token 被赋值:', v);
console.trace();
debugger;
this.___token = v;
}
});

从而定位到函数堆栈,发现在这里对_token进行操作的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_$tu.prototype._$rds = function() {
var g8 = tV, _$tO, _$te, _$tl = this;
_$Tu(this._debug, _$j.keKoA),
this._fingerprint = _$TQ.get(this._version, this._appId),
_$Tu(this._debug, g8(0x1fa).concat(this._fingerprint));
var _$tP = _$TN.get(this._fingerprint, this._appId)
, _$tf = (null === _$tP ? void (-0x109a + 0x9e4 + -0x1 * -0x6b6) : _$tP.tk) || ''
, _$tw = (null === _$tP ? void (-0x16a6 + -0x1345 + 0x29eb) : _$tP.algo) || ''
, _$tB = this._$pam(_$tf, _$tw);
_$j.bJPOJ(_$Tu, this._debug, _$j.bBsZC(_$ut, _$tO = _$ut(_$te = g8(0x299).concat(_$tB, g8(0x34e))).call(_$te, _$tf, g8(0x349))).call(_$tO, _$tw)),
_$tB ? _$Tu(this._debug, g8(0x322)) : (_$j.kjsgN(setTimeout, function() {
var g9 = g8
, _$tG = {
'oQACw': g9(0x2d2)
};
_$tl._$rgo().catch(function(_$tc) {
_$Tu(_$tl._debug, _$tG.oQACw.concat(_$tc));
});
}, -0x2d7 * -0x1 + 0x194b + 0x115 * -0x1a),
_$j.LEfzE(_$Tu, this._debug, g8(0x239)));
}
...

继续两边对比着单步跟函数,发现区别就在于_$tw = (null === _$tP ? void (-0x16a6 + -0x1345 + 0x29eb) : _$tP.algo) || ''这一句,在模拟的代码中_$tPnull,没有值,也就是_$TN.get()函数出了问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var _$TN = {
'get': function(_$tO, _$te) {
var _$tl = _$To.get(_$TF.STORAGE_KEY_TK)
, _$tP = _$j.kEfKY(_$Tf, _$T7(_$tl) ? _$tl : {}, [_$tO, _$te]);
if (!_$T7(_$tP))
return null;
var _$tf = _$tP.v || ''
, _$tw = null;
try {
_$tw = JSON.parse(_$Tz.stringify(_$Tb.parse(_$tf)));
} catch (_$tB) {
return null;
}
return _$Tv({
'e': _$tP.e,
't': _$tP.t
}) ? _$tw : null;
},

调试发现,_$tw就是token没错,但是这里在返回的时候直接return null,根本原因是_$Tv({ 'e': _$tP.e,'t': _$tP.t})这个函数执行有误,返回的是false,跟进这个函数

1
2
3
function _$Tv(_$tO) {
return !(!_$tO || !_$tO.t || !_$tO.e || -0x2177 * -0x1 + 0xf79 * 0x1 + 0x48 * -0xae === _$tO.e || _$j.ZAQpr(Date.now() - _$tO.t, _$j.uatyd(0x1dd6 + -0x29 * -0xe + 0x1 * -0x1c2c, _$tO.e)) || Date.now() - _$tO.t < 0xa09 + -0x195b + 0x4a * 0x35);
}

解混淆之后的代码如下,省略了不必要的判断,网页中返回的是false,而我们的代码中返回的是true,就是这个判断导致token无法被赋值

1
Date.now() - _$tO.t >= 1000 * _$tO.e

那么就是_$tO.t的值出错了,调试可以发现在网页中这里的值始终是1746689549622,而在模拟的代码中返回1746440897074

继续往回分析,这里的t的属性值是什么时候出错的呢?单步调试可以发现是在这里就出错了

1
2
3
4
var _$TN = {
'get': function(_$tO, _$te) {
var _$tl = _$To.get(_$TF.STORAGE_KEY_TK)
, _$tP = _$j.kEfKY(_$Tf, _$T7(_$tl) ? _$tl : {}, [_$tO, _$te]);

出问题在

1
_$tP = _$j.kEfKY(_$Tf, _$T7(_$tl) ? _$tl : {}, [_$tO, _$te])

这段代码计算的_$tP出错了,导致前面_$Tv({ 'e': _$tP.e,'t': _$tP.t})的执行出错了混淆定义在

1
2
3
'kEfKY': function(_$tM, _$ts, _$tO) {
return _$tM(_$ts, _$tO);
},

也就是_$Tf(_$T7(_$tl) ? _$tl : {},[_$tO, _$te]),两个参数经过调试都没有问题,问题出在这个_$Tf函数内部,需要跟进去

1
2
3
4
5
6
function _$Tf(_$tO, _$te) {
for (var _$tl = _$te.length, _$tP = 0x22f * 0x2 + -0x1 * 0x1407 + 0xfa9; null != _$tO && _$j.Rzqdz(_$tP, _$tl); ) {
_$tO = _$tO[_$te[_$tP++]];
}
return _$tP && _$j.jEKLa(_$tP, _$tl) ? _$tO : void (0x1 * -0x1807 + 0x793 + 0x9c * 0x1b);
}

就是在这个函数执行之后,_$tOt属性的赋值产生了差别

解混淆看一下

1
2
3
4
5
6
function _$Tf(_$tO, _$te) {
for (var _$tl = 2, _$tP = 0; 1 && (_$tP < _$tl); ) {
_$tO = _$tO[_$te[_$tP++]];
}
return _$tP && (_$tP === _$tl) ? _$tO : undefined;
}

其实就是一个数组的取值,可以等价成这种

1
2
3
4
5
6
7
function getNestedValue(obj, keys) {
let result = obj;
for (let i = 0; i < keys.length; i++) {
result = result[keys[i]];
}
return result;
}

比如_$tO的值如下

1
2
3
{i3a333pxa0qawaw8: {…}, idiz9xpwzjh0jpp1: {…}}
i3a333pxa0qawaw8: {73806: {…}}
idiz9xpwzjh0jpp1: {f06cc: {…}}

_$te的值如下

1
2
_$te
(2) ['idiz9xpwzjh0jpp1', 'f06cc']

第一次循环,取出来的是_$tO数组的_$tO['idiz9xpwzjh0jpp1']也就是{f06cc: {…}},然后把{f06cc: {…}}赋值给_$tO,第二次循环取出来的是_$tO['f06cc'],也就是f06cc的值,此时就发现,在模拟代码中的f06cc的值一直是错误的7074,并且这里v的值也不对,全局搜索可以知道这是localStorage里面存储的字段值,这个值在模拟代码中是固定值

调试网页发现每次这两个字段的值都是变化的,需要找到localStorage的字段在哪里被赋值

在Event Listener中打Script加载断点,然后执行hook代码,目标是hook这四个属性什么时候被赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(function() {
const targetKeys = ['WQ_dy1_tk_algo', 'WQ_dy1_vk', 'WQ_gather_wgl1', 'WQ_gather_cv1'];

const originalSetItem = localStorage.setItem;
localStorage.setItem = function(key, value) {
if (targetKeys.includes(key)) {
console.log(`监控到localStorage.setItem 被调用:key="${key}" value="${value}"`);
console.trace();
debugger;
}
return originalSetItem.apply(this, arguments);
};

const originalDefineProperty = Object.defineProperty;
Object.defineProperty(localStorage, 'setItem', {
value: localStorage.setItem,
configurable: true,
writable: true
});

console.log('%c[HOOK] localStorage.setItem 监控已启用', 'color: green; font-weight: bold;');
})();

注意这种hook方式不能hook到类似a.b = c这种赋值,不过网页不会这样赋值,因为这种赋值方式在页面刷新之后就没了

成功hook到赋值,在set方法里打下断点跟一下

断下来之后可以发现WQ_dy1_vk是在_$To.set(_$TF.STORAGE_KEY_VK, _$tD);进行赋值的,这也解释了为什么搜索字符串是搜不到的,因为字符串都加密了,并且值是_$te传入的,这里_$TF是个数组,存的都是需要的字段

在跟踪参数赋值的时候要注意,在登录之后,就hook不到了,想要hook参数的生成,要在登录的时候,页面加载之前就hook下来,或者不用登录,直接访问主页的时候也能hook到

那么就带来一个问题,既然代码能自己生成这四个自定义的字段值,能不能在userVar.js中不给赋值呢?发现不赋值的话,两个代码走的流程就不一样了,node的模拟代码在某一步会直接跑飞了

第一个想到的还是通过监控case的执行流,看到底是那一步出了问题,断点下载switch开始的地方,然后下hook代码,监控到在case 19处两者开始出现差异

node模拟的代码对于w.pop()返回的是false,而网页返回的是true,根本原因是执行到这里的时候w数组的值不同

1
2
3
4
5
6
7
// 网页js
w
(2) [{…}, true]

// 模拟代码
w
(2) [{…}, false]

往上找,根据hook脚本的输出可以知道前一个casecase 62,在这里下断点,可以看到执行到case 62的时候,w的数组在两边的值是一样的

当执行完w[w.length - 4] = x.call(w[w.length - 4], w[w.length - 3], w[w.length - 2], w[w.length - 1]);这句之后,w的值出现了区别,也就是问题出在这里

1
w[w.length - 4] = x.call(w[w.length - 4], w[w.length - 3], w[w.length - 2], w[w.length - 1]);

w[w.length - 4]存储的是w[1]也就是ƒ(_$tM, _$ts),函数是一样的,那问题应该出在参数上面

问题在于w[4]的值,网页中是0,而模拟代码中是undefined,所以还得往上看case块,也就是case 61这里,也就是这一句

1
2
3
w[w.length - 1] = w[w.length - 1][_1wdyn[336 + k[m++]]];
//等价于
w[4] = w[4][_1wdyn[336 + k[m++]]]

_1wdyn是个数组,结合调试可以知道,这里就是获取w数组的bu4属性,而这个属性在模拟代码中并没有这个字段,分析可知是上面_$tP的值出了问题

switch开始的时候下断点,断下来之后执行window._param = _$tP;,在这样不能hook到,因为不能hook未被赋值的对象,也就是undefined的对象hook不到,只能一步步跟,因为发现_$tP的值是在case 76处通过_$tP = w[w.length - 1];被赋值的,也就是需要hook w数组,当数组中的某个位置的元素(任意位置都行)被赋值成一个对象的时候,并且这个对象存在bu4属性的时候,断下来,打印堆栈,在switch开始处添加hook代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 假设 w 已经存在
w = new Proxy(w, {
set(target, prop, value, receiver) {
// 判断是数字索引
if (!isNaN(prop)) {
if (typeof value === 'object' && value !== null && 'bu4' in value) {
console.log(`[HOOK] w[${prop}] 被赋值了对象,且有 bu4 属性:`, value);
console.trace();
debugger;
}
}
// 继续正常赋值
target[prop] = value;
return true;
}
});

也就是说,要分析这里的y函数,通过调试发现是在function _$tm(_$tO)中的如下部分进行的bu4字段赋值

1
2
3
4
5
6
7
8
_$tJ(_$tO, aX(0x32e), function(_$tP) {
var aH = aX, _$tf, _$tw, _$tB, _$tG, _$tc, _$tD, _$tF = 0x108b + -0xa76 + 0x207 * -0x3, _$tT = !!window.location && !!window.location.host, _$tt = _$tT && -(0x979 + 0x1a5c + -0x4 * 0x8f5) !== _$M7(_$tf = window.location.host).call(_$tf, _$j.WQKLa) || _$tT && -(-0x8 * 0x3d + 0x18bd + 0x4 * -0x5b5) !== _$M7(_$tw = window.location.host).call(_$tw, aH(0x31b)), _$tW = !!document.body && !!document.body.innerHTML;
_$tt && _$tW && -(-0x4 * 0x44f + -0x217d + -0x1 * -0x32ba) !== _$M7(_$tB = document.body.innerHTML).call(_$tB, aH(0x24e)) && (_$tF |= -0x11fd + -0xa * 0x1 + 0x241 * 0x8),
_$tt && _$tW && -(0x17 * -0x76 + 0x3 * -0x42b + -0x1ed * -0xc) !== _$M7(_$tG = document.body.innerHTML).call(_$tG, aH(0x249)) && (_$tF |= -0x1f9a + 0x11db + 0xdc1),
_$tW && -(0xb37 + -0x236e * -0x1 + 0xf8c * -0x3) !== _$M7(_$tc = document.body.innerHTML).call(_$tc, aH(0x287)) && -(-0x15d3 + -0x49 * -0x3e + -0x76 * -0x9) !== _$M7(_$tD = document.body.innerHTML).call(_$tD, aH(0x1d8)) && (_$tF |= 0x2 * -0xbf9 + 0x6d1 + -0x1 * -0x1125);
var _$ta = document.documentElement;
return _$ta && _$ta.getAttribute(['di', aH(0x28e), aH(0x31f)].join('')) && (_$tF |= 0x23 * -0x86 + -0x71d + -0x198f * -0x1),
_$tF;

发现是document.body出错了,在模拟代码中返回的是字符串,而不是Object

1
2
3
sandBox.envFuncs.Document_body_get=function () {
return '<body></body>'
}

也就是说上面那种补环境的方法是错误的,修改了下补环境的代码,结果还是长度不对

1
2
3
4
5
6
7
8
9
sandBox.envFuncs.Document_body_get=function () {
for(const index in sandBox.tags){
if(sandBox.tags[index].toString()==='[object HTMLBodyElement]'){
return sandBox.tags[index]
}
}
console.log(`{sandBox.envFuncs.Document_body_get | 未找到document.body`)
return undefined
}

还得分析代码,分析function _$tm(_$tO)函数,首先可以发现代码中有很多return的逻辑

这个return的逻辑可以简单看下

1
2
3
4
5
6
7
8
9
10
11
12
13
_$tJ(_$tO, 'l', function(_$tP) {
return window.navigator.language;
}, _$tl),;
// _$tJ函数如下(解混淆后)
function _$tJ(_$tO, _$te, _$tl, _$tP) {
if (1 === _$tO && _$tj.includes(_$te) || 0 === _$tO)
try {
_$tP[_$te] = _$tl();
} catch (_$tf) {}
}
// _$tj变量的值如下(解混淆后)
var _$tj = ['pp', tV(0x34d), tV(0x2ce), 'v', _$j.jMfgK, 'pf', tV(0x21b), tV(0x1f9), _$j.kgIBT, tV(0x32e)];
// ['pp', 'sua', 'random', 'v', 'extend', 'pf', 'ccn', 'webglFp', 'canvas', 'bu4']

也就是说,上面代码简单来说就是判断_$tj数组中是否有'l'字符串,有的话就调用window.navigator.language并赋值给_$tl

所以大部分这种return是不用看的,还是要看上面那段对bu4属性赋值的代码,解混淆之后大概如下

1
2
3
4
5
6
7
8
9
10
11
12
13
_$tJ(_$tO, aX(0x32e), function(_$tP) {
var aH = aX, _$tf, _$tw, _$tB, _$tG, _$tc, _$tD;
var _$tF = 0;
var _$tT = !!window.location && !!window.location.host;//true
var _$tt = true && -(1) !== (-1) || _$tT && -(1) !== (-1);
var _$tW = !!document.body && !!document.body.innerHTML;
_$tt && _$tW && -(1) !== (-1) && (_$tF |= 1),
_$tt && _$tW && -(1) !== (-1) && (_$tF |= 2),
_$tW && -(1) !== (-1) && -(1) !== (-1) && (_$tF |= 4);
var _$ta = document.documentElement;
return _$ta && _$ta.getAttribute('dianshangji_tabid') && (_$tF |= 32),
_$tF;
}, _$tl);

根本原因就在getAttribute这里,_$ta是被模拟了环境的documentElementreturn语句的意思是如果 _$ta 存在 且 _$ta. ('dianshangji_tabid') 为真,就把 _$tF |= 32,最后返回 _$tF

1
2
3
4
5
6
7
8
9
sandBox.envFuncs.Document_documentElement_get=function () {
for(const index in sandBox.tags){
if(sandBox.tags[index].toString()==='[object HTMLHtmlElement]'){
return sandBox.tags[index]
}
}
console.log(`{sandBox.envFuncs.Document_documentElement_get | 未找到document.documentElement`)
return undefined
}

documentElement.getAttribute其实并没有实现,调用的是原型链上的Element_getAttribute

1
2
3
4
5
6
7
8
9
10
11
12
13
sandBox.envFuncs.Element_getAttribute=function (name) {
console.log(`{Element_getAttribute| get name ${name}}`)
return sandBox.getProtoAttribute.call(this,name)
}

//
//获取原型对象属性
sandBox.getProtoAttribute=function (key) {
if(this[sandBox.flags.protoFlag]){
return this[sandBox.flags.protoFlag][key]
}
return `getProtoAttribute->${key}未定义`
}

所以在模拟的代码里,会直接返回字符串,也就是类似能获取到值,所以模拟的代码中上面的return,修改完之后可以成功过了

其实这里可以不用管这四个字段的赋值,从网页中获取然后固定下来就行了,从利用的角度考虑,分析到这里就可以了,包括WQ_dy1_tk_algo属性还和XMLHTTPrequest请求有关,直接固定下来就可以了

后面分析针对评论接口的访问,其实也差不多,加密都是基本上一样的

加密函数如下

参数如下

需要分析这里的body是哪里来的

同时根据请求的body参数

1
{"requestSource":"pc","shopComment":0,"sameComment":0,"channel":null,"extInfo":{"isQzc":"0","spuId":"10148924346650","commentRate":"1","needTopAlbum":"1","bbtf":"","userGroupComment":"1"},"num":"10","pictureCommentType":"A","scval":null,"shadowMainSku":"0","shopType":"0","firstCommentGuid":"3f7028f4323425360880d1a71a2da9b4","sku":"10148924346650","category":"737;738;825","shieldCurrentComment":"1","pageSize":"10","isFirstRequest":true,"isCurrentSku":false,"sortType":"5","tagId":"","tagType":"","type":"0","pageNum":"1"}

找到对body参数的加密处,也是SHA256

现在处理加密结果长度不一样的问题,hook原型上的所有函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function() {
const proto = _$vq.prototype;

for (const key of Object.getOwnPropertyNames(proto)) {
const original = proto[key];

if (typeof original === 'function' && key !== 'constructor') {
proto[key] = function(...args) {
console.log(`[HOOKED] _$MH.prototype.${key}`);
console.log(' Arguments:', args);
const result = original.apply(this, args);
console.log(' Return value:', result);
return result;
};
}
}
})();

hook之后可以发现是clt函数里的参数变了,这就涉及到检测点了

检测点

报错堆栈检测

function _$v9()函数中,代码会new Error()操作,然后对比报错信息,是否有"node:internal/"

在本地node环境中的报错信息会有node关键字符串,最终影响bu5的参数值

过掉的方法是在node中加入堆栈报错的过滤

1
2
3
4
5
6
7
Error.prepareStackTrace = function (error, structuredStackTrace) {
return `${error.name}: ${error.message}\n` +
structuredStackTrace
.filter(callSite => !callSite.getFileName()?.startsWith("node:")) // 过滤掉 node:internal
.map(callSite => ` at ${callSite.toString()}`)
.join('\n');
};

在node中,如果访问 error.stack,引擎内部会调用Error.prepareStackTrace(error, structuredStackTrace),在这里面把报错信息给过滤掉

HTMLAllCollection检测

在某个循环中会判断HTMLAllCollection的实例是否为null,在浏览器中返回的是true,这里直接加上判断给强制赋值就行了

1
2
3
4
5
6
7
8
case 70:
j = h.pop();
if (j.toString() == "[object HTMLAllCollection]" && h[h.length - 1] == null){
h[h.length - 1] = true;
break;
}
h[h.length - 1] = h[h.length - 1] == j;
break;

此外还有一些window.toString()的检测,这种在沙箱代码中直接就过掉了,检测的关键就是上面的_$v9()函数,该函数的返回值_$vi有几个值需要注意下,如果说后面的版本更新之后,检测点大概率也是在这个函数内部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"wd": 0,
"l": 0,
"ls": 5,
"wk": 0,
"bu1": "0.1.9", // 不用管,参数
"bu3": 96, //childElement数目
"bu4": 0,
"bu5": 0,
"bu6": 26, // 和bu3一样
"bu7": 0,
"bu8": 0,
"random": "cg5G3pRaa0Wgp",
"bu12": -8
}