吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1047|回复: 30
上一主题 下一主题
收起左侧

[Web逆向] 某q音乐jsvmp浅析

  [复制链接]
跳转到指定楼层
楼主
中二 发表于 2026-8-19 23:50 回帖奖励

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。

前言

网址:aHR0cHM6Ly95LnFxLmNvbS8=

前段时间一直在研究风控问题,发现某些站检测点真的是三天一小改,五天一大改,算法几乎都是不变的,每次更新加环境or更环境都很头大,所以也是开始学习一手jsvmp插桩,虽然说现在都是ai梭哈,大力出奇迹,但是秉着知其然知其所以然还是得打牢基础。记录一下做个总结,方便后续复习。

正文

AST插桩

在源码里面搜索,总共搜索到12个call,需要在解释器内部关键位置CALL指令处理分支植入日志探针,一开始是直接学的手动插桩,后面看了几集教程视频,看了几篇文章,还是决定使用ast来写吧,确实省时省力啊!

分析代码,其实很明了,直接抓普通赋值语句右边调用了 .call() 方法的所有节点

也是匹配到了所有的call函数调用节点

const fs = require('fs');
const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const code = fs.readFileSync('./main.js', 'utf8');
const ast = parse(code, { sourceType: 'module' });
let count = 0;
traverse(ast, {
  AssignmentExpression(path) {
    const node = path.node;
    if (
      node.operator === '=' &&
      node.right.type === 'CallExpression' &&
      node.right.callee.type === 'MemberExpression' &&
      node.right.callee.property.type === 'Identifier' &&
      node.right.callee.property.name === 'call'
    ) {
      count++;
      console.log(path.toString());
    }
  }
});
console.log('\n匹配总数:', count);

提取三要素(AST 节点,不是字符串)

const call = node.right;
const calleeObject = call.callee.object; // Y 也就是被调用函数 fn
const originalArgs = call.arguments;     // .call( thisArg, arg1, arg2 )
const thisArg = originalArgs[0];         // call第一个参数就是this
const argsArray = t.arrayExpression(originalArgs.slice(1).map(clone)); // 后面全部参数打包数组

有些大佬的思路就是直接console.log(func、this、args、result),但是我为了方便后续日志导出本地分析,我就是使用了固定函数模板,下次也可以继续复用的,遇到更加复杂点的还是得优化!

call_hook函数:

// Call Hook
function hookCall(func, thisArg, args) {
  const result = func.call(thisArg, ...args);
  log("Call", {
    fn: func.name || "anonymous",
    thisArg: thisArg,
    args: args,
    result: result
  });
  return result;
}

call_op函数:

// Op Hook  
function hookOp(op, left, right) {
  var result;
  if (right === null) {
    result = new Function("a", "return " + op + "(a)")(left);
  } else {
    result = new Function("a", "b", "return a " + op + " b")(left, right);
  }
  log("Op", op + " | " + left + " | " + right + " | " + result);
  return result;
}

运算符我也是用的比较粗暴的,可能也是没有系统化学过ast的原因吧,东学点西学点的原因哈哈哈,直接就是只保留赋值表达式并且该赋值二元表达式的 left、right同时都是MemberExpression。

搜索发现没有&&/ ||,取到所有的节点之后替换成使用模版函数

// Op 插桩: h[n[++p]] = h[n[++p]] >= h[n[++p]]
if (isBinaryOpAssignment(node)) {
  const bin = node.right;
  node.right = t.callExpression(t.identifier("hookOp"), [
    t.stringLiteral(bin.operator),
    bin.left,
    bin.right
  ]);
  opCount++;
}

剩余模版代码

// 日志收集
window.__LOGS__ = [];
function log(type, data) {
  let strData;
  try {
    strData = typeof data === "object" ? JSON.stringify(data, null, 2) : String(data);
  } catch (e) {
    strData = "stringify_err:" + e.message + " raw:" + data;
  }
  var entry = "[" + type + "] " + strData;
  console.log(entry);
  window.__LOGS__.push(entry);
}
//hookCall
//hookOp
// 导出日志
function saveLogs() {
  console.log(JSON.stringify(window.__LOGS__, null, 2));
}

ast 总共就是这么写,写的有点low了 ,一份固定模板读取添加到ast之后的上面,对函数调用指令跟算术运算指令做了个插桩,后续练习其他项目的时候再慢慢优化模版吧

调试总结

这次主要分析的就是这个sign,入口也很简单,直接全局搜sign就行了

这次分析主要拿的还是搜索接口,需要删掉关键词重新搜索可能才会触发,sign就是u,那么u在哪里,可以看到上面代码n.next = 11,所以可以判断u是由ie(r.data)生成之后跳转到11分支给sign使用的。

ie函数就是c函数,点击c函数跳转就是jsvmp所在的地方了

