前言
本文章仅做安全学习交流用途,严禁作其他用途,如果侵犯您的权益请联系我删除。
所用工具: WeChatOpenDevTools
分析
请求需要的参数

signature
signature是参数拼接后进行标准MD5

x
x参数是这句代码生成的
A()[Tt(261)](A()[Tt(262)](Mt)[Tt(u[74])](A()[Tt(263)][Tt(264)]))[Tt(u[74])]()
是对Mt字符串做了一些处理

把
Tt(261)这些在控制台输出
而A()可能是返回一个
crypto,由此去进行猜测实现其逻辑
const crypto = require('crypto');
function doubleHash(data) {
// 1. 计算 SHA256 哈希(返回 Buffer)
const sha256Hash = crypto.createHash('sha256').update(data).digest('hex');
// 2. 计算 MD5 哈希(基于 SHA256 的结果)
const md5Hash = crypto.createHash('md5').update(sha256Hash).digest('hex');
return md5Hash; // 返回 32 位 Hex 字符串
}
// 示例用法
const Mt = '12087-145928-1745421493352-7720161b2684ca2d5d1426b6b0ea1307'; // 你的原始数据
const result = doubleHash(Mt);
console.log(result); // 输出:ca82cb3d69f2eef9b5494585bd4b84ac
这与我们在控制台中得到的一样

很多地方用到了数组u
const u = [0, 1, 8, 255, "length", "undefined", "a", 2, void 0, "d", 117, 63, 6, "fromCodePoint", 7, 12, "push", 169, 91, 8191, 88, 13, 14, "b", 39, 51, 166, "i", 5, 161, 173, 209, 3, 127, 76, 9, "g", 205, "f", 113, 4, 128, 246, 57, "l", "P", !1, "wx", 223, "tt", 235, 239, 243, 11, 17, 19, 23, 1e3, 224, 152, "h", 50, 40, 62, 65, 46, 249, 250, 251, 240, 234, 214, "e", "-", 244];
那么现在的目标就是分析Mt参数
const Mt = "" + Ht + u[73] + se + u[73] + X + u[73] + Ae;
把他分为四段
第一段:12087
来自这句代码
//X是时间戳1745421493352,mt是'1025204107460280321'
let Ht = "1" + mt[Tt(u[52])](-u[7]) + X[Tt(u[74])]()[Tt(u[52])](-u[7]);
//也就是
let Ht = "1" + mt['slice'](-2) + X['toString']()['slice'](-2);
那么第一段就是取1025204107460280321(我记得这个是userId)的后两位和时间戳的后两位
第二段:145928
来自这句代码
fe = ee(Tt(245))[Tt(u[42])](X / u[57]) % u[57] || u[1];
//也就是
fe = ee('oVapsI')['floor'](X / 1000) % 1000 || 1;
//fe赋值给了se
se = fe;
//se后面又进行了自增操作
se += pn(we[u[0]] % 10, u[7]) * fe % 1000003
通过一些代码实现了se,也就是第二段的生成
var we = [
[3,0,[3, 5, 8, 12]],
[5,1,[3, 5, 8, 12]],
[8,2,[3, 5, 8, 12]],
[12,3,[3, 5, 8, 12]],
[7,0,[7, 11, 2]],
[11,1,[7, 11, 2]],
[2,2,[7, 11, 2]],
[19,0,[19, 23, 17]],
[23,1,[19, 23, 17]],
[17,2,[19, 23, 17]]
]
let se = 739;
const u = [0, 1, 8, 255, "length", "undefined", "a", 2, void 0, "d", 117, 63, 6, "fromCodePoint", 7, 12, "push", 169, 91, 8191, 88, 13, 14, "b", 39, 51, 166, "i", 5, 161, 173, 209, 3, 127, 76, 9, "g", 205, "f", 113, 4, 128, 246, 57, "l", "P", !1, "wx", 223, "tt", 235, 239, 243, 11, 17, 19, 23, 1e3, 224, 152, "h", 50, 40, 62, 65, 46, 249, 250, 251, 240, 234, 214, "e", "-", 244];
const fe = 739;
const pn = (a, b) => a ** b;
// 模拟循环
for (let i = 0; i < we.length; i++) {
//we[u[4]] = u[1];
se += pn(we[i][0] % 10, 2) * fe % 1000003;
console.log(`Step ${i}: se = ${se}`);
}
现在只要分析出fe的生成即可,而fe来自这句代码
fe = ee(Tt(245))[Tt(u[42])](X / u[57]) % u[57] || u[1];
//也就是
fe = ee('oVapsI')['floor'](X / 1000) % 1000 || 1;
X是时间戳,在这次的请求中,发现时间戳和fe似乎有一种简单的关系,就是fe是时间戳的第8-10位

再次请求发现确实是这样

