本帖最后由 xinjun_ying 于 2023-8-15 14:50 编辑
一直在搞其他事情,好久没搞js加密分析了,正好遇到一个很简单的数据要搞。想着也很久没在论坛发帖了,这次就发下。【某点数据】的榜单数据抓取。
[Java] 纯文本查看 复制代码 列表页接口:【https://xxx/pc/app/v1/rank?market_id=11&genre_id=4&country_id=24&device_id=0&page=1&time=1691424000&rank_type=1&brand_id=1&k=AVUJVF8GAh5QBQVWDFVeBwEcXRwXQhFUXgQGHlkBAVcOTUgRGkwNHBdCEUoYBhxaCFpa】
打开列表页链接清空下请求,点击下其他的分类,一下子就看到数据返回的接口了。除了k是根据动态计算的,其他都是按照分类或者时间戳写死的。
[Asm] 纯文本查看 复制代码 老规矩先全局搜下接口路径,发现没有,好家伙,藏的那么深,从接口执行堆栈流程看到有个getAppRank 过于明显果断从这里开始debug。
[Asm] 纯文本查看 复制代码 debug过程中发现接口路径是从【/web/api/rank】替换到【/app/v1/rank】
[Asm] 纯文本查看 复制代码 最后我们顺着debug看下k的生成规则是啥,发现k是由请求参数+basse64生成这就好弄了,分析下这块逻辑。
简单带你直接把这块js扣下看看缺啥补啥。具体看代码上的注释,已经把原代码和修改的备注了。
[Asm] 纯文本查看 复制代码 20230810 【'1d8en73(i4'】 本以为是固定参数,后发现是每天虽则签名更改,一天都不会变动,等有空再更新上来
[JavaScript] 纯文本查看 复制代码 function getToken(e, path, n, r) {
function u8ArrZBase64(u8Arr) {
try{
let CHUNK_SIZE = 0x8000;
let index = 0;
let length = u8Arr.length;
let result = '';
let slice;
while (index < length) {
slice = u8Arr.subarray(index, Math.min(index + CHUNK_SIZE, length));
result += String.fromCharCode.apply(null, slice);
index += CHUNK_SIZE;
}
return btoa(result);
}
catch(e) {
throw e;
}
}
var s = n.s
, d = n.k
, f = n.l
, v = n.d
, l = n.sort
, k = n.num
, y = function(content, t, e) {
for (var a = Array.from(content), n = Array.from(t), r = a.length, o = n.length, d = String.fromCodePoint, i = 0; i < r; i++)
a[i] = d(a[i].codePointAt(0) ^ n[(i + e) % o].codePointAt(0));
return a.join("")
}(function(s, t, path, e) {
return [s, t, e, path].join("(&&)")
}(function(t, e) {
// 原代码var n = c()(t); 缺c()(t) debug后发现只是简单校验t,我们可以直接把t复制给n
var n = t;
// 原代码
//if (!_()(n)) {
// var r = [];
// for (var d in n)
// m()(n[d]) && "get" === e && (n[d] = n[d].join("")),
// "post" === e && (m()(n[d]) || o()(n[d])) && (n[d] = JSON.stringify(n[d])),
// r.push(n[d]);
// return r.sort(),
// r.join("")
//}
// debug并删除无用代码,就是简单拼接
var r = [];
for (var d in n){
r.push(n[d])
}
return r.sort(),r.join("");
// Object(h.b)(s, d, f) 方法主要针对入参n中s,k的Uint8Array,并转成字符串,进去发现只是可以是写死的字符串1d8en73(i4
}(e, r), parseInt((new Date).getTime() / 1e3) - 655876800 - v, path, l), '1d8en73(i4', k);
// 最后就是u8Arr转Base64
return u8ArrZBase64(new TextEncoder().encode(y));
}
var path = '/v1/rank';
var eData = {
"market_id": 11,
"genre_id": 6,
"country_id": 24,
"device_id": 0,
"page": 1,
"time": 1690732800,
"rank_type": 1,
"brand_id": 0
}
// s,k,l 均会过期,定时从列表页解析即可,https://xxx/rank/googleplay/11-1-14-24-0?time=1690387200000
var nData = {
"s": "7f260473b50b9a6beb9a384d3adc2027",
"k": "d8462b23fc155996",
"l": "4e226e4686f23bc3",
"d": 0,
"sort": "dd",
"num": 10
}
var rData = "get";
getToken(eData, path,nData, rData)
[Asm] 纯文本查看 复制代码 前几天没来得及更新,次日发现某点还有个需要签名会每天变动,因此以下补充签名生成规则。对应上述代码中 【Object(h.b)(s, d, f)】,开始debug进去查看,着急搬砖直接上解析后的加密规则,在代码里都注释了
[JavaScript] 纯文本查看 复制代码 function readUInt32BE(tt, t) {
return 16777216 * tt[t] + (tt[t + 1] << 16 | tt[t + 2] << 8 | tt[t + 3])
}
function oFunction(t) {
for (var e = t.length / 4 | 0, r = new Array(e), i = 0; i < e; i++)
r[i] = readUInt32BE(t, 4 * i);
return r
}
// 后续至关重用
var f = [0, 1, 2, 4, 8, 16, 32, 64, 128, 27, 54]
, d = function() {
for (var t = new Array(256), e = 0; e < 256; e++)
t[e] = e < 128 ? e << 1 : e << 1 ^ 283;
for (var r = [], n = [], o = [[], [], [], []], h = [[], [], [], []], l = 0, f = 0, i = 0; i < 256; ++i) {
var d = f ^ f << 1 ^ f << 2 ^ f << 3 ^ f << 4;
d = d >>> 8 ^ 255 & d ^ 99,
r[l] = d,
n[d] = l;
var c = t[l]
, m = t[c]
, v = t[m]
, y = 257 * t[d] ^ 16843008 * d;
o[0][l] = y << 24 | y >>> 8,
o[1][l] = y << 16 | y >>> 16,
o[2][l] = y << 8 | y >>> 24,
o[3][l] = y,
y = 16843009 * v ^ 65537 * m ^ 257 * c ^ 16843008 * l,
h[0][d] = y << 24 | y >>> 8,
h[1][d] = y << 16 | y >>> 16,
h[2][d] = y << 8 | y >>> 24,
h[3][d] = y,
0 === l ? l = f = 1 : (l = c ^ t[t[t[v ^ c]]],
f ^= t[t[f]])
}
return {
SBOX: r,
INV_SBOX: n,
SUB_MIX: o,
INV_SUB_MIX: h
}
}();
function _reset(tt) {
for (var t = tt, e = t.length, r = e + 6, n = 4 * (r + 1), o = [], h = 0; h < e; h++)
o[h] = t[h];
for (h = e; h < n; h++) {
var l = o[h - 1];
h % e == 0 ? (l = l << 8 | l >>> 24,
l = d.SBOX[l >>> 24] << 24 | d.SBOX[l >>> 16 & 255] << 16 | d.SBOX[l >>> 8 & 255] << 8 | d.SBOX[255 & l],
l ^= f[h / e | 0] << 24) : e > 6 && h % e == 4 && (l = d.SBOX[l >>> 24] << 24 | d.SBOX[l >>> 16 & 255] << 16 | d.SBOX[l >>> 8 & 255] << 8 | d.SBOX[255 & l]),
o[h] = o[h - e] ^ l
}
for (var c = [], m = 0; m < n; m++) {
var v = n - m
, y = o[v - (m % 4 ? 0 : 4)];
c[m] = m < 4 || v <= 4 ? y : d.INV_SUB_MIX[0][d.SBOX[y >>> 24]] ^ d.INV_SUB_MIX[1][d.SBOX[y >>> 16 & 255]] ^ d.INV_SUB_MIX[2][d.SBOX[y >>> 8 & 255]] ^ d.INV_SUB_MIX[3][d.SBOX[255 & y]]
}
return {
'_nRounds' : r,
'_keySchedule' : o,
'_invKeySchedule' : c
}
}
function DAES(t) {
var _key = oFunction(t);
var rs = _reset(_key);
return {
'_key' : _key,
'_nRounds' : rs._nRounds,
'_keySchedule' : rs._keySchedule,
'_invKeySchedule' : rs._invKeySchedule
}
}
function dFrom(sData,sP) {
k(sData,sP,0,16);
return sData;
}
function k(t, e, r, n) {
r = Number(r) || 0;
var f = t.length - r;
n ? (n = Number(n)) > f && (n = f) : n = f;
var o = e.length;
if (o % 2 != 0)
throw new TypeError("Invalid hex string");
n > o / 2 && (n = o / 2);
for (var i = 0; i < n; ++i) {
var c = parseInt(e.substr(2 * i, 2), 16);
if (isNaN(c))
return i;
t[r + i] = c
}
return i
}
function l(t, e, r, n, o) {
for (var h, l, f, d, c = r[0], m = r[1], v = r[2], y = r[3], w = t[0] ^ e[0], _ = t[1] ^ e[1], M = t[2] ^ e[2], S = t[3] ^ e[3], E = 4, k = 1; k < o; k++)
h = c[w >>> 24] ^ m[_ >>> 16 & 255] ^ v[M >>> 8 & 255] ^ y[255 & S] ^ e[E++],
l = c[_ >>> 24] ^ m[M >>> 16 & 255] ^ v[S >>> 8 & 255] ^ y[255 & w] ^ e[E++],
f = c[M >>> 24] ^ m[S >>> 16 & 255] ^ v[w >>> 8 & 255] ^ y[255 & _] ^ e[E++],
d = c[S >>> 24] ^ m[w >>> 16 & 255] ^ v[_ >>> 8 & 255] ^ y[255 & M] ^ e[E++],
w = h,
_ = l,
M = f,
S = d;
return h = (n[w >>> 24] << 24 | n[_ >>> 16 & 255] << 16 | n[M >>> 8 & 255] << 8 | n[255 & S]) ^ e[E++],
l = (n[_ >>> 24] << 24 | n[M >>> 16 & 255] << 16 | n[S >>> 8 & 255] << 8 | n[255 & w]) ^ e[E++],
f = (n[M >>> 24] << 24 | n[S >>> 16 & 255] << 16 | n[w >>> 8 & 255] << 8 | n[255 & _]) ^ e[E++],
d = (n[S >>> 24] << 24 | n[w >>> 16 & 255] << 16 | n[_ >>> 8 & 255] << 8 | n[255 & M]) ^ e[E++],
[h >>>= 0, l >>>= 0, f >>>= 0, d >>>= 0]
}
function N(t, e, r, n) {
e < 0 && (e = 4294967295 + e + 1);
for (var i = 0, f = Math.min(t.length - r, 4); i < f; ++i)
t[r + i] = e >>> 8 * (n ? i : 3 - i) & 255
}
function writeUInt32BE(tData, t, e,r) {
return t = +t,
e |= 0,
true ? (tData[e] = t >>> 24,
tData[e + 1] = t >>> 16,
tData[e + 2] = t >>> 8,
tData[e + 3] = 255 & t) : N(tData, t, e, !1),
e + 4
}
function decryptBlock(t,kData){
var e = (t = oFunction(t))[1];
t[1] = t[3],
t[3] = e;
var thisData = DAES(new TextEncoder().encode(kData));
var tData = new Uint8Array(16);
var r = l(t, thisData._invKeySchedule, d.INV_SUB_MIX, d.INV_SBOX, thisData._nRounds)
, h = new Uint8Array(16);
// console.log('e:'+e);
// console.log('h:'+h);
// console.log('r:'+r);
// console.log('t:'+t);
writeUInt32BE(tData, r[0], 0),
writeUInt32BE(tData, r[3], 4),
writeUInt32BE(tData, r[2], 8),
writeUInt32BE(tData, r[1], 12),
h;
return tData;
}
function exports(a, b) {
for (var t = Math.min(a.length, b.length), r = new Uint8Array(16), i = 0; i < t; ++i)
r[i] = a[i] ^ b[i];
return r
}
function slice(tData, t, e) {
var r, n = tData.length;
if ((t = ~~t) < 0 ? (t += n) < 0 && (t = 0) : t > n && (t = n),
(e = void 0 === e ? n : ~~e) < 0 ? (e += n) < 0 && (e = 0) : e > n && (e = n),
e < t && (e = t),true)
(r = tData.subarray(t, e)).__proto__ = d.prototype;
return r
}
function ttt(t) {
var e = t[15];
if (e < 1 || e > 16)
throw new Error("unable to decrypt data");
var i = -1;
for (; ++i < e; )
if (t[i + (16 - e)] !== e)
throw new Error("unable to decrypt data");
if (16 === e)
return;
return slice(t, 0, 16 - e)
}
function getSign(sData,kData,lData){
// 按s字符串生成 对应 16位数组
var dFromResult = dFrom(new Uint8Array(16),sData);
console.log(dFromResult);
// 将k转16为数组通过 DAES方法 生成_key、_nRounds、_keySchedule、_invKeySchedule,等签名所需参数,并返回执行结果
var decryptBlockResult= decryptBlock(dFromResult,kData);
console.log(decryptBlockResult);
var exportsResult = exports(decryptBlockResult,new TextEncoder().encode(lData));
console.log(exportsResult);
// 按数组切片重组并转成字符串得到签名
return new TextDecoder("utf-8").decode(ttt(exportsResult));
}
var params = process.argv.splice(2);
process.stdout.write(getSign(params[0],params[1],params[2]));
[Asm] 纯文本查看 复制代码 最后先调用getSign 再根据参数调用getToken即可
|