开无痕跑一下jsvmp跟网页对比产出有些不一致,一开始我还以为是更新变了,其实不然,jsvmp估计就是有一些检测点,比如检测网址或者就是cookie等缓存信息影响,后面看了日志才能发现其检测点!这时候就对补环境很有用了,一直补都跟网页上不一样,并不一定是你补的不对哈,可能就是某些东西被检测了,你有这个值,但是这个值跟他想要的不一样就过不去,只要插桩看一下检测点就能针对性去补检测点环境了。

日志分析

这里使用的明文值为12345

总共导出了3000多行的日志,初步就可以分析zzca873dd41zwq69wr8hrqun6rvk1b5srwqncdc4c4d03是由 固定zzc+A873DD4+1ZWQ69wr8HRqUN6Rvk1b5srwQNc+DC4C4D03拼接而成的。

直接全局搜索A873DD4 ,A873DD4是跟: [ 23,14, 6,36,16,40,7,19]有关,此刻是没过检测点的日志

[Op] + | 23 | 1 | 24
[Op] + | 14 | 1 | 15
[Op] + | 6 | 1 | 7
[Op] + | 36 | 1 | 37
[Op] + | 16 | 1 | 17
[Op] + | 40 | 1 | 41
[Op] + | 7 | 1 | 8
[Op] + | 19 | 1 | 20
[Call] {
  "fn": "map",
  "thisArg": [
    23,
    14,
    6,
    36,
    16,
    40,
    7,
    19
  ],
  "args": [
    null
  ],
  "result": [
    "C",
    "8",
    "D",
    "9",
    "B",
    null,
    "0",
    "6"
  ]
}
[Call] {
  "fn": "join",
  "thisArg": [
    "C",
    "8",
    "D",
    "9",
    "B",
    null,
    "0",
    "6"
  ],
  "args": [
    ""
  ],
  "result": "C8D9B06"
} [
    ""
  ],
  "result": "A873DD4"
}

看了很多大佬的文章,说是直接前面生成的hash值取固定下标,我一直取的是错误的,后面才想起来可能是更新了,其实不然,于是仔细看了下日志,发现有不少检测点,先把相关检测点分析一下,再分析日志吧

检测点分析

正常检测点日志:

[Op] === | object | object | true
[Op] === | object | object | true
[Op] === | object | object | true
[Call] {
  "fn": "RegExp",
  "args": [
    "Headless",
    "i"
  ],
  "result": {}
}
[Call] {
  "fn": "test",
  "thisArg": {},
  "args": [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0"
  ],
  "result": false
}
[Op] === | undefined | 1 | false
[Call] {
  "fn": "indexOf",
  "thisArg": "y.qq.com",
  "args": [
    "qq.com"
  ],
  "result": 2
}
[Op] > | 2 | -1 | true
[Call] {
  "fn": "some",
  "thisArg": [
    "qq.com",
    "joox.com",
    "tencentmusic.com",
    "wavecommittee.com",
    "kugou.com",
    "kuwo.cn"
  ],
  "args": [
    null
  ],
  "result": true
}

非正常检测点日志:

[Op] === | object | object | true
[Op] === | object | object | true
[Op] === | object | object | true
[Call] {
  "fn": "RegExp",
  "args": [
    "Headless",
    "i"
  ],
  "result": {}
}
[Call] {
  "fn": "test",
  "thisArg": {},
  "args": [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0"
  ],
  "result": false
}
[Op] === | undefined | 1 | false
[Call] {
  "fn": "indexOf",
  "thisArg": "ntp.msn.cn",
  "args": [
    "qq.com"
  ],
  "result": -1
}
[Op] > | -1 | -1 | false
[Call] {
  "fn": "indexOf",
  "thisArg": "ntp.msn.cn",
  "args": [
    "joox.com"
  ],
  "result": -1
}
[Op] > | -1 | -1 | false
[Call] {
  "fn": "indexOf",
  "thisArg": "ntp.msn.cn",
  "args": [
    "tencentmusic.com"
  ],
  "result": -1
}
[Op] > | -1 | -1 | false
[Call] {
  "fn": "indexOf",
  "thisArg": "ntp.msn.cn",
  "args": [
    "wavecommittee.com"
  ],
  "result": -1
}
[Op] > | -1 | -1 | false
[Call] {
  "fn": "indexOf",
  "thisArg": "ntp.msn.cn",
  "args": [
    "kugou.com"
  ],
  "result": -1
}
[Op] > | -1 | -1 | false
[Call] {
  "fn": "indexOf",
  "thisArg": "ntp.msn.cn",
  "args": [
    "kuwo.cn"
  ],
  "result": -1
}
[Op] > | -1 | -1 | false
[Call] {
  "fn": "some",
  "thisArg": [
    "qq.com",
    "joox.com",
    "tencentmusic.com",
    "wavecommittee.com",
    "kugou.com",
    "kuwo.cn"
  ],
  "args": [
    null
  ],
  "result": false
}

日志检测对比:

某某对象检测,我也不知道具体检测哪些对象,估计得改针对性插桩才能知道吧

[Op] === | object | object | true

创建了 Headless 检测正则。两个 User-Agent 都没有包含 Headless,所以结果一致,不影响后续结果

