本文将会逆向美团的 JS
,逐步分析 mtgsig
算法生成,本文涉及的知识点有:
1 加密定位
1.1 定位加密文件
首先我们先来到目标网站:https://h5.waimai.meituan.com/
进入之后我们发现只要做啥操作就会需要你登录(部分设备是可以浏览首页与部分商店的),但是我们不要慌,因为在登录发送验证码这一步可是会生成加密参数的,所以我们可以无成本借助这个接口进行分析。
首先我们打开网站,进到了账号登录页面,随便输入一个手机号,点击获取验证码
点击之后会教你输入滑块验证码,我们是可以不需要管它的,因为请求已经发送了。此时不用管,可以在浏览器调试工具找到那个请求
查看一下请求头,发现其确实是有加密参数 Mtgsig
的
此时可以点击发起程序,查看该请求是由哪个 JS
文件发起的
由上图可以看出来发起的 JS
文件是:H5guard.js
,没错,这个文件其实就是包含加密的文件了!
进入文件看了一下,发现是有混淆的,并且我们全局搜索 Mtgsig
的时候也是没有搜到有价值的信息
不要慌,我们先将文件下载下来,用 Nodejs
分析一下
1.2 自动化去混淆
我们可以看到,里面很少有明文字符串,很多取变量的方式都是通过 b(xxx)
的方式取的,这是一种很常见的混淆方式:通过将变量加密后放到一个数组里面,然后再通过一个函数进行解密,这里的解密函数就是 b
函数,数组应该就是 JS
最上面定义的 a
数组(这个你可以分析 b
函数就能看出来的)
对于这种加密,我们可以自己先在 NodeJS
里面讲加密数组和解密函数定义出来,然后使用 NodeJS
进行去混淆替换。
首先我们先定义一下,看看解密函数是否可以用,要与浏览器打断点之后的结果进行比对
复制 var a = [ "4b2d3e25283f222425" , "5c3d3138" , ... .] ,
b = function b (c , d) {
var e = a[c -= 0 ];
if ( void 0 === b .CZaeRU) {
b . SfNFAJ = function (i) {
for ( var j = "" , k = i . length , l = parseInt ( "0x" + i .substr ( 0 , 2 )) , m = 2 ; m < k; m += 2 ) {
var n = parseInt ( "0x" + i .charAt (m) + i .charAt (m + 1 ));
j += String .fromCharCode (n ^ l)
}
return decodeURIComponent (j)
}
,
b .fXovsQ = {} ,
b .CZaeRU = ! 0
}
var f = b .fXovsQ[c];
return void 0 === f ? ( void 0 === b .iuCJDI && ( b .iuCJDI = ! 0 ) ,
e = b .SfNFAJ (e) ,
b .fXovsQ[c] = e) : e = f ,
e
};
console .log ( b ( 3819 ))
上面是我的解密代码,因为 a
数组太长了,我就省略了,然后我尝试解密 b(3819)
看看是什么结果,运行发现没有报错,结果是:guardReq
,在浏览器比对一下看看是不是一致的
发现是没有问题的,那么此时我们就可以使用下面的 JavaScript
代码进行修复了
复制 const fs = require ( 'fs' );
// 读取 JavaScript 文件并处理内容
fs .readFile ( 'raw.js' , 'utf8' , (err , data) => {
if (err) {
console .error ( 'Error reading the file:' , err);
return ;
}
// 使用正则表达式匹配 b(数字) 的表达式并替换
const result = data .replace ( /b\((\d + )\)/ g , (match , number) => {
const processedNumber = b ( Number (number));
return `' ${ processedNumber } '` ;
});
// 将处理后的内容写回文件
fs .writeFile ( 'fix.js' , result , 'utf8' , (err) => {
if (err) {
console .error ( 'Error writing to the file:' , err);
return ;
}
console .log ( 'File processed and updated successfully!' );
});
});
通过这一段代码跑一下,我们发现文件里面的大部分混淆都解完了
这个时候我们再来搜索一下 Mtgsig
,就很容易搜索到了
最终定位到了加密入口:
这里是比较常见的 JSVMP 特征了,主要是通过死循环和 case
语句这些控制流来打乱代码逻辑,提高分析的难度!回到浏览器发现加密的函数就是 function fV(ho, hp)
2 JSVMP 插桩
首先我们在控制流打一个日志断点
复制 `s[O]: ${s[O]}, K:${K}, O:${O}`
在这个 Switch
返回的时候打一个断点,然后刷新页面,接下来你会在 console
里面看到日志信息,我们可以权限之后将其保存为文本文件,方便分析
然后在日志里面我们可以找到我们需要的 Mtgsig
复制 {
"a1" : "1.1" ,
"a2" : 1689840879135 ,
"a3" : "wy6003v1vu665296yu1w7u6106w4u5618166z78970v979585uuwy575" ,
"a5" : "Qa+BPeZn18s3vrN7vJHgh2a38QJsjexm" ,
"a6" : "hs1.4A7RoRP0dbIKmoIPl+WUiTJBJXPFp3Ihl9BQYDss3Icc/UyKuiFKvLPX3lp4zUQ3Ryez0mwQimAowQHDvcg48JaCxT9+ATbTi8KvMDW+TIqE=" ,
"x0" : 4 ,
"d1" : "ec7cbbdc128e586eeb328a0dfef2e3b1"
}
可以看到其有好几个关键参数,我们就需要一个一个分析了
3 参数分析
3.1 a1
从上文中得知 a1
是一个字符串,为 1.1
,大概率是一个常量,为了保险起见,我们还是需要去看一下 js
和日志文件
我们先在日志文件搜索一下 1.1, 找到第一个出现的位置
发现当 s[O]
为 299
的时候还没有,当 s[O]
为 1044
的时候就有了,那这个肯定是在 299
的时候操作得到了咯,我们就跟进到 case 299
去
发现这个 1.1
就是 f5
, 那么这个 f5
又是怎么来的呢?我们点击一下,查看引用,发现他就是一个常量
3.2 a2
继续使用同样的办法分析 a2
,找到第一个位置,发现 66
的时候还没有,但是 15
的时候就有了,所以生成规则在 66
我们在代码中看到 case 66
的地方,发现这个值来自于 hy
继续点击引用,看看 hy
是怎么来的
复制 hy = (hu !== 'GET' && fN (ht , fL) ,
hu !== 'GET' && fN (ht , fM) ,
( new Date)[ 'valueOf' ]())
这个很简单,中间只是有一些干扰项,但是不难看出,hy
来源于 (new Date)['valueOf']()
3.3 a3
继续重复步骤,查找 a3
,追踪到 case 10
,此时 K
的值如下:
复制 [ [object Object], dfpId ]
查看代码发现其操作为:取数组内该对象的属性,所以我们应该继续追踪该对象是怎么来的
这边看不到对象的值,你可以修改一下插桩的代码
复制 `s[O]: ${s[O]}, K:${JSON.stringify(K, (k, v) => { if (typeof v === 'function') { return v.toString(); } return v;})}, O:${O}`
从代码可以知道,我们会将 K
这个数组进行 JSON
序列化,且如果碰到函数了,我就直接返回他的 String
格式,这个时候日志就不一样了
发现 a3
就是这个 dfpId
, 那么这个 dfpId
又是怎么来的呢?我们跟踪到 604
发现他来源于 K.push(fm)
, 所以其来源于 fm
的 k3
,因此我们要继续寻找 k3
按照上面说的办法继续追踪,发现 k3
来源于 eN['k3'] = eP['data']['dfp']
,而 eP
来源于 d2
函数的 eM
,最终找到一个变量 em
而 em
里面的 k3
来源于函数 function d0(eM)
,分析函数逻辑可以知道:
复制 _defineProperty(eM = {}, 'dfpId', eP)
eP = eN['getItem']('dfpId')
eN = window['localStorage']
所以这个值就是 window.localStorage.getItem('dfpId')
有兴趣的可以继续往上跟,看看这个是怎么来了,这边其实是 Cookie
里面的一个值,来源于 WEBDFPID
, 至于 Cookie
里面怎么来的,可以自己清除完所有的 Cookie
之后再自己分析,这边这个参数不是很重要!
3.4 a5
继续之前的步骤,找到第一个 a5
出现的位置,发现是 case 6
,所以应该追踪 case 29
,可以看到生成代码如下:
复制 K [ K . length - 3 ] = E .call ( K [ K . length - 3 ] , K [ K . length - 2 ] , K [ K . length - 1 ]);
此时的 K
如下:
复制 [
"function fI(ho){for(var hp,hq=ho[b(25)],hr=hq%3,ht=[],hv=0,hw=hq-hr;hv<hw;hv+=16383)ht[b(33)](fH(ho,hv,hv+16383>hw?hw:hv+16383));return 1===hr?(hp=ho[hq-1],ht[b(33)](f3[hp>>2]+f3[hp<<4&63]+b(3222))):2===hr&&(hp=(ho[hq-2]<<8)+ho[hq-1],ht[b(33)](f3[hp>>10]+f3[hp>>4&63]+f3[hp<<2&63]+\"=\")),ht[b(323)](\"\")}" ,
null ,
// 命名这个数组为 a1
[
104 ,
99 ,
31 ,
66 ,
34 ,
98 ,
225 ,
206 ,
91 ,
140 ,
137 ,
56 ,
139 ,
10 ,
176 ,
129 ,
120 ,
132 ,
175 ,
206 ,
23 ,
43 ,
73 ,
23
]
]
所以 a5 = fI(fo)
, 此时 fo
就是 K
的最后一项,我们暂时叫这个数组为 a1
因此接下来我们需要找这个 a1
的生成位置了,继续使用之前的办法来找到该数组出现的第一个位置
可以追踪到 case 5
,代码如下:
复制 K [ K . length - 4 ] = E .call ( K [ K . length - 4 ] , K [ K . length - 3 ] , K [ K . length - 2 ] , K [ K . length - 1 ]);
此时的 K
的值为:
复制 [
"function fI(ho){for(var hp,hq=ho[b(25)],hr=hq%3,ht=[],hv=0,hw=hq-hr;hv<hw;hv+=16383)ht[b(33)](fH(ho,hv,hv+16383>hw?hw:hv+16383));return 1===hr?(hp=ho[hq-1],ht[b(33)](f3[hp>>2]+f3[hp<<4&63]+b(3222))):2===hr&&(hp=(ho[hq-2]<<8)+ho[hq-1],ht[b(33)](f3[hp>>10]+f3[hp>>4&63]+f3[hp<<2&63]+\"=\")),ht[b(323)](\"\")}" ,
null ,
"function fP(ho,hp){for(var hq,hr,ht,hu,K=[],E=Function.prototype.call,O=180;;)switch(s[O++]){case 0:K.push(hq);continue;case 1:return K.pop();case 2:hq[hu]=K[K.length-1];continue;case 3:K[K.length-2]=K[K.length-2][K[K.length-1]];continue;case 4:K.pop();continue;case 6:K[K.length-0]=[];continue;case 11:K.push(s[O++]);continue;case 14:K[K.length-2]=K[K.length-2]+K[K.length-1];continue;case 15:K.push(null);continue;case 16:K.push(hu);continue;case 17:hu=K[K.length-1];continue;case 19:K.length-=2;continue;case 23:K[K.length-5]=E.call(K[K.length-5],K[K.length-4],K[K.length-3],K[K.length-2],K[K.length-1]);continue;case 25:return;case 28:K.push(ho);continue;case 29:K[K.length-2]=K[K.length-2]<K[K.length-1];continue;case 31:K.push(ht);continue;case 32:K.push(hr);continue;case 33:K.push(hp);continue;case 34:ht=K[K.length-1];continue;case 37:hq=K.pop();continue;case 42:K[K.length-2]=K[K.length-2]%K[K.length-1];continue;case 49:hr=K[K.length-1];continue;case 53:!K.pop()&&(O+=6);continue;case 55:!K.pop()&&(O+=52);continue;case 62:O-=58;continue;case 63:hr=K.pop();continue;case 67:hq[hr]=K[K.length-1];continue;case 68:K.length-=4;continue;case 70:hu=K.pop();continue;case 73:K[K.length-3]=E.call(K[K.length-3],K[K.length-2],K[K.length-1]);continue;case 78:K.push(hu++);continue;case 80:K.push(fD);continue;case 82:O-=12;continue;case 87:K.push(b);continue}}",
null ,
// 命名下面这个数组为 a2
[
9 ,
63 ,
178 ,
229 ,
25 ,
146 ,
110 ,
214 ,
28 ,
211 ,
174 ,
250 ,
234 ,
222 ,
30 ,
154
] ,
"[1689838680,5,\"2.0.1\",2]"
]
所以数组 a1 = fP(a2, "[1689838680,5,\"2.0.1\",2]")
接下来我们就要继续找这个 a2
是怎么来的了,继续使用相同的方法,追踪到 case 29
,此时 K 为(这边 case
相关的代码就不带大家去看了,大家可以自己去看):
复制 [
"function fz(ho){for(var hp=[],hq=0;hq<ho[b(25)];hq+=2){var hr=ho[b(30)](hq)+ho[b(30)](hq+1),ht=parseInt(hr,16);hp[b(33)](ht)}return hp}" ,
null ,
"093fb2e519926ed61cd3aefaeade1e9a"
]
可以知道:a2 = fz("093fb2e519926ed61cd3aefaeade1e9a")
,设定这个字符串为 str1
,接下来我们继续找这个 str1
是怎么来的,追踪到上一个 case 29
此时的 K
为:
复制 [
"function(s){return hex(md51(s))}" ,
{
"md5" : "function(s){return hex(md51(s))}" ,
"md5Array": "function md51(s){var i,n=s.length,state=[1732584193,-271733879,-1732584194,271733878];for(i=64;i<=s.length;i+=64)md5cycle(state,md5blk(s.subarray(i-64,i)));s=s.subarray(i-64);var tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(i=0;i<s.length;i++)tail[i>>2]|=s[i]<<(i%4<<3);if(tail[i>>2]|=128<<(i%4<<3),i>55)for(md5cycle(state,tail),i=0;i<16;i++)tail[i]=0;return tail[14]=8*n,md5cycle(state,tail),state}",
"md5ToHex" : "function hex(x){for(var i=0;i<x.length;i++)x[i]=rhex(x[i]);return x.join(\"\")}"
} ,
// 设这个值为 uniarr1
{
"0" : 104 ,
"1" : 49 ,
"2" : 46 ,
"3" : 51 ,
"4" : 50 ,
"5" : 97 ,
"6" : 111 ,
"7" : 56 ,
"8" : 109 ,
"9" : 86 ,
"10" : 90 ,
"11" : 69 ,
"12" : 115 ,
"13" : 105 ,
"14" : 81 ,
"15" : 114 ,
"16" : 121 ,
"17" : 52 ,
"18" : 51 ,
"19" : 111 ,
"20" : 54 ,
"21" : 68 ,
"22" : 54 ,
"23" : 114 ,
"24" : 81 ,
"25" : 110 ,
"26" : 120 ,
"27" : 121 ,
"28" : 66 ,
"29" : 119 ,
"30" : 50 ,
"31" : 122 ,
"32" : 104 ,
"33" : 111 ,
"34" : 53 ,
"35" : 98 ,
"36" : 115 ,
"37" : 82 ,
"38" : 88 ,
"39" : 111 ,
"40" : 98 ,
"41" : 109 ,
"42" : 43 ,
"43" : 110 ,
"44" : 113 ,
"45" : 67 ,
"46" : 115 ,
"47" : 76 ,
"48" : 115 ,
"49" : 54 ,
"50" : 57 ,
"51" : 97 ,
"52" : 53 ,
"53" : 78 ,
"54" : 75 ,
"55" : 121 ,
"56" : 109 ,
"57" : 84 ,
"58" : 117 ,
"59" : 102 ,
"60" : 75 ,
"61" : 109 ,
"62" : 52 ,
"63" : 80 ,
"64" : 90 ,
"65" : 101 ,
"66" : 111 ,
"67" : 75 ,
"68" : 65 ,
"69" : 86 ,
"70" : 52 ,
"71" : 87 ,
"72" : 88 ,
"73" : 116 ,
"74" : 99 ,
"75" : 80 ,
"76" : 119 ,
"77" : 97 ,
"78" : 87 ,
"79" : 83 ,
"80" : 73 ,
"81" : 47 ,
"82" : 51 ,
"83" : 100 ,
"84" : 116 ,
"85" : 66 ,
"86" : 66 ,
"87" : 57 ,
"88" : 79 ,
"89" : 118 ,
"90" : 102 ,
"91" : 78 ,
"92" : 85 ,
"93" : 99 ,
"94" : 79 ,
"95" : 106 ,
"96" : 107 ,
"97" : 68 ,
"98" : 113 ,
"99" : 87 ,
"100" : 115 ,
"101" : 110 ,
"102" : 74 ,
"103" : 88 ,
"104" : 83 ,
"105" : 119 ,
"106" : 97 ,
"107" : 98 ,
"108" : 121 ,
"109" : 75 ,
"110" : 51 ,
"111" : 97 ,
"112" : 90 ,
"113" : 75 ,
"114" : 50 ,
"115" : 119 ,
"116" : 55 ,
"117" : 55 ,
"118" : 80 ,
"119" : 110 ,
"120" : 90 ,
"121" : 49 ,
"122" : 89 ,
"123" : 116 ,
"124" : 85 ,
"125" : 85 ,
"126" : 85 ,
"127" : 43 ,
"128" : 43 ,
"129" : 117 ,
"130" : 109 ,
"131" : 74 ,
"132" : 112 ,
"133" : 82 ,
"134" : 102 ,
"135" : 105 ,
"136" : 71 ,
"137" : 100 ,
"138" : 57 ,
"139" : 98 ,
"140" : 105 ,
"141" : 98 ,
"142" : 106 ,
"143" : 79 ,
"144" : 85 ,
"145" : 109 ,
"146" : 117 ,
"147" : 105 ,
"148" : 120 ,
"149" : 54 ,
"150" : 55 ,
"151" : 55 ,
"152" : 74 ,
"153" : 100 ,
"154" : 100 ,
"155" : 90 ,
"156" : 99 ,
"157" : 108 ,
"158" : 99 ,
"159" : 85 ,
"160" : 115 ,
"161" : 104 ,
"162" : 97 ,
"163" : 101 ,
"164" : 119 ,
"165" : 49 ,
"166" : 87 ,
"167" : 57 ,
"168" : 55 ,
"169" : 112 ,
"170" : 110 ,
"171" : 50 ,
"172" : 103 ,
"173" : 74 ,
"174" : 53 ,
"175" : 55 ,
"176" : 72 ,
"177" : 110 ,
"178" : 121 ,
"179" : 82 ,
"180" : 87 ,
"181" : 84 ,
"182" : 101 ,
"183" : 65 ,
"184" : 119 ,
"185" : 53 ,
"186" : 121 ,
"187" : 107 ,
"188" : 82 ,
"189" : 79 ,
"190" : 97 ,
"191" : 116 ,
"192" : 110 ,
"193" : 120 ,
"194" : 56 ,
"195" : 100 ,
"196" : 102 ,
"197" : 97 ,
"198" : 108 ,
"199" : 56 ,
"200" : 105 ,
"201" : 67 ,
"202" : 79 ,
"203" : 75 ,
"204" : 107 ,
"205" : 119 ,
"206" : 85 ,
"207" : 116 ,
"208" : 55 ,
"209" : 65 ,
"210" : 88 ,
"211" : 51 ,
"212" : 116 ,
"213" : 77 ,
"214" : 50 ,
"215" : 74 ,
"216" : 49 ,
"217" : 122 ,
"218" : 84 ,
"219" : 110 ,
"220" : 47 ,
"221" : 98 ,
"222" : 79 ,
"223" : 75 ,
"224" : 113 ,
"225" : 114 ,
"226" : 86 ,
"227" : 78 ,
"228" : 100 ,
"229" : 113 ,
"230" : 56 ,
"231" : 120 ,
"232" : 82 ,
"233" : 111 ,
"234" : 112 ,
"235" : 68 ,
"236" : 120 ,
"237" : 49 ,
"238" : 79 ,
"239" : 69 ,
"240" : 114 ,
"241" : 72 ,
"242" : 88 ,
"243" : 107 ,
"244" : 72 ,
"245" : 86 ,
"246" : 50 ,
"247" : 72 ,
"248" : 85 ,
"249" : 101 ,
"250" : 110 ,
"251" : 121 ,
"252" : 55 ,
"253" : 122 ,
"254" : 113 ,
"255" : 99 ,
"256" : 115 ,
"257" : 50 ,
"258" : 113 ,
"259" : 54
}
]
因此 str1 = hex(md51(uniarr1))
,继续找 uniarr1
,追踪到 case 194
,发现其来源于
复制 uniarr1 = new Uint8Array(fw(hI))
所以我们只要继续找 hI
即可,跟踪代码得知:
所以继续看 f1 / f2
,但是其实通过传参我们知道 hp
是 true
,所以其实我们要找的是 hp
,这个其实不同的接口是不一样的,建议按照实际来分析,我们这边就只分析 f1
了,根据代码,可以知道:
到这一步其实已经定位到关键的加密处了,这里就是生成我们需要的 a6
参数的地方,这里的 fm
如下:
复制 {
"k1" : "2.0.1" ,
"k5" : 1689838680762 ,
"sessionId" : "660d685609004219b004e6926edae8ef" ,
"k2" : "1663127585004OKACMEA75613c134b6a252faa6802015be905511506" ,
"k3" : "wy6003v1vu665296yu1w7u6106w4u5618166z78970v979585uuwy575" ,
"k9" : "loading|0|0|1689838680329" ,
"k12" : [
414 ,
896
] ,
"k25" : "MacIntel" ,
"k27" : "" ,
"k30" : "" ,
"k48" : -480 ,
"k49" : "Asia/Shanghai" ,
"k68" : [
0 ,
0 ,
0 ,
0 ,
0
] ,
"k50" : "008eca200" ,
"k61" : 1 ,
"k0" : 1689909072 ,
"isShort" : 1 ,
"k47" : [
1 ,
true ,
"Infinity"
] ,
"k7" : "" ,
"k62" : "" ,
"k63" : "cd06a5c56736bd3d03dacc9cb7ecd94e73050dbaa558615c1a2fc369c7d30077" ,
"k52" : [
"332,304,SPAN,3914054" ,
"327,307,SPAN,17842" ,
"128,316,INPUT,15801" ,
"161,323,INPUT,13577"
] ,
"k53" : [
"261,26,DIV,70392034" ,
"332,304,SPAN,3914157" ,
"257,24,DIV,3902165" ,
"256,23,DIV,2197640" ,
"327,307,SPAN,17948" ,
"128,316,INPUT,15945" ,
"161,310,INPUT,13627"
] ,
"k54" : [] ,
"k55" : [
"261,26,DIV,70392032" ,
"332,304,SPAN,3914157" ,
"257,24,DIV,3902165" ,
"256,23,DIV,2197640" ,
"327,307,SPAN,17947" ,
"128,316,INPUT,15944" ,
"161,310,INPUT,13625"
] ,
"k56" : [
"261,26,70392032" ,
"332,304,3914157" ,
"257,24,3902165" ,
"256,23,2197640" ,
"327,307,17947" ,
"128,316,15944" ,
"161,310,13625"
] ,
"k57" : [
"`,INPUT,16843,0,Numpad0" ,
"\\b,INPUT,16700,Backspace,Backspace"
] ,
"k58" : [
{
"inputName" : "phoneNumInput" ,
"editStartedTimeStamp" : 1689838695862 ,
"keyboardEvent" : "0-2-1-1443" ,
"editFinishedTimeStamp" : 1689838698710
}
] ,
"k59" : [] ,
"k6" : [
"https://h5.waimai.meituan.com/login" ,
"https://h5.waimai.meituan.com/waimai/mindex/home"
] ,
"k8" : "ffffffffffffffffffffff3ffffffffffdfbfffffffffffffffffffffffffc" ,
"k11" : [
[
414 ,
896
] ,
[
414 ,
896
] ,
24 ,
24
] ,
"k13" : 1 ,
"k14" : 1 ,
"k15" : 0 ,
"k16" : 0 ,
"k17" : 2 ,
"k18" : 0 ,
"k19" : null ,
"k20" : [
"zh-CN" ,
"en" ,
"en-GB" ,
"en-US" ,
"pt"
] ,
"k21" : "zh-CN" ,
"k22" : 8 ,
"k23" : 8 ,
"k24" : "1" ,
"k26" : [] ,
"k28" : null ,
"k29" : null ,
"k31" : 1 ,
"k32" : "Google Inc." ,
"k33" : "yes" ,
"k34": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAAXNSR0IArs4c6QAAAmJJREFUSEu1lj9rFEEYxn8jGkIUEa208Q8oJlqZDxAP7LSwSWVjFRVbG6u9/QaCVlfoB7BSDttcOkFPsBJSiFgcCBaCBoLijTx7M5vZ5fbPnHcvLMxx+87vfZ6ZfWcM1loUgwGkHdjOfsVFN4G0G5VjMnC3C2kKCdCUf69i/iGgp2UYksRmUIXUXm/IrAIrrQ+M2pENMLF6HuAIeBF8UEJ12XWKlSXFUp7FMnADeAd8C+ZcpgiWzQP3v8YbbhyuexO4oPoi8Ah4Bbxxk90HvpbAdcvjN14UWLrOOht+u9kfAx8jwEqTaxdabJ7c7sPAA+A1oPFN4ArwPRIs5rp76vijE9D/ASwBT4Enbo0vAbeBLzOABbwFnGlQ3jvkVHrwp/+w2rMEFbwueieBX4HieYCbVI9OQX8M7C0AXKd6uAbDXUB2z9NqKa4Fr8JQ1oaby1v9EPg74+bya7tVscg9dax94AjwzO1qD74K63cXAG46pdznWGyZLXpD/kqV1b2aSYIeYGDbQicGOXl3WiOpUqsirxW/fQO6gegUcGdy2xLK4CpoRadzYNEi4ZrwtKvygzsHwm5WUljWE4D9XxEFTLGwrWFTwGEBGu8UD+lzwAudkbomNd2TqsuoAU9JanMZbCnZJFjbuK0kTNDZBZbLSfUds7lpBy/X2Ci7mr09X6BmTDGmm4GzsFa7yi1eS7/iX8ugSjsALx7ewRh/lSyBF6M+V+mEZpfoouKydbPbL2U73lZNe+ctx48tcf7PCp+fX+ZnPTgsZFKEwt+2/R7P7cv7bmCpErbeszI2rGo8PsquwP8AXQ2zvX9W2EcAAAAASUVORK5CYII=",
"k36" : "8b2d479621d560508819cc25fcfecf96" ,
"k37" : "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/114.0.0.0" ,
"k38" : 6 ,
"k39" : "" ,
"k40" : "" ,
"k41" : "" ,
"k42" : "" ,
"k43" : "" ,
"k46" : "srgb" ,
"k51" : "1|1|1" ,
"k64" : {
"parse" : {
"function" : "function parse() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"stringify" : {
"function" : "function stringify() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"decodeURI" : {
"function" : "function decodeURI() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"decodeURIComponent" : {
"function" : "function decodeURIComponent() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"encodeURI" : {
"function" : "function encodeURI() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"encodeURIComponent" : {
"function" : "function encodeURIComponent() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"escape" : {
"function" : "function escape() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"unescape" : {
"function" : "function unescape() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"atob" : {
"function" : "function atob() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"btoa" : {
"function" : "function btoa() { [native code] }" ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
}
} ,
"k65" : [
"https://www.dpfile.com/app/owl/static/owl_1.10.0.js" ,
"https://appsec-mobile.meituan.com/h5guard/H5guard.js"
] ,
"k66" : {
"Window" : {
"function" : "function Window() { [native code] }" ,
"typeof" : "function" ,
"Object" : {
"type" : "[object Function]"
} ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"Navigator" : {
"function" : "function Navigator() { [native code] }" ,
"typeof" : "function" ,
"Object" : {
"type" : "[object Function]"
} ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"window" : {
"function" : "[object Window]" ,
"typeof" : "object" ,
"Object" : {
"type" : "[object Window]" ,
"PropertyDescriptor" : {
"writable" : false ,
"enumerable" : false ,
"configurable" : false
}
} ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
} ,
"navigator" : {
"function" : "[object Navigator]" ,
"typeof" : "object" ,
"Object" : {
"type" : "[object Navigator]"
} ,
"toString" : {
"ret" : "function toString() { [native code] }"
}
}
} ,
"k67" : {
"location" : false ,
"document" : false ,
"top" : false
} ,
"k35" : "124.04344968475198" ,
"k44" : "1910267445" ,
"k45" : "2036a7b5-a4b0-4c59-bae6-6f50cb658213.local" ,
"k60" : "9efffffff8" ,
"k10": "PingFang SC,Arial,Arial Hebrew,Arial Rounded MT Bold,Courier,Courier New,Georgia,Helvetica,Helvetica Neue,Palatino,Times,Times New Roman,Trebuchet MS,Verdana,Academy Engraved LET,American Typewriter,Apple Color Emoji,Apple SD Gothic Neo,AVENIR,Bangla Sangam MN,Baskerville,Bodoni 72,Bodoni 72 Oldstyle,Bodoni 72 Smallcaps,Bradley Hand,Chalkboard SE,Chalkduster,Cochin,Copperplate,Didot,Euphemia UCAS,Futura,Geeza Pro,Gill Sans,Heiti SC,Heiti TC,Hiragino Kaku Gothic ProN,Hiragino Mincho ProN,Hoefler Text,Kailasa,Malayalam Sangam MN,Marion,Marker Felt,Noteworthy,OPTIMA,Papyrus,Party LET,Rockwell,Savoye LET,Sinhala Sangam MN,Snell Roundhand,Tamil Sangam MN,Telugu Sangam MN,Thonburi,Zapfino,Andale Mono,Arial Black,Arial Narrow,Arial Unicode MS,Comic Sans MS,Geneva,Impact,LUCIDA GRANDE,Microsoft Sans Serif,Monaco,Tahoma,Wingdings,Wingdings 2,Wingdings 3,Apple Chancery,Ayuthaya,Big Caslon,Brush Script MT,Chalkboard,Gujarati Sangam MN,Gurmukhi MN,Kannada Sangam MN,Krungthep,Nadeem,Oriya Sangam MN,Plantagenet Cherokee,Skia",
"k70" : {
"sessionId" : "660d685609004219b004e6926edae8ef" ,
"appkey" : "cd06a5c56736bd3d03dacc9cb7ecd94e73050dbaa558615c1a2fc369c7d30077" ,
"dfpid" : "wy6003v1vu665296yu1w7u6106w4u5618166z78970v979585uuwy575" ,
"url": "https://h5.waimai.meituan.com/login?force=true&back_url=https%3A%2F%2Fh5.waimai.meituan.com%2Fwaimai%2Fmindex%2Fhome%3Ftype%3Dmain_page%26utm_source%3D60030%26channel%3Dmtib%26code%3D121056%26msg%3D%25E8%25AF%25B7%25E6%25B1%2582%25E5%25BC%2582%25E5%25B8%25B8%2C%25E6%258B%2592%25E7%25BB%259D%25E6%2593%258D%25E4%25BD%259C%26response_code%3Dce288c2da9654631b2539aa76142b5ff%26request_code%3Dd029066306424b0aa59dfa9d1e017a60",
"clickData" : [] ,
"moveData" : [] ,
"touchData" : [] ,
"ScreenWidth" : 414 ,
"ScreenHeight" : 896 ,
"ScreenAvailHeight" : 896 ,
"ScreenAvailWidth" : 414 ,
"ScreenOrientation" : "portrait-primary" ,
"ScreenPixelDepth" : 24 ,
"ScreenColorDepth" : 24
} ,
"k4" : 1689838686054
}
实际上就是 js
收集的一些浏览器指纹等信息,篇幅太长了,这边不做过多介绍了,读者可以按照这个逻辑将代码抠出来即可!
接下来我们可以梳理一下a5
的生成逻辑:
复制 f1 = de (fm , ! 1 ) // fm 是浏览器指纹
f2 = de (fm , ! 0 ) // fm 是浏览器指纹
hI = hp ? f1 : f2; // 该接口的 hp 是 true
uniarr1 = new Uint8Array ( fw (hI))
str1 = hex ( md51 (uniarr1))
a2 = fz (str1)
a1 = fP (a2 , "[1689838680,5,\"2.0.1\",2]" ) // 这边第二个参数也可以使用相同的方法找出来生成规则
a5 = fI (a1)
生成 a1 的时候的第二个参数也可以使用相同的方法找出来生成规则,这边不做太多介绍了,读者感兴趣的可以自己去找一下,实际第一个参数是 fm
里面的 k5
, 第二个是一个检测参数,可以写死,第三个是fm
里面的 k1
,第四个是没分析,也可以写死。
3.5 a6
使用相同的办法搜索 a6
,追踪到 case 756
,发现其来源于 hI
, 而这个参数其实在分析 a5
的时候,那就不需要继续分析了,所以其实是现有 a6
然后才有 a5
的,a5
依赖于 a6 的生成!不多讲了,感兴趣的可以去分析 de
方法的逻辑
3.6 x0
多次抓包发现都是 4
,不多分析了,暂时写死吧
3.7 d1
使用相同的方法搜索 d1
,定位到 case 699
,发现其来源于 it
,查看引用发现 it
有好几地方修改的
感兴趣的可以慢慢分析,其实 it 是一步步拼接起来了,也就是先投入一个空字符串,然后经过多次的 case 367
后拼接而成,这边慢慢分析就知道了,我们找到第一个 case 367
,从前往后看,发现 K
的值如下:
复制 K:["84"]
K:["ff"]
K:["9b"]
K:["42"]
...
K:["d9"]
而我们的 d1
是 84ff9b424341249e22869a21c7bebfd9
,所以刚好就是一次两个字符两个字符拼接起来的,所以我们就需要一步步分析这个字符串又是怎么来的,这边只带大家分析第一个字符串 84
,其他的可以自己分析。
首先搜索 "84"
,找到第一个生成的位置,追踪到 case 29
查看 case 29
的代码为:
复制 K [ K . length - 3 ] = E .call ( K [ K . length - 3 ] , K [ K . length - 2 ] , K [ K . length - 1 ]);
此时的 K
为:
复制 [
"function toString() { [native code] }" ,
132 ,
16
]
所以 84
来源于 parseInt(132).toString(16)
,因此我们需要找这个 132 是哪里来的。继续往上找发现其实就是其中一个数组的 0
号元素,数组为:
复制 [ 132 , 255 , 155 , 66 , 67 , 65 , 36 , 158 , 34 , 134 , 154 , 33 , 199 , 190 , 191 , 217 ]
没错,就是我们要的值,因此我们又需要找这个数组的来源,追踪到 case 131
,发现其来源于 il
,而分析代码又可以看到 il
有很多操作的地方
从操作流程中可以看到一个 il = K.pop()
,此时为case 1019
,我们追踪到这个地方发现这个时候 il
还为空列表,所以这里应该是起始值了,因此我们可以提取这部分日志需要分析,也就是我的文件里面 441
行到 1843
行
需要注意的是 1843
行不一定是最终的,可能之前就生成了 il
,但是一直没有使用,因此我们可以看到几个跟 il
有关的 case
,他们分别是:
复制 case 2 : il[ 10 ] |= K [ K . length - 1 ];
case 23 : il[ 14 ] = K [ K . length - 1 ];
case 125 : il[ 9 ] |= K [ K . length - 1 ];
case 229 : il[ 12 ] = K [ K . length - 1 ];
case 447 : il[ 9 ] = K [ K . length - 1 ];
case 586 : il[ 10 ] = K [ K . length - 1 ];
case 839 : il[ 11 ] = K [ K . length - 1 ];
case 851 : il[ 11 ] |= K [ K . length - 1 ];
case 1069 : il[ 8 ] = K [ K . length - 1 ];
case 1073 : il[ 15 ] = K [ K . length - 1 ];
我们要根据日志分析他们的流程,判断哪个是最后一个,很容易就可以看出来是 case 1069
接下来我们应该正向分析,根据我们摘出来的日志文件进行分析,对照日志和代码可以看出来大致的逻辑,梳理如下:
日志第1行到第17
行,也就是 case 1019
到 case 690
复制
il = [] // il = K.pop()
ik = "84ff9b424341249e9a59bb3a73a410f2" // 暂时不知道哪里来的没关系, 后面再分析
im = ik
io = 0
然后这边是一直重复,日志直接从第 17
行到了 1122
行了
复制 /* case 的顺序是:
131 -> 0 -> 41 -> 15 -> 29 -> 6 -> 10 -> 11 -> 131 -> 599 -> 29 -> 6 -> 11 ->
15 -> 912-> 11 -> 224 -> 8 -> 256 -> 0 -> 41 -> 15 -> 29 -> 6 -> 10 -> 11 ->
43 -> 11 -> 690 -> 811 -> 41 -> 0 -> 41 -> 15 -> 29 -> 6 -> 256 -> 0 -> 41 ->
15 -> 29 -> 6 -> 10 -> 11 -> 256 -> 8 -> 29 -> 6 -> 21 -> 11 -> 256 -> 0 ->
41 -> 15 -> 29 -> 6 -> 10 -> 11 -> 256 -> 8 -> 15 -> 21 -> 11 -> 29 -> 6 ->
21 -> 11 -> 29 -> 6
*/
for ( let i = 0 ; i < ik . length ; i += 2 ) {
ip = parseInt ( "0x" + ik .charAt (io) + ik .charAt (io + 1 ))
il .push (ip)
}
在分析的时候为什么避免切换,可以自己把操作加载日志上,如:
慢慢分析可以得到以下逻辑:
复制 il[ 12 ] = il[ 0 ] ^ il[ 4 ] ^ iq[ 0 ]
il[ 13 ] = il[ 1 ] ^ il[ 5 ] ^ iq[ 1 ]
il[ 14 ] = il[ 2 ] ^ il[ 6 ] ^ iq[ 2 ]
il[ 15 ] = il[ 3 ] ^ il[ 7 ] ^ iq[ 3 ]
il[ 9 ] = 189 & (il[ 4 ] ^ il[ 5 ] ^ il[ 0 ])
il[ 10 ] = 219 & (il[ 5 ] ^ il[ 6 ] ^ il[ 1 ])
il[ 11 ] = 126 & (il[ 6 ] ^ il[ 7 ] ^ il[ 2 ])
fh = { "check_a" : [ 0 , 1 , 0 , 0 , 0 , 1 ]}
ir = fh[ "check_a" ]
if (ir[ 0 ] != 1 && ir[ 1 ] == 1 ){
il[ 9 ] |= 2 ;
}
if (ir[ 2 ] != 1 && ir[ 3 ] != 1 && ir[ 5 ] == 1 ){
il[ 11 ] |= 1 ;
}
il[ 8 ] = il[ 9 ] ^ il[ 10 ] ^ il[ 11 ] ^ il[ 12 ] ^ il[ 13 ] ^ il[ 14 ] ^ il[ 15 ]
然后我们只要将这个 il
数组每一项遍历转成字符串即可,代码如下:
复制 il .map ((x) => {
let i = x .toString ( 16 )
return i . length === 1 ? '0' + i : i
}) .join ( "" )
这部分逻辑梳理完毕之后,我们就要看那个 ik
是怎么来的!
同样的方法继续找,追踪到 case 29
,来源于:hex([1117519748,-1641791165,985356698,-233790349])
,继续查看这个数组怎么来的,本文篇幅太长了,方法都是一样的,篇幅太长了,这边不做过多介绍了,直接说一下,该数组生成规则如下:
复制
let num = 4294967295
let timestamp = a2
// 实际上就是 md5
let hY = hex ( md51 ( new Uint8Array ( fw (a6) .concat ( fA (timestamp & num)))))
let num1 = hashMix (dataArr , timestamp)
let arr1 = convertUnsignedIntToBytes (num1)
let num2 = hashMix ( new Uint8Array ( convertStringToBytes (a5)) , timestamp)
let arr2 = convertUnsignedIntToBytes (num2)
let num3 = num1 ^ (num & timestamp)
let num4 = (num1 ^ num2) ^ (timestamp & num)
let arr3 = convertHexToBytes ( hex ([num1 , num2 , num3 , num4]))
let str = [ ... arr1 , ... arr2 , ... arr3] .map ( function (hp) {
return (hp >>> 4 & 15 ) .toString ( 16 ) + (hp & 15 ) .toString ( 16 );
}) .join ( "" );
let ie = ` ${ a1 }${ a2 }${ a3 }${ str }${ num2 }${ hY } `
let ih = md5State (ie)
let hU = timestamp & num
let n1 = hU << x0
let n2 = hU << ( 32 - x0)
let n = n1 | n2
ih[ 0 ] = n ^ ih[ 0 ]
ih[ 1 ] = num2 ^ ih[ 1 ]
ih[ 2 ] = (num2 ^ ih[ 2 ]) ^ n
ih[ 3 ] = (ih[ 3 ] ^ ih[ 0 ])
这边的代码是重新整理过的函数名了,有兴趣的可以自己还原!