那么
fe的实现代码如下
const X = Date.now(); // 当前时间戳(或传入的 X)
const fe = Math.floor(X / 1000) % 1000 || 1;
console.log(X);
console.log(fe); // 输出 1-999 的整数
第二段的整体生成代码如下
const X = Date.now(); // 当前时间戳(或传入的 X)
const fe = Math.floor(X / 1000) % 1000 || 1;
var we = [
[3,0,[3, 5, 8, 12]],
[5,1,[3, 5, 8, 12]],
[8,2,[3, 5, 8, 12]],
[12,3,[3, 5, 8, 12]],
[7,0,[7, 11, 2]],
[11,1,[7, 11, 2]],
[2,2,[7, 11, 2]],
[19,0,[19, 23, 17]],
[23,1,[19, 23, 17]],
[17,2,[19, 23, 17]]
]
let se = 739;
const u = [0, 1, 8, 255, "length", "undefined", "a", 2, void 0, "d", 117, 63, 6, "fromCodePoint", 7, 12, "push", 169, 91, 8191, 88, 13, 14, "b", 39, 51, 166, "i", 5, 161, 173, 209, 3, 127, 76, 9, "g", 205, "f", 113, 4, 128, 246, 57, "l", "P", !1, "wx", 223, "tt", 235, 239, 243, 11, 17, 19, 23, 1e3, 224, 152, "h", 50, 40, 62, 65, 46, 249, 250, 251, 240, 234, 214, "e", "-", 244];
const pn = (a, b) => a ** b;
// 模拟循环
for (let i = 0; i < we.length; i++) {
//we[u[4]] = u[1];
se += pn(we[i][0] % 10, 2) * fe % 1000003;
}
console.log(se);
第三段:1745421493352
这个应该是时间戳
第四段:7720161b2684ca2d5d1426b6b0ea1307
第四段大概率来自这里的for循环

把这一段丢给ds分析,直接给我返回了生成的代码
const crypto = require('crypto');
// 模拟 u 数组(假设部分值)
const u = [0, 1, 8, 255, 2, "undefined", /* ... */];
// 模拟 gt 函数(简化版)
function gt(input) {
const customChars = 'W5/.]$"2[4KmaZHDFf91z8+,|B%r;l~iq&xbEn?cMIRNwQC:<LO}vS@A6_{70^jkXts*hJe(>U3p=Ydg`yGPTV#)!ou';
let output = [];
let buffer = 0;
let shift = 0;
for (let i = 0; i < input.length; i++) {
const index = customChars.indexOf(input[i]);
if (index !== -1) {
buffer |= index << shift;
shift += 6; // 假设 6-bit 编码(类似 Base64)
if (shift >= 8) {
output.push(buffer & 0xFF);
buffer >>= 8;
shift -= 8;
}
}
}
if (shift > 0) {
output.push(buffer & 0xFF);
}
return Buffer.from(output).toString('hex'); // 假设 At 返回 Hex
}
// 模拟 Qt 函数
const L = {};
const ot = { 259: "MD5", 260: "toString" };
function Qt(key) {
if (typeof L[key] === u[5]) {
L[key] = gt(ot[key]); // 假设 ot[key] 是方法名
}
return L[key];
}
// 模拟 A()(CryptoJS)
function A() {
return {
[Qt(259)]: (data) => ({
[Qt(260)]: () => crypto.createHash('md5').update(data).digest('hex')
})
};
}
// 示例运行
let Ae = '1025204107460280321';
let _e = 3352;//_e经简单分析发现是时间戳的后四位
Ae = A()[Qt(259)](Ae + _e)[Qt(260)]();
console.log(Ae); // 输出 MD5(Ae + _e) 的 Hex
把两个参数放进去,运行得到结果,发现一致

x的生成
最后对一些细节修改后,得到了x的生成代码,x的生成实际上就是关于时间戳的一些逻辑

再测试一下,发现没问题