RegExp("Headless", "i")

估计猜测是自动化检测,这个并不受影响

[Op] === | undefined | 1 | false

受影响检测点:

被检测日志:

"ntp.msn.cn".indexOf("qq.com") // -1
-1 > -1                     // false

如果没过会继续检测,所有域名都没匹配到,所以是false

"ntp.msn.cn".indexOf("joox.com")          // -1
"ntp.msn.cn".indexOf("tencentmusic.com")  // -1
"ntp.msn.cn".indexOf("wavecommittee.com") // -1
"ntp.msn.cn".indexOf("kugou.com")         // -1
"ntp.msn.cn".indexOf("kuwo.cn")           // -1

正常日志:

"y.qq.com".indexOf("qq.com") // 2
2 > -1                     // true

some() 立即停止,不再检测后面的域名

joox.com
tencentmusic.com
wavecommittee.com
kugou.com
kuwo.cn

最终检测点就是域名检测

算法分析

对比一下自己遇到的坑吧,检测点到底哪里受了影响呢

zzca873dd41zwq69wr8hrqun6rvk1b5srwqncdc4c4d03是由 固定zzc+A873DD4+1ZWQ69wr8HRqUN6Rvk1b5srwQNc+DC4C4D03拼接而成的。

这边暂且A873DD4就叫车头密文吧,1ZWQ69wr8HRqUN6Rvk1b5srwQNc就叫中间密文,DC4C4D03就叫车尾密文。

检测点受影响前分析:

经过分析,明文到密文这一步还是没受到影响的
直接拿浏览器的搜索明文日志分析吧

日志其实一目了然,"12345" 到 "8cb2237d0679ca88db6464eac60da96345513964" 就是hash算法,不是md5就是sha1

Call] {
  "fn": "split",
  "thisArg": "0123456789abcdef",
  "args": [
    ""
  ],
  "result": [
    "0",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    "a",
    "b",
    "c",
    "d",
    "e",
    "f"
  ]
}
[Call] {
  "fn": "charCodeAt",
  "thisArg": "12345",
  "args": [
    0
  ],
  "result": 49
}
[Call] {
  "fn": "charCodeAt",
  "thisArg": "12345",
  "args": [
    1
  ],
  "result": 50
}
[Call] {
  "fn": "charCodeAt",
  "thisArg": "12345",
  "args": [
    2
  ],
  "result": 51
}
[Call] {
  "fn": "charCodeAt",
  "thisArg": "12345",
  "args": [
    3
  ],
  "result": 52
}
[Call] {
  "fn": "charCodeAt",
  "thisArg": "12345",
  "args": [
    4
  ],
  "result": 53
}
[Call] {
  "fn": "c",
  "thisArg": {
    "blocks": [
      825373492,
      889192448,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0
    ],
    "h0": 1732584193,
    "h1": 4023233417,
    "h2": 2562383102,
    "h3": 271733878,
    "h4": 3285377520,
    "hBytes": 0,
    "bytes": 5,
    "start": 5,
    "block": 889192448,
    "hashed": false,
    "finalized": false,
    "first": true,
    "lastByteIndex": 5
  },
  "args": [
    "12345"
  ],
  "result": {
    "blocks": [
      825373492,
      889192448,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0
    ],
    "h0": 1732584193,
    "h1": 4023233417,
    "h2": 2562383102,
    "h3": 271733878,
    "h4": 3285377520,
    "hBytes": 0,
    "bytes": 5,
    "start": 5,
    "block": 889192448,
    "hashed": false,
    "finalized": false,
    "first": true,
    "lastByteIndex": 5
  }
}
[Call] {
  "fn": "c",
  "args": [
    "12345"
  ],
  "result": "8cb2237d0679ca88db6464eac60da96345513964"
}

没有魔改,直接套用就行了

const crypto = require('crypto');
function sha1(str) {
    return crypto.createHash('sha1').update(str, 'utf8').digest('hex');
}
const res = sha1('明文');
console.log(res);

检测点之后日志分析:
车头密文分析:

正常日志:

经过多次调试,[23, 14, 6, 36, 16, 40, 7,19]下标数组是固定的,正常日志就如各大文章视频所说直接取下标就行了。

[Call] {
  "fn": "map",
  "thisArg": [
    23,
    14,
    6,
    36,
    16,
    40,
    7,
    19
  ],
  "args": [
    null
  ],
  "result": [
    "A",
    "8",
    "7",
    "3",
    "D",
    null,
    "D",
    "4"
  ]
}
[Call] {
  "fn": "join",
  "thisArg": [
    "A",
    "8",
    "7",
    "3",
    "D",
    null,
    "D",
    "4"
  ],
  "args": [
    ""
  ],
  "result": "A873DD4"
}

跟日志也是能对得上的~~~~

检测点日志分析:

[23, 14, 6, 36, 16, 40, 7,19]是一样的,但是结果却是数组每个+1 去取下标,这样子就导致了提取的值不一致

