文章总结: 本文详细分析了xhs-xsvmpwasm的逆向过程,通过添加日志点解析混淆代码,揭示了参数生成流程包括随机数处理、时间戳转换和MD5操作。文章深入剖析了两个WASM函数的功能,一个是与固定密钥进行异或操作,另一个是Base58加密。最终提供了完整的Node.js实现代码,可用于模拟签名生成过程。
综合评分: 85
文章分类: 爬虫,逆向分析,WEB安全
【网页逆向】xhs-xs vmp wasm
原创
可乐还是百事好
爬虫逆向小林哥
2025年10月17日 14:59
江苏
过程
入口
参数的拼接跟前一个版本一样,然后后面的参数是拼接好的uri的md5
主要看 window.mnsv2(c, d) 这里的vmp,其加载是在当前文件的下方
然后整个vmp是经过混淆的,嫌麻烦可以解下混淆,很简单的基本混淆
日志
下图 标了 循环 栈、索引,多层if else 嵌套的判断待执行指令
经过几次测试,无论是直接输出栈 _0x1d4fa7 还是序列化后还是通过 …_0x1d4fa7 打散都不太好分析,里面内容太多了
这时候发现下面的指令都在给_0x1d4fa7[_0x1c5004] fu赋值
那么我们直接给它的值和索引下日志点,同时注意第一个判断点会改变索引的大小
if (_0x30595f = parseInt('' + _0x484194[_0xf3aac1] + _0x484194[_0xf3aac1 + 0x1], 0x10),
_0xf3aac1 += 0x2,
_0x12b83a[_0x564652(0x7e)](0x46, _0x30595f)) {
for (_0x2f0111 = _0x1d4fa7[_0x1c5004--],
_0x1d4fa7[_0x1c5004] = _0x1d4fa7[_0x1c5004] != _0x2f0111; _0x12b83a[_0x564652(0x91)](_0x30595f, 0x10c3); )
0x10c3 === _0x1c5004 && (_0x1d4fa7[_0x1c5004--][_0x1c5004] = _0x1d4fa7[_0x1c5004++]),
_0x1c5004--;
}
因此我们在第一个else后下日志点,同时过滤其值===undefined的情况
if(_0x1d4fa7[_0x1c5004] !== undefined){console.log('索引: ', _0x30595f, '值: ', _0x1d4fa7[_0x1c5004])}; false
注意日志打的是先输出逻辑走向,然后下一处才是它生成的值 好了,比如下图标记位置 执行了75的判断才会生成下面的一个unit8Arrary
分析的实话看不出来就需要到具体的指令逻辑里面去下更详细的断点了,比如这里的0x4b
console.log('对象: ', _0x31793d, '函数: ',_0x568eef, '参数: ', _0x4a995d); false
注意这里的断点需要下到对应位置,因为这里是三目运算的嵌套,下到最前面输出不出来
好了这样日志就会详细一点了,同样在需要进一步分析的地方尽可能日志打全一些
encode 取长度
流程
encode
/api/sns/web/v1/homefeed{"cursor_score":"","num":18,"refresh_type":1,"note_index":10,"unread_begin_note_id":"","unread_end_note_id":"","unread_note_count":0,"category":"homefeed.career_v3","search_key":"","need_num":8,"image_formats":["jpg","webp","avif"],"need_filter_image":false}
[47, 97, 112, 105, 47, 115, 110, 115, 47, 119, 101, 98, 47, 118, 49, 47, 104, 111, 109, 101, 102, 101, 101, 100, 123, 34, 99, 117, 114, 115, 111, 114, 95, 115, 99, 111, 114, 101, 34, 58, 34, 34, 44, 34, 110, 117, 109, 34, 58, 49, 56, 44, 34, 114, 101, 102, 114, 101, 115, 104, 95, 116, 121, 112, 101, 34, 58, 49, 44, 34, 110, 111, 116, 101, 95, 105, 110, 100, 101, 120, 34, 58, 49, 48, 44, 34, 117, 110, 114, 101, 97, 100, 95, 98, 101, 103, 105, 110, 95, 110, …]
length
[47, 97, 112, 105, 47, 115, 110, 115, 47, 119, 101, 98, 47, 118, 49, 47, 104, 111, 109, 101, 102, 101, 101, 100, 123, 34, 99, 117, 114, 115,
282
时间戳
1758099180450
随机数
0.2750698210600291 * 固定值(4294967295)= 1181415885.2943273 取整
转字节
1181415885.to_bytes = [205, 249, 106, 70] # 样例
拼接
[119, 104, 96, 41] # 固定
[205, 249, 106, 70]
[119, 104, 96, 41, 205, 249, 106, 70]
挑一些流程讲 1
1 0.2750698210600291 * 固定值(4294967295)= 1181415885.2943273 取整
2 然后得出一个unit8Arrary [205, 249, 106, 70]
这里的流程主要走的是43和58
我们下断
console.log('左: ', _0x1d4fa7[_0x1c5004], '&' , '右: ',_0x2f0111, '=', _0x1d4fa7[_0x1c5004] & _0x2f0111); false
console.log('左: ', _0x1d4fa7[_0x1c5004], '>>>' , '右: ',_0x2f0111, '=', _0x1d4fa7[_0x1c5004] '>>>' _0x2f0111); false
最新日志在执行下面操作
借用AI梳理流程
给出Node
function splitIntToBytes(num) {
// 创建一个4字节的Buffer(用于存储32位整数)
const buffer = Buffer.alloc(4);
// 以小端序写入整数(低字节在前,与示例中的拆分顺序一致)
buffer.writeUInt32LE(num, 0);
const byte1 = buffer[0];
const byte2 = buffer[1];
const byte3 = buffer[2];
const byte4 = buffer[3];
return [byte1, byte2, byte3, byte4];
}
// 测试示例
const num = 1181415885;
const bytes = splitIntToBytes(num);
console.log(bytes
经过简单分析发现是不断拼接concat一个数组,接下来我会根据拼接过程来梳理流程
data_1 = [119, 104, 96, 41] // 固定
data_2 = splitIntToBytes(random_num)
接下来看data_3,用到了上面的一个时间戳,这么跟前面的处理逻辑还不一样
首先是8位的 ,然后除了值之后前四位与41进行^操作,然后更改下第一个位
const resultArray = [
162, 139, 224, 86,
153, 1, 0, 0
].map(item => item ^ 41);
resultArray[0] = 91 ^ 41
console.log(resultArray)
let timestamp = Date.now(),
time_bytes = splitIntToBytes(timestamp, 8);
time_bytes[0] = (time_bytes.slice(1,5).reduce((a, b) => a + b) & 255) + (time_bytes.slice(5,8).reduce((a, b) => a + b) & 255)
let data_3 = time_bytes.map(item => item ^ 41);
接下来从cookie中读取loadts接着转换
后面又出现了to_bytes操作,从现有日志看不出来这个23 22是哪里来的
需要上更详细日志,在后面还有拿windows对象属性的数量,也可写死
在后面就是uri-bytes长度
后面再看看对md5的操作 把16进制字符串转字节
9b5fcc2c58f53e56a2c4c8541328f420
'9b' parseInt('9b', 16) -> 155 205 = 86
'5f' parseInt('5f', 16) -> 95 205 = 146
......
这里 155 95 是十六进制字符串转的值 然后根据固定值 205 操作得到结果
不明确的就是这里205是否固定以及和205进行了什么操作
日志往前找发现之前生成的随机值* 4294967295 = 1181415885 & 255 = 205 一直没用是在这里生成的
后面就是155 ^ 205 = 86
let md5_bytes = Array.from(Buffer.from(md5, 'hex')),
data_8 = md5_bytes.map(item => item ^ (random_num & 255))
接着拿a1操作
19878b09dd10mm7ocfy3ser4w5vb34ekbc1zpexrn50000320765
encode
Uint8Array(52) [49, 57, 56, 55, 56, 98, 48, 57, 100, 100, 49, 48, 109, 109, 55, 111, 99, 102, 121, 51, 115,
这里的逻辑就是
let a1_bytes = Array.from(encode(a1)),
data_9 = [a1_bytes.length, ...a1_bytes];
再接着encode xhs-pc-web,逻辑和a1一样
最后将上面的组合起来成为了一个长度124的array
function encode(str) {
const encoder = new TextEncoder();
return encoder.encode(str)
}
function splitIntToBytes(num, size) {
const buffer = Buffer.alloc(size);
switch (size) {
case 1:
buffer.writeUInt8(num, 0);
break;
case 2:
buffer.writeUInt16LE(num, 0);
break;
case 4:
buffer.writeUInt32LE(num, 0);
break;
case 8:
buffer.writeBigUInt64LE(BigInt(num), 0); // 处理64位大整数
break;
default:
throw new Error(`不支持的字节长度: ${size}(仅支持1、2、4、8)`);
}
return [...buffer];
}
function sign(uri, md5, loadts, a1) {
let uri_encode = encode(uri),
random_num = 1181415885;//Math.floor(Math.random() * 4294967295);
const data_1 = [119, 104, 96, 41];
let data_2 = splitIntToBytes(random_num, 4);
let timestamp = 1758099180450,//Date.now(),
time_bytes = splitIntToBytes(timestamp, 8);
time_bytes[0] = (time_bytes.slice(1,5).reduce((a, b) => a + b) & 255) + (time_bytes.slice(5,8).reduce((a, b) => a + b) & 255)
let data_3 = time_bytes.map(item => item ^ 41);
let data_4 = splitIntToBytes(loadts, 8); // cookie loadts
let data_5 = splitIntToBytes(23, 4); // 随机数
let data_6 = splitIntToBytes(1296, 4); // windows对象属性数量
let data_7 = splitIntToBytes(uri_encode.length, 4)
let md5_bytes = Array.from(Buffer.from(md5, 'hex')),
data_8 = md5_bytes.map(item => item ^ (random_num & 255))
let a1_bytes = Array.from(encode(a1)),
data_9 = [a1_bytes.length, ...a1_bytes];
let app_id_bytes = Array.from(encode("xhs-pc-web")),
data_10 = [app_id_bytes.length, ...app_id_bytes];
let data_11 = [1, (random_num & 255) ^ 115, 249, 83, 103, 103, 201, 181, 131, 99, 94, 7, 68, 250, 132, 21];
let array_bytes = [...data_1, ...data_2, ...data_3, ...data_4, ...data_5, ...data_6, ...data_7, ...data_8, ...data_9, ...data_10, ...data_11]
console.log(array_bytes)
}
let uri = '/api/sns/web/v1/homefeed{"cursor_score":"","num":18,"refresh_type":1,"note_index":10,"unread_begin_note_id":"","unread_end_note_id":"","unread_note_count":0,"category":"homefeed.career_v3","search_key":"","need_num":8,"image_formats":["jpg","webp","avif"],"need_filter_image":false}'
sign(uri, "9b5fcc2c58f53e56a2c4c8541328f420", 1758098914362, "19878b09dd10mm7ocfy3ser4w5vb34ekbc1zpexrn50000320765")
这边新出了个,长度还是124应该是内部做了位运算
继续下面看日志
WASM
下面调了两个函数,先来看第一个
1 申请内存
2 函数 xy_18d12b8e541b1fd80b1c3c2eeb63a208
3 入参 [68376, 124, 858975407]
4 输出 内存68504
最后从buffer里面slice [ArrayBuffer(16777216), 68504, 124] 输出[216, 63, 75, 188, 7, 156, 216, 159, 158, 212……
中间注意释放了两次内存
第二个函数
1 申请内存
2 函数 xy_daa92688ff9309e1a29d25224fd0164a
3 入参 [68376, 124]
4 输出 内存 68760
最后调用7b结尾的函数就出了结果
上面方案是由wasm补环境实现,从开源的算法xhshow 总结下上面wasm两处函数作用:
xy_18d12b8e541b1fd80b1c3c2eeb63a208
与一组固定密钥进行异或
let array_bytes = [...data_1, ...data_2, ...data_3, ...data_4, ...data_5, ...data_6, ...data_7, ...data_8, ...data_9, ...data_10, ...data_11]
let key = [175, 87, 43, 149, 202, 101, 178, 217, 236, 118, 187, 93, 46, 151, 203, 101, 50, 153, 204, 102, 51, 153, 204,
102, 51, 153, 204, 230, 115, 57, 156, 206, 103, 51, 25, 12, 6, 3, 1, 0, 0, 0, 0, 0, 128, 64, 32, 144, 72, 36,
18, 137, 196, 226, 113, 56, 28, 14, 7, 3, 1, 128, 64, 160, 80, 40, 20, 138, 197, 98, 49, 24, 12, 6, 131, 193,
96, 48, 152, 76, 38, 147, 201, 100, 178, 89, 172, 86, 171, 213, 234, 245, 250, 253, 126, 63, 159, 79, 39,
147, 73, 164, 210, 233, 116, 58, 157, 78, 39, 147, 73, 164, 210, 233, 244, 122, 61, 30, 143, 71, 35, 145, 72,
164]
console.log(JSON.stringify(array_bytes))
const arrayXor = (a, b) => a.map((val, i) => val ^ (b[i] || 0));
const uint8Arr = new Uint8Array(arrayXor(array_bytes, key))
xy_daa92688ff9309e1a29d25224fd0164a
base58加密
function Base58EncodeFromUint8Array(uint8Array) {
let num = 0n;
for (let i = 0; i < uint8Array.length; i++) {
num = num * 256n + BigInt(uint8Array[i]);
}
let result = '';
while (num > 0n) {
const remainder = num % 58n;
num = num / 58n;
result = BASE58_ALPHABET[Number(remainder)] + result;
}
return result;
}
广子
❝
有需要用于爬虫、逆向、抓包的测试机又不想花时间折腾Root、刷机、环境配置可以来看下下面联系方式,或者B站视频评论区见
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:爬虫逆向小林哥 可乐还是百事好《【网页逆向】xhs-xs vmp wasm》