贴一下x参数的最终代码
const userId = '102520xxxxxxxxxx321';
const X = Date.now(); // 当前时间戳(或传入的 X)
var part_1 = '1' + userId.slice(-2) + X.toString().slice(-2)
const fe = Math.floor(X / 1000) % 1000 || 1;
var we = [
[3,0,[3, 5, 8, 12]],
[5,1,[3, 5, 8, 12]],
[8,2,[3, 5, 8, 12]],
[12,3,[3, 5, 8, 12]],
[7,0,[7, 11, 2]],
[11,1,[7, 11, 2]],
[2,2,[7, 11, 2]],
[19,0,[19, 23, 17]],
[23,1,[19, 23, 17]],
[17,2,[19, 23, 17]]
]
let se = Math.floor(X / 1000) % 1000 || 1;
const u = [0, 1, 8, 255, "length", "undefined", "a", 2, void 0, "d", 117, 63, 6, "fromCodePoint", 7, 12, "push", 169, 91, 8191, 88, 13, 14, "b", 39, 51, 166, "i", 5, 161, 173, 209, 3, 127, 76, 9, "g", 205, "f", 113, 4, 128, 246, 57, "l", "P", !1, "wx", 223, "tt", 235, 239, 243, 11, 17, 19, 23, 1e3, 224, 152, "h", 50, 40, 62, 65, 46, 249, 250, 251, 240, 234, 214, "e", "-", 244];
const pn = (a, b) => a ** b;
var me;
// 模拟循环
for (let i = 0; i < we.length; i++) {
if (i<4){
me = 2;
}else if (i<7){
me = 3;
}else{
me = 5;
}
part_1 = part_1 ^ (we[i][u[0]] * me);
se += pn(we[i][0] % 10, 2) * fe % 1000003;
}
const part_2 = se;
const part_3 = X;
const crypto = require('crypto');
// 模拟 u 数组(假设部分值)
// 模拟 gt 函数(简化版)
function gt(input) {
const customChars = 'W5/.]$"2[4KmaZHDFf91z8+,|B%r;l~iq&xbEn?cMIRNwQC:<LO}vS@A6_{70^jkXts*hJe(>U3p=Ydg`yGPTV#)!ou';
let output = [];
let buffer = 0;
let shift = 0;
for (let i = 0; i < input.length; i++) {
const index = customChars.indexOf(input[i]);
if (index !== -1) {
buffer |= index << shift;
shift += 6; // 假设 6-bit 编码(类似 Base64)
if (shift >= 8) {
output.push(buffer & 0xFF);
buffer >>= 8;
shift -= 8;
}
}
}
if (shift > 0) {
output.push(buffer & 0xFF);
}
return Buffer.from(output).toString('hex'); // 假设 At 返回 Hex
}
// 模拟 Qt 函数
const L = {};
const ot = { 259: "MD5", 260: "toString" };
function Qt(key) {
if (typeof L[key] === u[5]) {
L[key] = gt(ot[key]); // 假设 ot[key] 是方法名
}
return L[key];
}
// 模拟 A()(CryptoJS)
function A() {
return {
[Qt(259)]: (data) => ({
[Qt(260)]: () => crypto.createHash('md5').update(data).digest('hex')
})
};
}
// 示例运行
let Ae = userId;
let _e = X.toString().slice(-4);//_e经简单分析发现是时间戳的后四位
Ae = A()[Qt(259)](Ae + _e)[Qt(260)]();
const part_4 = Ae;
var part_final = [part_1, part_2, part_3, part_4].join('-');
console.log(part_final);
function doubleHash(data) {
// 1. 计算 SHA256 哈希(返回 Buffer)
const sha256Hash = crypto.createHash('sha256').update(data).digest('hex');
// 2. 计算 MD5 哈希(基于 SHA256 的结果)
const md5Hash = crypto.createHash('md5').update(sha256Hash).digest('hex');
return md5Hash; // 返回 32 位 Hex 字符串
}
// 示例用法
const Mt = part_final; // 你的原始数据
const result = doubleHash(Mt);
console.log(result);
data参数
搜索data:,定位到这里,随便下几个断点,运行一下,这里看到encryptKey,iv,再看一下密文的格式,那么就去猜一下是不是AES

结果发现是的,加密模式是AES/CBC/PKCS7Padding

先贴出
AES加密的代码
//const crypto = require('crypto');
function encryptData(data, utf8Key, utf8Iv) {
// 1. 将 UTF-8 字符串的 key 和 iv 转换为 Buffer
const key = Buffer.from(utf8Key, 'utf8'); // 必须 16/24/32 字节
const iv = Buffer.from(utf8Iv, 'utf8'); // 必须 16 字节
// 3. 加密(AES-256-CBC + PKCS7Padding)
const cipher = crypto.createCipheriv('aes-192-cbc', key, iv);
let encrypted = cipher.update(data, 'utf8', 'base64');
encrypted += cipher.final('base64');
return encrypted;
}
// 示例
const data = '{"activityId":"1112082026842644480","appid":"wxafec6f8422cb357b","timestamp":"1745509279072","signature":"DB43BEB837BBA50AB358BAA92AD34FF9","x":"534495690cd09938a581047b307f3686","v":2}';
const utf8Key = '2FbTjVHnrwYx0K5Nvo5wAQ=='; // UTF-8 字符串
const utf8Iv = '329341a65f1fb0d6'; // UTF-8 字符串
try {
const encrypted = encryptData(data, utf8Key, utf8Iv);
console.log('加密结果(Base64):', encrypted);
} catch (err) {
console.error('加密失败:', err.message);
}
但是发现每次请求的key和iv不一样

通过单步调试,走到了这里,重点在
getUserCryptoManager().getLatestUserKey()这里,这决代码返回了key和iv
查找资料发现这似乎是微信的接口

为了避免小程序与开发者后台通信时数据被截取和篡改,微信平台维护了一个用户维度的可靠 Key,用于小程序和后台通信时进行加密和签名。
开发者可以分别通过小程序前端 API 和微信后台提供的服务端接口,获取到用户加密 key。
微信平台只提供可靠的加密 key,开发者需自行实现加密方式,对向服务端接口请求的数据进行端处加解密。
在小程序中开发者可以使用UserCryptoManager.getLatestUserKey获取获取用户最新的加密密钥信息。
这里看到每次获取的key和iv只有一个小时的有效期,也就是说无法用固定的key和iv来加密。