[Op] + | 23 | 1 | 24
[Op] + | 14 | 1 | 15
[Op] + | 6 | 1 | 7
[Op] + | 36 | 1 | 37
[Op] + | 16 | 1 | 17
[Op] + | 40 | 1 | 41
[Op] + | 7 | 1 | 8
[Op] + | 19 | 1 | 20
[Call] {
  "fn": "map",
  "thisArg": [
    23,
    14,
    6,
    36,
    16,
    40,
    7,
    19
  ],
  "args": [
    null
  ],
  "result": [
    "C",
    "8",
    "D",
    "9",
    "B",
    null,
    "0",
    "6"
  ]
}
[Call] {
  "fn": "join",
  "thisArg": [
    "C",
    "8",
    "D",
    "9",
    "B",
    null,
    "0",
    "6"
  ],
  "args": [
    ""
  ],
  "result": "C8D9B06"
}

跟日志上面的结果也是能对得上的,最起码检测点不过车头密文是会受影响的,每个数组都会加1

车尾密文:

车尾密文就不多说了,跟车头密文是一致的只是换了下标数组,检测点不一致照样会收影响

中间密文:

正常日志:

[Op] < | 0 | 20 | true
[Op] * | 0 | 2 | 0
[Op] * | 8 | 16 | 128
[Op] * | 0 | 2 | 0
[Op] + | 0 | 1 | 1
[Op] + | 128 | 12 | 140
[Op] ^ | 140 | 89 | 213
[Call] {
  "fn": "push",
  "thisArg": [
    213
  ],
  "args": [
    213
  ],
  "result": 1
}
[Op] < | 1 | 20 | true
[Op] * | 1 | 2 | 2
[Op] * | 11 | 16 | 176
[Op] * | 1 | 2 | 2
[Op] + | 2 | 1 | 3
[Op] + | 176 | 2 | 178
[Op] ^ | 178 | 39 | 149
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149
  ],
  "args": [
    149
  ],
  "result": 2
}
[Op] < | 2 | 20 | true
[Op] * | 2 | 2 | 4
[Op] * | 2 | 16 | 32
[Op] * | 2 | 2 | 4
[Op] + | 4 | 1 | 5
[Op] + | 32 | 3 | 35
[Op] ^ | 35 | 179 | 144
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144
  ],
  "args": [
    144
  ],
  "result": 3
}
[Op] < | 3 | 20 | true
[Op] * | 3 | 2 | 6
[Op] * | 7 | 16 | 112
[Op] * | 3 | 2 | 6
[Op] + | 6 | 1 | 7
[Op] + | 112 | 13 | 125
[Op] ^ | 125 | 150 | 235
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235
  ],
  "args": [
    235
  ],
  "result": 4
}
[Op] < | 4 | 20 | true
[Op] * | 4 | 2 | 8
[Op] * | 0 | 16 | 0
[Op] * | 4 | 2 | 8
[Op] + | 8 | 1 | 9
[Op] + | 0 | 6 | 6
[Op] ^ | 6 | 218 | 220
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220
  ],
  "args": [
    220
  ],
  "result": 5
}
[Op] < | 5 | 20 | true
[Op] * | 5 | 2 | 10
[Op] * | 7 | 16 | 112
[Op] * | 5 | 2 | 10
[Op] + | 10 | 1 | 11
[Op] + | 112 | 9 | 121
[Op] ^ | 121 | 82 | 43
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43
  ],
  "args": [
    43
  ],
  "result": 6
}
[Op] < | 6 | 20 | true
[Op] * | 6 | 2 | 12
[Op] * | 12 | 16 | 192
[Op] * | 6 | 2 | 12
[Op] + | 12 | 1 | 13
[Op] + | 192 | 10 | 202
[Op] ^ | 202 | 58 | 240
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240
  ],
  "args": [
    240
  ],
  "result": 7
}
[Op] < | 7 | 20 | true
[Op] * | 7 | 2 | 14
[Op] * | 8 | 16 | 128
[Op] * | 7 | 2 | 14
[Op] + | 14 | 1 | 15
[Op] + | 128 | 8 | 136
[Op] ^ | 136 | 252 | 116
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116
  ],
  "args": [
    116
  ],
  "result": 8
}
[Op] < | 8 | 20 | true
[Op] * | 8 | 2 | 16
[Op] * | 13 | 16 | 208
[Op] * | 8 | 2 | 16
[Op] + | 16 | 1 | 17
[Op] + | 208 | 11 | 219
[Op] ^ | 219 | 177 | 106
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106
  ],
  "args": [
    106
  ],
  "result": 9
}
[Op] < | 9 | 20 | true
[Op] * | 9 | 2 | 18
[Op] * | 6 | 16 | 96
[Op] * | 9 | 2 | 18
[Op] + | 18 | 1 | 19
[Op] + | 96 | 4 | 100
[Op] ^ | 100 | 52 | 80
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80
  ],
  "args": [
    80
  ],
  "result": 10
}
[Op] < | 10 | 20 | true
[Op] * | 10 | 2 | 20
[Op] * | 6 | 16 | 96
[Op] * | 10 | 2 | 20
[Op] + | 20 | 1 | 21
[Op] + | 96 | 4 | 100
[Op] ^ | 100 | 186 | 222
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222
  ],
  "args": [
    222
  ],
  "result": 11
}
[Op] < | 11 | 20 | true
[Op] * | 11 | 2 | 22
[Op] * | 14 | 16 | 224
[Op] * | 11 | 2 | 22
[Op] + | 22 | 1 | 23
[Op] + | 224 | 10 | 234
[Op] ^ | 234 | 123 | 145
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145
  ],
  "args": [
    145
  ],
  "result": 12
}
[Op] < | 12 | 20 | true
[Op] * | 12 | 2 | 24
[Op] * | 12 | 16 | 192
[Op] * | 12 | 2 | 24
[Op] + | 24 | 1 | 25
[Op] + | 192 | 6 | 198
[Op] ^ | 198 | 120 | 190
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190
  ],
  "args": [
    190
  ],
  "result": 13
}
[Op] < | 13 | 20 | true
[Op] * | 13 | 2 | 26
[Op] * | 0 | 16 | 0
[Op] * | 13 | 2 | 26
[Op] + | 26 | 1 | 27
[Op] + | 0 | 13 | 13
[Op] ^ | 13 | 64 | 77
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190,
    77
  ],
  "args": [
    77
  ],
  "result": 14
}
[Op] < | 14 | 20 | true
[Op] * | 14 | 2 | 28
[Op] * | 10 | 16 | 160
[Op] * | 14 | 2 | 28
[Op] + | 28 | 1 | 29
[Op] + | 160 | 9 | 169
[Op] ^ | 169 | 242 | 91
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190,
    77,
    91
  ],
  "args": [
    91
  ],
  "result": 15
}
[Op] < | 15 | 20 | true
[Op] * | 15 | 2 | 30
[Op] * | 6 | 16 | 96
[Op] * | 15 | 2 | 30
[Op] + | 30 | 1 | 31
[Op] + | 96 | 3 | 99
[Op] ^ | 99 | 133 | 230
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190,
    77,
    91,
    230
  ],
  "args": [
    230
  ],
  "result": 16
}
[Op] < | 16 | 20 | true
[Op] * | 16 | 2 | 32
[Op] * | 4 | 16 | 64
[Op] * | 16 | 2 | 32
[Op] + | 32 | 1 | 33
[Op] + | 64 | 5 | 69
[Op] ^ | 69 | 143 | 202
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190,
    77,
    91,
    230,
    202
  ],
  "args": [
    202
  ],
  "result": 17
}
[Op] < | 17 | 20 | true
[Op] * | 17 | 2 | 34
[Op] * | 5 | 16 | 80
[Op] * | 17 | 2 | 34
[Op] + | 34 | 1 | 35
[Op] + | 80 | 1 | 81
[Op] ^ | 81 | 161 | 240
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190,
    77,
    91,
    230,
    202,
    240
  ],
  "args": [
    240
  ],
  "result": 18
}
[Op] < | 18 | 20 | true
[Op] * | 18 | 2 | 36
[Op] * | 3 | 16 | 48
[Op] * | 18 | 2 | 36
[Op] + | 36 | 1 | 37
[Op] + | 48 | 9 | 57
[Op] ^ | 57 | 121 | 64
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190,
    77,
    91,
    230,
    202,
    240,
    64
  ],
  "args": [
    64
  ],
  "result": 19
}
[Op] < | 19 | 20 | true
[Op] * | 19 | 2 | 38
[Op] * | 6 | 16 | 96
[Op] * | 19 | 2 | 38
[Op] + | 38 | 1 | 39
[Op] + | 96 | 4 | 100
[Op] ^ | 100 | 179 | 215
[Call] {
  "fn": "push",
  "thisArg": [
    213,
    149,
    144,
    235,
    220,
    43,
    240,
    116,
    106,
    80,
    222,
    145,
    190,
    77,
    91,
    230,
    202,
    240,
    64,
    215
  ],
  "args": [
    215
  ],
  "result": 20
}

