提示!本文章仅供学习交流,严禁用于任何商业和非法用途,如有侵权,可联系本文作者删除!
最近有人在jsvmp-某乎_x-zes-96参数算法还原(手把手教学)CSDN上的发布文章下说跟某乎的加密不一样,然后就去看了下,发现某乎更新了加密算法,具体啥时候更新的就不太清楚了,之后发现通过jsdom来补环境的方式已经行不通了,同时抠出的加密函数只有在知乎页面生成的加密参数才有效,因为暂时不清楚要补哪些环境,所以补环境到方案就先放一放,本文呢主要给大家介绍两种方式,一种是比较简单的方式通过jsrpc获取加密参数,另一种就是本文的重点,直接还原出它的算法。
aHR0cHM6Ly93d3cuemhpaHUuY29tL3NlYXJjaD9xPXB5dGhvbiZ0eXBlPWNvbnRlbnQ=
如何定位加密参数,又如何抠加密函数就不说了,可以看jsvmp-某乎_x-zes-96参数算法还原(手把手教学)这篇文章,首先把加密函数抠出来,然后测试一下抠出的函数是否有用,在前言中也有提到jsdom已经行不通了,所以直接将抠好的代码放到浏览器中运行,上一篇还原文章中说到最终是把一个md5值进行加密,所以这里抠完代码之后也直接拿md5值进行测试,这里说一下测试的时候注意点:
如上图,要注意的点就是params、cookie这两个参数一定要和生成md5值之前传的参数一致,还有要注意的是headers中是否有传x-zst-81值,我这里因为是举例找的是初始的search_v3请求,所以没有携带x-zst-81值,大家测试时候自己注意下,否则即使最后参数生成的是正确的也无法拿到数据的,之后来看看测试结果:
这里先拿到md5值,之后生成加密参数:
上图可以看到成功拿到数据,所以证明抠出来的加密函数是正确的,接下来就是完善一下代码,将代码完善成如下:
这样的话就只需要传params、cookie、x-zst-81就好了,而get_x_zse_96这个函数就是抠的下图中的:
抠完之后将上图中划线的部分改成自己抠出来的加密函数就好了,因为是在浏览器中运行的,会发现浏览器中没有md5这个方法,不过这里直接给大家简单封装了一下,放到代码中直接使用就好了:
function md5(val) {
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
function hex_md5(s) {
return binl2hex(core_md5(str2binl(s), s.length * chrsz));
}
/*
* Convert an array of little-endian words to a hex string.
*/
function binl2hex(binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for (var i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8 + 4)) & 0xF) +
hex_tab.charAt((binarray[i >> 2] >> ((i % 4) * 8)) & 0xF);
}
return str;
}
function core_md5(x, len) {
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for (var i = 0; i < x.length; i += 16) {
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936);
d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897);
d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416);
d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682);
d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510);
d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632);
c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691);
d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083);
c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438);
d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690);
c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467);
d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784);
c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i + 5], 4, -378558);
d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060);
d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174);
d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487);
d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844);
d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571);
d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359);
d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070);
d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t) {
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
}
function bit_rol(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
}
function md5_ff(a, b, c, d, x, s, t) {
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t) {
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t) {
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t) {
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}
function safe_add(x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
/*
* Convert a string to an array of little-endian words
* If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
*/
function str2binl(str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
for (var i = 0; i < str.length * chrsz; i += chrsz)
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (i % 32);
return bin;
}
return hex_md5(val)
}
到此为止在浏览器中运行的条件已经准备完毕,接下来测试一下是否正确:
发现也是能用的,接下来就是按照jsrpc的方式,将js代码放入jsrpc框架中,至于jsrpc如何使用,大家可以到公众号中看JsRpc实战-猿人学-反混淆刷题平台第20题(wasm)、网洛者-反反爬练习平台第七题(JSVMPZL - 初体验)这两篇以及k哥写的更加详细的RPC 技术及其框架 Sekiro 在爬虫逆向中的应用,加密数据一把梭这篇文章,最后将代码整合完成之后测试结果如下:
经过多次替换params参数来测试也是能够拿到数据的,通过jsrpc的方式获取加密参数就说到这里了,接下来就开始说重点部分,如何还原这个加密算法。
既然已经抠出了加密算法,且在浏览器中生成的加密参数也能够拿到数据,那么我们接下来就直接在浏览器中调试就好了,在调试之前,先做一些准备工作,把调试中的干扰给去掉,我们能看到这个更新之后的jsvpm里面有大量的三元表达式,只要有一个地方判断不对了,那么后面肯定就全部错了,所以要先排除一些能够排除的干扰项,比如里面有判断运行时间的,只要一打断点肯定会超时,就会导致分支发现变化,所以先找到以下地方,将其修改一下:
之后找到jsvmp循环一条条执行指令的位置,直接在该位置插桩:
插桩内容:
console.log("索引:", this.s, "值:", this.C.toString())
其实看过jsvmp-某乎_x-zes-96参数算法还原(手把手教学)这篇文章的对这一步应该挺熟悉了,剩下的插桩内容其实跟那篇文章里面的差不多,所以本次的插装完全可以参考那篇文章,这里就不做过多说明了。
先说一下更新之后的算法,与上一次相比首先是可以看出本次更新之后生成的加密参数,在传入md5值不变的情况下生成的加密参数是不一样的,而一般导致这种情况的无非是加密参数里面加了时间戳或者随机值,至于是哪种待会儿带着大家见分晓,细心的同学还会发现以前版本生成的加密参数长度是44位,现在生成的加密长度是64位,这些是从表面上看出来的,接下就直接来分析算法,为大家解开本次算法神秘的面纱。
话不多说,插完桩后直接运行(因为会检测环境,所以需要在打开知乎页面下),如图:
看到这个图就不用我说怎么开始分析了吧,在讲旧版算法的时候已经说的是相当详细了,所以这里直接跳过这一段,先来到生成这串加密参数的第一个值的位置:
大家看图,日志信息都输出的很详细了,还记的旧版本的算法里面会有一个初始的字符串,然后加密参数就是从里面依次获取的,这次也是一样,直接开始记录重要的步骤,因为是从下往上看的,所以做记录的时候也按这个步骤:(无关或者不重要的步骤就直接跳过了,至于为什么还是参考之前那篇文章)
214 ^ 58 = 236
18 << 8 = 4608
236 | 4608 = 4844
205 << 16 = 13434880
4844 | 13434880 = 13439724
13439724 & 63 = 44
"6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE".charAt(44) //u
从记录中能看到得出一个值的过程,接来下看看后面几个值怎么生成的并记录下:
13439724 >>> 6 = 209995
209995 & 63 = 11
"6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE".charAt(11) //c
13439724 >>> 12 = 3281
3281 & 63 = 17
"6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE".charAt(17) //F
13439724 >>> 18 = 51
51 & 63 = 51
"6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE".charAt(51) //K
看到这个地方,还原过旧版本的同学就能看出来,这里是和旧版本一样的方式,在旧版本中是总共分成11组,每组4个值,每组每次计算出第一个值,之后第二到第四个值就是通过第一个值中最后计算出来的一个数值来生成,最终生成的加密参数长度是44,本次算法中也是一样,不过这次是分成了16组,每组4个值,最终生成的加密参数长度是64,所以后面的值怎么生成的也就不用继续说了吧,不知道看看旧版的分析文章就好了。
到目前为止能够看出的也就是第二到第四个值怎么来的,而第一个值我们虽然看到了计算过程,但是参数计算的数值从哪里来的其实是不知道的:
上图中划出来的三个值就是目前不知道哪里来的,不过不用慌,日志继续往上看,然后看到如下图:
对数字比较敏感的同学一般看到这个地方就能发现点什么了,圈出来的三个值不就是我上面划出来的吗,对数字不是那么敏感的同学,可能会因为这个颜色不太显眼而忽略这个地方,不过也肯定会继续往上看的,然后看到如下:
圈出来的这行数组该显眼一点了吧,此时总该注意到这一行了吧,这是一个48位数组,到这里应该会有怀疑了吧,计算出第一组加密值中第一个值的参数来自于这个48位数组,如果到这里还是不太明白为什么会注意这个48位数组的,可以从刚才记录分析第一组加密值那里开始继续记录分析剩下的十五组加密值,当分析的差不多的时候在回过头来看看这个48位数组,之后得出的结论就是:每一组加密值中第一个值都是由这个48位数组中的三个元素参与计算的,之前也有说过本次加密参数总共分成16组,每组中有3个元素参与,正好对应这个48位数组,接下来就是探究这个48位数组如何来的。
在逆向的过程中我们需要有怀疑的精神,我自己反正就有怀疑,那么怀疑什么呢?不管是老版本还是现在的新版本,都是传入了一个md5值,最终生成了加密参数,但是到此时都没有看到这个md5值,所以当时我就直接去搜索这个md5值了,直接找到了md5值最开始出现的地方:
到这里之后就直接开始记录这个md5值变化的过程了,记录如下:
md5_charCodeAt_arr=[]
md5_str = '027d51eae1600614794230b45f8db859'
md5_str.charCodeAt(0) // 48
md5_charCodeAt_arr.push(48)
md5_str.charCodeAt(1) // 50
md5_charCodeAt_arr.push(50)
md5_str.charCodeAt(2) // 55
md5_charCodeAt_arr.push(55)
md5_str.charCodeAt(3) // 100
md5_charCodeAt_arr.push(100)
.
.
.
md5_str.charCodeAt(31)//57
md5_charCodeAt_arr.push(57)
//中间打省略号因为发现了这个规律,这里就直接可以用代码实现这个步骤了
md5_charCodeAt_arr=[]
for (let i = 0; i <md5_str.length ; i++) {
md5_charCodeAt_arr.push(md5_str.charCodeAt(i))
}
上面是md5值第一阶段的过程记录,接下来继续:
上图圈出来的两个部分不正好就是开始分析的时候说的,加密参数变化无非是里面加了时间戳或者随机值,这里刚好两个都出现了,接着记录:
//获取当前时间戳(Date.now()) - 另外一个时间戳
1659703064282 - 1659703064173 = 109
//获取随机数(Math.random()) * 127
0.3076580506661184 * 127 = 39.072572434597035
//通过Math.floor向下取整
Math.floor(39.072572434597035) =39
上面的127是一个定值,多次对比之后就能确定下来了,之后接着往下看:
//向数组开头添加一个新的元素0
md5_charCodeAt_arr.unshift(0)
//向数组开头添加一个新的元素39,也就是上面计算出来的
md5_charCodeAt_arr.unshift(39)
//往数组中放15个14,此时数组的长度为48了
md5_charCodeAt_arr.push(14)
这里往数组中放15个14也是可以经过多次验证的发现是固定的逻辑,接着继续往下看:
//之后通过slice取0-16位
md5_charCodeAt_arr.slice(0,16)
注意上图,这里开始就又要进行计算了,这里由于记录比较长,就只记录前面和后面几步了:
//截取的16位数组
[39, 0, 48, 50, 55, 100, 53, 49, 101, 97, 101, 49, 54, 48, 48, 54]
//这里开始是从上面截取的16位数组中依次取一位数与相应的值进行异或运算,之后将计算的出数值放入新的数组中
39 ^ 48 = 23
23 ^ 42 = 61
---
0 ^ 53 = 53
53 ^ 42 = 31
---
48 ^ 57 = 9
9 ^ 42 = 35
.
.
.
48 ^ 100 = 84
84 ^ 42 = 126
---
54 ^ 55 = 1
1 ^ 42 = 43
//因为是16位的数组,每个值都需要计算,所以相当于是分了16组,每组计算的结果都放入了一个新的数组中
new_16_array=[61,31,35,40,40,125,121,44,43,122,122,126,44,43,126,43]
经过上面一系列运算之后就来到了下面这步:
这里是将这个新组成的16位数组放入函数中进行计算,然后又生成一个新的16数组,直接直接将这个函数贴出来:
function (e) {
var t = new Array(16)
, n = new Array(36);
n[0] = B(e, 0),
n[1] = B(e, 4),
n[2] = B(e, 8),
n[3] = B(e, 12);
for (var r = 0; r < 32; r++) {
var o = G(n[r + 1] ^ n[r + 2] ^ n[r + 3] ^ h.zk[r]);
n[r + 4] = n[r] ^ o
}
return i(n[35], t, 0),
i(n[34], t, 4),
i(n[33], t, 8),
i(n[32], t, 12),
t
}
这个函数是加密函数中的,所以直接抠出来就好了,剩下的就是缺啥补啥就行了。接着继续看:
//取md5_charCodeAt_arr中的16-48位 ,也就是之前前面得到的48位数组
_16_48_arry = md5_charCodeAt_arr.slice(16, 48)
//之后又是将这个截取出来的32位和上面刚生成的16位数组放入下面函数中
function(e, t) {
for (var n = [], r = e.length, i = 0; 0 < r; r -= 16) {
for (var o = e.slice(16 * i, 16 * (i + 1)), a = new Array(16), c = 0; c < 16; c++)
a[c] = o[c] ^ t[c];
t = __g.r(a),
n = n.concat(t),
i++
}
return n
}
这个函数也是从加密函数中抠出来的,还是缺啥补啥,这一步完成之后会得到一个新的32位数组,最后将刚才生成的16位数组和这个32位数组通过concat合在一起就生成了开始需要参数计算的48位数组,到此48位数组如何生成的就讲解完成了。
再说一下刚才的时间戳和随机值,在后面生成48位数组的过程中会发现这个时间戳其实并没有参与进去,所以其实导致最终加密参数变化的就是这个随机值。本篇文章的分析到这里就差不多了,这篇文章也主要是为了分析这个48位数组怎么来的,至于最终的加密参数还不知道如何分析的直接看之前分析旧版的那篇文章就好了。
最后验证一下还原之后的算法是否正确,首先看看js版本的:
再来看看python版本的:
能够看到不管是js版本还是python版本的都能够拿到数据。
旧版本算法分析文章
jsvmp-某乎_x-zes-96参数算法还原(手把手教学)