被风控日志:

[Op] < | 0 | 20 | true
[Op] * | 0 | 2 | 0
[Op] * | 8 | 16 | 128
[Op] * | 0 | 2 | 0
[Op] + | 0 | 1 | 1
[Op] + | 128 | 12 | 140
[Op] ^ | 140 | 149 | 25
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25
  ],
  "args": [
    25
  ],
  "result": 1
}
[Op] < | 1 | 20 | true
[Op] * | 1 | 2 | 2
[Op] * | 11 | 16 | 176
[Op] * | 1 | 2 | 2
[Op] + | 2 | 1 | 3
[Op] + | 176 | 2 | 178
[Op] ^ | 178 | 114 | 192
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192
  ],
  "args": [
    192
  ],
  "result": 2
}
[Op] < | 2 | 20 | true
[Op] * | 2 | 2 | 4
[Op] * | 2 | 16 | 32
[Op] * | 2 | 2 | 4
[Op] + | 4 | 1 | 5
[Op] + | 32 | 3 | 35
[Op] ^ | 35 | 150 | 181
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181
  ],
  "args": [
    181
  ],
  "result": 3
}
[Op] < | 3 | 20 | true
[Op] * | 3 | 2 | 6
[Op] * | 7 | 16 | 112
[Op] * | 3 | 2 | 6
[Op] + | 6 | 1 | 7
[Op] + | 112 | 13 | 125
[Op] ^ | 125 | 179 | 206
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206
  ],
  "args": [
    206
  ],
  "result": 4
}
[Op] < | 4 | 20 | true
[Op] * | 4 | 2 | 8
[Op] * | 0 | 16 | 0
[Op] * | 4 | 2 | 8
[Op] + | 8 | 1 | 9
[Op] + | 0 | 6 | 6
[Op] ^ | 6 | 58 | 60
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60
  ],
  "args": [
    60
  ],
  "result": 5
}
[Op] < | 5 | 20 | true
[Op] * | 5 | 2 | 10
[Op] * | 7 | 16 | 112
[Op] * | 5 | 2 | 10
[Op] + | 10 | 1 | 11
[Op] + | 112 | 9 | 121
[Op] ^ | 121 | 37 | 92
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92
  ],
  "args": [
    92
  ],
  "result": 6
}
[Op] < | 6 | 20 | true
[Op] * | 6 | 2 | 12
[Op] * | 12 | 16 | 192
[Op] * | 6 | 2 | 12
[Op] + | 12 | 1 | 13
[Op] + | 192 | 10 | 202
[Op] ^ | 202 | 170 | 96
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96
  ],
  "args": [
    96
  ],
  "result": 7
}
[Op] < | 7 | 20 | true
[Op] * | 7 | 2 | 14
[Op] * | 8 | 16 | 128
[Op] * | 7 | 2 | 14
[Op] + | 14 | 1 | 15
[Op] + | 128 | 8 | 136
[Op] ^ | 136 | 255 | 119
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119
  ],
  "args": [
    119
  ],
  "result": 8
}
[Op] < | 8 | 20 | true
[Op] * | 8 | 2 | 16
[Op] * | 13 | 16 | 208
[Op] * | 8 | 2 | 16
[Op] + | 16 | 1 | 17
[Op] + | 208 | 11 | 219
[Op] ^ | 219 | 101 | 190
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190
  ],
  "args": [
    190
  ],
  "result": 9
}
[Op] < | 9 | 20 | true
[Op] * | 9 | 2 | 18
[Op] * | 6 | 16 | 96
[Op] * | 9 | 2 | 18
[Op] + | 18 | 1 | 19
[Op] + | 96 | 4 | 100
[Op] ^ | 100 | 22 | 114
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114
  ],
  "args": [
    114
  ],
  "result": 10
}
[Op] < | 10 | 20 | true
[Op] * | 10 | 2 | 20
[Op] * | 6 | 16 | 96
[Op] * | 10 | 2 | 20
[Op] + | 20 | 1 | 21
[Op] + | 96 | 4 | 100
[Op] ^ | 100 | 171 | 207
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207
  ],
  "args": [
    207
  ],
  "result": 11
}
[Op] < | 11 | 20 | true
[Op] * | 11 | 2 | 22
[Op] * | 14 | 16 | 224
[Op] * | 11 | 2 | 22
[Op] + | 22 | 1 | 23
[Op] + | 224 | 10 | 234
[Op] ^ | 234 | 156 | 118
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118
  ],
  "args": [
    118
  ],
  "result": 12
}
[Op] < | 12 | 20 | true
[Op] * | 12 | 2 | 24
[Op] * | 12 | 16 | 192
[Op] * | 12 | 2 | 24
[Op] + | 24 | 1 | 25
[Op] + | 192 | 6 | 198
[Op] ^ | 198 | 143 | 73
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73
  ],
  "args": [
    73
  ],
  "result": 13
}
[Op] < | 13 | 20 | true
[Op] * | 13 | 2 | 26
[Op] * | 0 | 16 | 0
[Op] * | 13 | 2 | 26
[Op] + | 26 | 1 | 27
[Op] + | 0 | 13 | 13
[Op] ^ | 13 | 9 | 4
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73,
    4
  ],
  "args": [
    4
  ],
  "result": 14
}
[Op] < | 14 | 20 | true
[Op] * | 14 | 2 | 28
[Op] * | 10 | 16 | 160
[Op] * | 14 | 2 | 28
[Op] + | 28 | 1 | 29
[Op] + | 160 | 9 | 169
[Op] ^ | 169 | 186 | 19
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73,
    4,
    19
  ],
  "args": [
    19
  ],
  "result": 15
}
[Op] < | 15 | 20 | true
[Op] * | 15 | 2 | 30
[Op] * | 6 | 16 | 96
[Op] * | 15 | 2 | 30
[Op] + | 30 | 1 | 31
[Op] + | 96 | 3 | 99
[Op] ^ | 99 | 34 | 65
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73,
    4,
    19,
    65
  ],
  "args": [
    65
  ],
  "result": 16
}
[Op] < | 16 | 20 | true
[Op] * | 16 | 2 | 32
[Op] * | 4 | 16 | 64
[Op] * | 16 | 2 | 32
[Op] + | 32 | 1 | 33
[Op] + | 64 | 5 | 69
[Op] ^ | 69 | 95 | 26
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73,
    4,
    19,
    65,
    26
  ],
  "args": [
    26
  ],
  "result": 17
}
[Op] < | 17 | 20 | true
[Op] * | 17 | 2 | 34
[Op] * | 5 | 16 | 80
[Op] * | 17 | 2 | 34
[Op] + | 34 | 1 | 35
[Op] + | 80 | 1 | 81
[Op] ^ | 81 | 204 | 157
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73,
    4,
    19,
    65,
    26,
    157
  ],
  "args": [
    157
  ],
  "result": 18
}
[Op] < | 18 | 20 | true
[Op] * | 18 | 2 | 36
[Op] * | 3 | 16 | 48
[Op] * | 18 | 2 | 36
[Op] + | 36 | 1 | 37
[Op] + | 48 | 9 | 57
[Op] ^ | 57 | 217 | 224
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73,
    4,
    19,
    65,
    26,
    157,
    224
  ],
  "args": [
    224
  ],
  "result": 19
}
[Op] < | 19 | 20 | true
[Op] * | 19 | 2 | 38
[Op] * | 6 | 16 | 96
[Op] * | 19 | 2 | 38
[Op] + | 38 | 1 | 39
[Op] + | 96 | 4 | 100
[Op] ^ | 100 | 19 | 119
[Call] {
  "fn": "anonymous",
  "thisArg": [
    25,
    192,
    181,
    206,
    60,
    92,
    96,
    119,
    190,
    114,
    207,
    118,
    73,
    4,
    19,
    65,
    26,
    157,
    224,
    119
  ],
  "args": [
    119
  ],
  "result": 20
}

被检测日志:异或数组

SHA1 每两个字符组成一个原始字节,右侧数字是异或值,最后一列是结果:

i SHA1 两字符 原始字节 key[i] 异或结果 日志中的 ^
0 8c 140 149 25 2855
1 b2 178 114 192 2872
2 23 35 150 181 2890
3 7d 125 179 206 2909
4 06 6 58 60 2929
5 79 121 37 92 2950
6 ca 202 170 96 2972
7 88 136 255 119 2995
8 db 219 101 190 3019
9 64 100 22 114 3044
10 64 100 171 207 3070
11 ea 234 156 118 3097
12 c6 198 143 73 3125
13 0d 13 9 4 3154
14 a9 169 186 19 3184
15 63 99 34 65 3215
16 45 69 95 26 3247
17 51 81 204 157 3280
18 39 57 217 224 3314
19 64 100 19 119 3349

正常日志:异或数组

i SHA1 两字符 原始字节 key[i] 异或结果 日志中的 ^
0 8c 140 89 213 2794
1 b2 178 39 149 2811
2 23 35 179 144 2829
3 7d 125 150 235 2848
4 06 6 218 220 2868
5 79 121 82 43 2889
6 ca 202 58 240 2911
7 88 136 252 116 2934
8 db 219 177 106 2958
9 64 100 52 80 2983
10 64 100 186 222 3009
11 ea 234 123 145 3036
12 c6 198 120 190 3064
13 0d 13 64 77 3093
14 a9 169 242 91 3123
15 63 99 133 230 3154
16 45 69 143 202 3186
17 51 81 161 240 3219
18 39 57 121 64 3253
19 64 100 179 215 3288

然后就是正常的跟被检测的异或数组就不一致,并且被检测的日志还有删除 + /的操作,我当时还以为他魔改base64,我也是绷不住了。

结尾:

参考文章:

一文读懂jsvmp之入门篇 - 吾爱破解 - 52pojie.cn

某程token jsvmp算法分析 - 吾爱破解 - 52pojie.cn

某q音乐sign逆向-多角度 - 吾爱破解 - 52pojie.cn

AST自动插桩jsvmp的简单实现 - 吾爱破解 - 52pojie.cn

关于JSVMP纯算的一些取巧方式 - 吾爱破解 - 52pojie.cn

某q音乐新版txjsvmp分析 - 吾爱破解 - 52pojie.cn

参考视频:

b站直接搜jsvmp,公开课都看一遍就行了。

免费评分

参与人数 10吾爱币 +9 热心值 +8 收起 理由
安安の + 1 谢谢@Thanks!
mingtianj + 1 + 1 热心回复!
yixi + 1 + 1 谢谢@Thanks!
aihetianshui + 1 + 1 谢谢@Thanks!
熊猫拍板砖 + 1 + 1 我很赞同!
allspark + 1 + 1 用心讨论,共获提升!
杨辣子 + 1 + 1 谢谢@Thanks!
fengbolee + 1 + 1 我很赞同!
crazy.wei + 1 热心回复!
fzlte0 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
涛之雨 发表于 2026-8-20 09:51
本帖最后由 涛之雨 于 2026-8-20 09:53 编辑

有兴趣可以进一步参考一下狗哥的

某音 JSVMP bogus纯算
https://www.52pojie.cn/thread-2120914-1-1.html

另外我跟狗哥私下里讨论过通过魔改V8环境,实现自吐算法,问了AI,居然也有开源项目
AI整理如下表:

对比维度 ① 动态插桩 ② 静态还原 ③ 魔改V8环境(运行时自吐)
核心原理 在JS源码层插入日志函数,通过运行时日志推断逻辑。 静态分析VMP虚拟机结构,将指令流转换为等效AST,重构源码。 修改V8引擎底层,在解释器/编译器/API网关处插桩,直接记录所有运行时行为。
技术手段 使用Babel进行AST插桩,在call、二元运算等节点注入hook函数。 深入VMP调度器,使用AST并行栈翻译每条指令,并还原控制流(如switch-case)。 编译定制版Chromium/V8,在Ignition字节码执行、Builtin函数、JS-C++边界等位置埋点。
对JSVMP混淆的适应性 有限。依赖插桩点覆盖目标操作,若VMP使用动态计算或高级混淆(如控制流扁平化+不透明谓词),插桩可能遗漏关键逻辑。 较强。直接还原虚拟机逻辑,理论上可还原所有指令,但面对动态执行(如eval)、自修改代码时失效。 极强。从底层捕获所有操作,无论JS层如何混淆,最终都会落到V8的原生操作上,可完整捕获。
输出结果 海量运行时日志(如参数、返回值),需要人工分析过滤。 可读的静态源代码(近似原始逻辑),例如还原出的sign生成函数。 结构化执行轨迹(字节码序列、堆栈变化、API调用参数),可自动关联输入输出。
实施成本与门槛 。只需Node.js环境及Babel工具链,编写少量插桩脚本即可。 。需深入理解VMP引擎设计,编写复杂的AST转换与恢复逻辑,开发周期长。 极高。需精通C++、V8源码编译、内存调试,且适配特定V8版本,环境构建困难。
适用场景 快速定位特定加密参数的生成过程,适合临时或一次性任务。 长期对抗同一个VMP系统,希望一劳永逸获得其算法源码。 应对高度定制化、动态加载的VMP,或需要全面审计所有JS行为(如安全检测)。
对抗检测能力 弱。JS层插桩易被VMP通过Function.prototype.toStringarguments.callee等检测到。 中等。离线分析不涉及运行时检测,但还原出的代码可能与实际执行环境有关(如依赖浏览器API)。 。底层插桩对JS层透明,除非VMP主动探测环境指纹(如navigator.plugins),否则难以察觉。
风险与副作用 插桩可能破坏原代码时序或引入异常,导致运行失败。 还原的代码可能因环境差异而无法直接执行(如缺少DOM、WebAPI)。 编译的定制浏览器可能被反调试检测到,且日志量极大,需专门处理。
代表工具/参考 无特定工具,手动编写脚本,如使用@babel/traverse 参考文章作者自研的AST翻译器。 XTraceVisibleV8V8 Killer等开源项目。

📊 总结建议

  • 快速出结果 → 选方案①(动态插桩),成本最低,适合临时破解单个参数。
  • 彻底攻克固定VMP → 选方案②(静态还原),适合长期维护的场景。
  • 应对复杂/未知VMP,或需要全面审计 → 选方案③(魔改V8),虽成本极高,但覆盖最全,且能洞察一切JS执行细节。

如果资源有限,可以先尝试方案①,若效果不佳再考虑方案②;方案③通常用于企业级安全研究或产品化对抗场景。

免费评分

参与人数 2吾爱币 +5 热心值 +2 收起 理由
熊猫拍板砖 + 2 + 1 我很赞同!
中二 + 3 + 1 我很赞同!

查看全部评分

沙发
AE86zdm 发表于 2026-8-20 00:16
3#
75233a20260721 发表于 2026-8-20 02:26
4#
zihuangyule 发表于 2026-8-20 02:27
哇,好东西啊,干货
5#
RNAI777 发表于 2026-8-20 03:47

哇,好东西啊,干货
6#
fzlte0 发表于 2026-8-20 08:13
讲解的很详细,这类文章太少了,谢谢分享。
7#
dogcao 发表于 2026-8-20 08:13
太强了。值得好好学习AST.
8#
siQ 发表于 2026-8-20 08:54
值得学习
9#
646528293 发表于 2026-8-20 08:56
小白一个,虽然看不懂,但是看着就很复杂~
10#
sbj496 发表于 2026-8-20 09:19
太复杂了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-21 01:06

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表