某q音乐jsvmp浅析
# 声明本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。
# 前言
网址: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; // 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] = h] >= h]
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]有关,此刻是没过检测点的日志
```
+ | 23 | 1 | 24
+ | 14 | 1 | 15
+ | 6 | 1 | 7
+ | 36 | 1 | 37
+ | 16 | 1 | 17
+ | 40 | 1 | 41
+ | 7 | 1 | 8
+ | 19 | 1 | 20
{
"fn": "map",
"thisArg": [
23,
14,
6,
36,
16,
40,
7,
19
],
"args": [
null
],
"result": [
"C",
"8",
"D",
"9",
"B",
null,
"0",
"6"
]
}
{
"fn": "join",
"thisArg": [
"C",
"8",
"D",
"9",
"B",
null,
"0",
"6"
],
"args": [
""
],
"result": "C8D9B06"
} [
""
],
"result": "A873DD4"
}
```
看了很多大佬的文章,说是直接前面生成的hash值取固定下标,我一直取的是错误的,后面才想起来可能是更新了,其实不然,于是仔细看了下日志,发现有不少检测点,先把相关检测点分析一下,再分析日志吧
### 检测点分析
正常检测点日志:
```
=== | object | object | true
=== | object | object | true
=== | object | object | true
{
"fn": "RegExp",
"args": [
"Headless",
"i"
],
"result": {}
}
{
"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
}
=== | undefined | 1 | false
{
"fn": "indexOf",
"thisArg": "y.qq.com",
"args": [
"qq.com"
],
"result": 2
}
> | 2 | -1 | true
{
"fn": "some",
"thisArg": [
"qq.com",
"joox.com",
"tencentmusic.com",
"wavecommittee.com",
"kugou.com",
"kuwo.cn"
],
"args": [
null
],
"result": true
}
```
非正常检测点日志:
```
=== | object | object | true
=== | object | object | true
=== | object | object | true
{
"fn": "RegExp",
"args": [
"Headless",
"i"
],
"result": {}
}
{
"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
}
=== | undefined | 1 | false
{
"fn": "indexOf",
"thisArg": "ntp.msn.cn",
"args": [
"qq.com"
],
"result": -1
}
> | -1 | -1 | false
{
"fn": "indexOf",
"thisArg": "ntp.msn.cn",
"args": [
"joox.com"
],
"result": -1
}
> | -1 | -1 | false
{
"fn": "indexOf",
"thisArg": "ntp.msn.cn",
"args": [
"tencentmusic.com"
],
"result": -1
}
> | -1 | -1 | false
{
"fn": "indexOf",
"thisArg": "ntp.msn.cn",
"args": [
"wavecommittee.com"
],
"result": -1
}
> | -1 | -1 | false
{
"fn": "indexOf",
"thisArg": "ntp.msn.cn",
"args": [
"kugou.com"
],
"result": -1
}
> | -1 | -1 | false
{
"fn": "indexOf",
"thisArg": "ntp.msn.cn",
"args": [
"kuwo.cn"
],
"result": -1
}
> | -1 | -1 | false
{
"fn": "some",
"thisArg": [
"qq.com",
"joox.com",
"tencentmusic.com",
"wavecommittee.com",
"kugou.com",
"kuwo.cn"
],
"args": [
null
],
"result": false
}
```
日志检测对比:
某某对象检测,我也不知道具体检测哪些对象,估计得改针对性插桩才能知道吧
```
=== | object | object | true
```
创建了 Headless 检测正则。两个 User-Agent 都没有包含 Headless,所以结果一致,不影响后续结果
```
RegExp("Headless", "i")
```
估计猜测是自动化检测,这个并不受影响
```
=== | 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"
]
}
{
"fn": "charCodeAt",
"thisArg": "12345",
"args": [
0
],
"result": 49
}
{
"fn": "charCodeAt",
"thisArg": "12345",
"args": [
1
],
"result": 50
}
{
"fn": "charCodeAt",
"thisArg": "12345",
"args": [
2
],
"result": 51
}
{
"fn": "charCodeAt",
"thisArg": "12345",
"args": [
3
],
"result": 52
}
{
"fn": "charCodeAt",
"thisArg": "12345",
"args": [
4
],
"result": 53
}
{
"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
}
}
{
"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);
```
检测点之后日志分析:
车头密文分析:
正常日志:
经过多次调试,下标数组是固定的,正常日志就如各大文章视频所说直接取下标就行了。
```
{
"fn": "map",
"thisArg": [
23,
14,
6,
36,
16,
40,
7,
19
],
"args": [
null
],
"result": [
"A",
"8",
"7",
"3",
"D",
null,
"D",
"4"
]
}
{
"fn": "join",
"thisArg": [
"A",
"8",
"7",
"3",
"D",
null,
"D",
"4"
],
"args": [
""
],
"result": "A873DD4"
}
```
跟日志也是能对得上的~~~~
检测点日志分析:
是一样的,但是结果却是数组每个+1 去取下标,这样子就导致了提取的值不一致
```
+ | 23 | 1 | 24
+ | 14 | 1 | 15
+ | 6 | 1 | 7
+ | 36 | 1 | 37
+ | 16 | 1 | 17
+ | 40 | 1 | 41
+ | 7 | 1 | 8
+ | 19 | 1 | 20
{
"fn": "map",
"thisArg": [
23,
14,
6,
36,
16,
40,
7,
19
],
"args": [
null
],
"result": [
"C",
"8",
"D",
"9",
"B",
null,
"0",
"6"
]
}
{
"fn": "join",
"thisArg": [
"C",
"8",
"D",
"9",
"B",
null,
"0",
"6"
],
"args": [
""
],
"result": "C8D9B06"
}
```
跟日志上面的结果也是能对得上的,最起码检测点不过车头密文是会受影响的,每个数组都会加1
车尾密文:
车尾密文就不多说了,跟车头密文是一致的只是换了下标数组,检测点不一致照样会收影响
中间密文:
正常日志:
```
< | 0 | 20 | true
* | 0 | 2 | 0
* | 8 | 16 | 128
* | 0 | 2 | 0
+ | 0 | 1 | 1
+ | 128 | 12 | 140
^ | 140 | 89 | 213
{
"fn": "push",
"thisArg": [
213
],
"args": [
213
],
"result": 1
}
< | 1 | 20 | true
* | 1 | 2 | 2
* | 11 | 16 | 176
* | 1 | 2 | 2
+ | 2 | 1 | 3
+ | 176 | 2 | 178
^ | 178 | 39 | 149
{
"fn": "push",
"thisArg": [
213,
149
],
"args": [
149
],
"result": 2
}
< | 2 | 20 | true
* | 2 | 2 | 4
* | 2 | 16 | 32
* | 2 | 2 | 4
+ | 4 | 1 | 5
+ | 32 | 3 | 35
^ | 35 | 179 | 144
{
"fn": "push",
"thisArg": [
213,
149,
144
],
"args": [
144
],
"result": 3
}
< | 3 | 20 | true
* | 3 | 2 | 6
* | 7 | 16 | 112
* | 3 | 2 | 6
+ | 6 | 1 | 7
+ | 112 | 13 | 125
^ | 125 | 150 | 235
{
"fn": "push",
"thisArg": [
213,
149,
144,
235
],
"args": [
235
],
"result": 4
}
< | 4 | 20 | true
* | 4 | 2 | 8
* | 0 | 16 | 0
* | 4 | 2 | 8
+ | 8 | 1 | 9
+ | 0 | 6 | 6
^ | 6 | 218 | 220
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220
],
"args": [
220
],
"result": 5
}
< | 5 | 20 | true
* | 5 | 2 | 10
* | 7 | 16 | 112
* | 5 | 2 | 10
+ | 10 | 1 | 11
+ | 112 | 9 | 121
^ | 121 | 82 | 43
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43
],
"args": [
43
],
"result": 6
}
< | 6 | 20 | true
* | 6 | 2 | 12
* | 12 | 16 | 192
* | 6 | 2 | 12
+ | 12 | 1 | 13
+ | 192 | 10 | 202
^ | 202 | 58 | 240
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240
],
"args": [
240
],
"result": 7
}
< | 7 | 20 | true
* | 7 | 2 | 14
* | 8 | 16 | 128
* | 7 | 2 | 14
+ | 14 | 1 | 15
+ | 128 | 8 | 136
^ | 136 | 252 | 116
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116
],
"args": [
116
],
"result": 8
}
< | 8 | 20 | true
* | 8 | 2 | 16
* | 13 | 16 | 208
* | 8 | 2 | 16
+ | 16 | 1 | 17
+ | 208 | 11 | 219
^ | 219 | 177 | 106
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106
],
"args": [
106
],
"result": 9
}
< | 9 | 20 | true
* | 9 | 2 | 18
* | 6 | 16 | 96
* | 9 | 2 | 18
+ | 18 | 1 | 19
+ | 96 | 4 | 100
^ | 100 | 52 | 80
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80
],
"args": [
80
],
"result": 10
}
< | 10 | 20 | true
* | 10 | 2 | 20
* | 6 | 16 | 96
* | 10 | 2 | 20
+ | 20 | 1 | 21
+ | 96 | 4 | 100
^ | 100 | 186 | 222
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80,
222
],
"args": [
222
],
"result": 11
}
< | 11 | 20 | true
* | 11 | 2 | 22
* | 14 | 16 | 224
* | 11 | 2 | 22
+ | 22 | 1 | 23
+ | 224 | 10 | 234
^ | 234 | 123 | 145
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80,
222,
145
],
"args": [
145
],
"result": 12
}
< | 12 | 20 | true
* | 12 | 2 | 24
* | 12 | 16 | 192
* | 12 | 2 | 24
+ | 24 | 1 | 25
+ | 192 | 6 | 198
^ | 198 | 120 | 190
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80,
222,
145,
190
],
"args": [
190
],
"result": 13
}
< | 13 | 20 | true
* | 13 | 2 | 26
* | 0 | 16 | 0
* | 13 | 2 | 26
+ | 26 | 1 | 27
+ | 0 | 13 | 13
^ | 13 | 64 | 77
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80,
222,
145,
190,
77
],
"args": [
77
],
"result": 14
}
< | 14 | 20 | true
* | 14 | 2 | 28
* | 10 | 16 | 160
* | 14 | 2 | 28
+ | 28 | 1 | 29
+ | 160 | 9 | 169
^ | 169 | 242 | 91
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80,
222,
145,
190,
77,
91
],
"args": [
91
],
"result": 15
}
< | 15 | 20 | true
* | 15 | 2 | 30
* | 6 | 16 | 96
* | 15 | 2 | 30
+ | 30 | 1 | 31
+ | 96 | 3 | 99
^ | 99 | 133 | 230
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80,
222,
145,
190,
77,
91,
230
],
"args": [
230
],
"result": 16
}
< | 16 | 20 | true
* | 16 | 2 | 32
* | 4 | 16 | 64
* | 16 | 2 | 32
+ | 32 | 1 | 33
+ | 64 | 5 | 69
^ | 69 | 143 | 202
{
"fn": "push",
"thisArg": [
213,
149,
144,
235,
220,
43,
240,
116,
106,
80,
222,
145,
190,
77,
91,
230,
202
],
"args": [
202
],
"result": 17
}
< | 17 | 20 | true
* | 17 | 2 | 34
* | 5 | 16 | 80
* | 17 | 2 | 34
+ | 34 | 1 | 35
+ | 80 | 1 | 81
^ | 81 | 161 | 240
{
"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
}
< | 18 | 20 | true
* | 18 | 2 | 36
* | 3 | 16 | 48
* | 18 | 2 | 36
+ | 36 | 1 | 37
+ | 48 | 9 | 57
^ | 57 | 121 | 64
{
"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
}
< | 19 | 20 | true
* | 19 | 2 | 38
* | 6 | 16 | 96
* | 19 | 2 | 38
+ | 38 | 1 | 39
+ | 96 | 4 | 100
^ | 100 | 179 | 215
{
"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
}
```
被风控日志:
```
< | 0 | 20 | true
* | 0 | 2 | 0
* | 8 | 16 | 128
* | 0 | 2 | 0
+ | 0 | 1 | 1
+ | 128 | 12 | 140
^ | 140 | 149 | 25
{
"fn": "anonymous",
"thisArg": [
25
],
"args": [
25
],
"result": 1
}
< | 1 | 20 | true
* | 1 | 2 | 2
* | 11 | 16 | 176
* | 1 | 2 | 2
+ | 2 | 1 | 3
+ | 176 | 2 | 178
^ | 178 | 114 | 192
{
"fn": "anonymous",
"thisArg": [
25,
192
],
"args": [
192
],
"result": 2
}
< | 2 | 20 | true
* | 2 | 2 | 4
* | 2 | 16 | 32
* | 2 | 2 | 4
+ | 4 | 1 | 5
+ | 32 | 3 | 35
^ | 35 | 150 | 181
{
"fn": "anonymous",
"thisArg": [
25,
192,
181
],
"args": [
181
],
"result": 3
}
< | 3 | 20 | true
* | 3 | 2 | 6
* | 7 | 16 | 112
* | 3 | 2 | 6
+ | 6 | 1 | 7
+ | 112 | 13 | 125
^ | 125 | 179 | 206
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206
],
"args": [
206
],
"result": 4
}
< | 4 | 20 | true
* | 4 | 2 | 8
* | 0 | 16 | 0
* | 4 | 2 | 8
+ | 8 | 1 | 9
+ | 0 | 6 | 6
^ | 6 | 58 | 60
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60
],
"args": [
60
],
"result": 5
}
< | 5 | 20 | true
* | 5 | 2 | 10
* | 7 | 16 | 112
* | 5 | 2 | 10
+ | 10 | 1 | 11
+ | 112 | 9 | 121
^ | 121 | 37 | 92
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92
],
"args": [
92
],
"result": 6
}
< | 6 | 20 | true
* | 6 | 2 | 12
* | 12 | 16 | 192
* | 6 | 2 | 12
+ | 12 | 1 | 13
+ | 192 | 10 | 202
^ | 202 | 170 | 96
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96
],
"args": [
96
],
"result": 7
}
< | 7 | 20 | true
* | 7 | 2 | 14
* | 8 | 16 | 128
* | 7 | 2 | 14
+ | 14 | 1 | 15
+ | 128 | 8 | 136
^ | 136 | 255 | 119
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119
],
"args": [
119
],
"result": 8
}
< | 8 | 20 | true
* | 8 | 2 | 16
* | 13 | 16 | 208
* | 8 | 2 | 16
+ | 16 | 1 | 17
+ | 208 | 11 | 219
^ | 219 | 101 | 190
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190
],
"args": [
190
],
"result": 9
}
< | 9 | 20 | true
* | 9 | 2 | 18
* | 6 | 16 | 96
* | 9 | 2 | 18
+ | 18 | 1 | 19
+ | 96 | 4 | 100
^ | 100 | 22 | 114
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114
],
"args": [
114
],
"result": 10
}
< | 10 | 20 | true
* | 10 | 2 | 20
* | 6 | 16 | 96
* | 10 | 2 | 20
+ | 20 | 1 | 21
+ | 96 | 4 | 100
^ | 100 | 171 | 207
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114,
207
],
"args": [
207
],
"result": 11
}
< | 11 | 20 | true
* | 11 | 2 | 22
* | 14 | 16 | 224
* | 11 | 2 | 22
+ | 22 | 1 | 23
+ | 224 | 10 | 234
^ | 234 | 156 | 118
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114,
207,
118
],
"args": [
118
],
"result": 12
}
< | 12 | 20 | true
* | 12 | 2 | 24
* | 12 | 16 | 192
* | 12 | 2 | 24
+ | 24 | 1 | 25
+ | 192 | 6 | 198
^ | 198 | 143 | 73
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114,
207,
118,
73
],
"args": [
73
],
"result": 13
}
< | 13 | 20 | true
* | 13 | 2 | 26
* | 0 | 16 | 0
* | 13 | 2 | 26
+ | 26 | 1 | 27
+ | 0 | 13 | 13
^ | 13 | 9 | 4
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114,
207,
118,
73,
4
],
"args": [
4
],
"result": 14
}
< | 14 | 20 | true
* | 14 | 2 | 28
* | 10 | 16 | 160
* | 14 | 2 | 28
+ | 28 | 1 | 29
+ | 160 | 9 | 169
^ | 169 | 186 | 19
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114,
207,
118,
73,
4,
19
],
"args": [
19
],
"result": 15
}
< | 15 | 20 | true
* | 15 | 2 | 30
* | 6 | 16 | 96
* | 15 | 2 | 30
+ | 30 | 1 | 31
+ | 96 | 3 | 99
^ | 99 | 34 | 65
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114,
207,
118,
73,
4,
19,
65
],
"args": [
65
],
"result": 16
}
< | 16 | 20 | true
* | 16 | 2 | 32
* | 4 | 16 | 64
* | 16 | 2 | 32
+ | 32 | 1 | 33
+ | 64 | 5 | 69
^ | 69 | 95 | 26
{
"fn": "anonymous",
"thisArg": [
25,
192,
181,
206,
60,
92,
96,
119,
190,
114,
207,
118,
73,
4,
19,
65,
26
],
"args": [
26
],
"result": 17
}
< | 17 | 20 | true
* | 17 | 2 | 34
* | 5 | 16 | 80
* | 17 | 2 | 34
+ | 34 | 1 | 35
+ | 80 | 1 | 81
^ | 81 | 204 | 157
{
"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
}
< | 18 | 20 | true
* | 18 | 2 | 36
* | 3 | 16 | 48
* | 18 | 2 | 36
+ | 36 | 1 | 37
+ | 48 | 9 | 57
^ | 57 | 217 | 224
{
"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
}
< | 19 | 20 | true
* | 19 | 2 | 38
* | 6 | 16 | 96
* | 19 | 2 | 38
+ | 38 | 1 | 39
+ | 96 | 4 | 100
^ | 100 | 19 | 119
{
"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 | 异或结果 | 日志中的 `^` |
| --- | --- | --- | --- | --- | --- |
| 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 | 异或结果 | 日志中的 `^` |
| --- | --- | --- | --- | --- | --- |
| 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](https://www.52pojie.cn/forum.php?mod=viewthread&tid=2117124&highlight=jsvmp)
[某程token jsvmp算法分析 - 吾爱破解 - 52pojie.cn](https://www.52pojie.cn/forum.php?mod=viewthread&tid=2034891&highlight=jsvmp)
[某q音乐sign逆向-多角度 - 吾爱破解 - 52pojie.cn](https://www.52pojie.cn/thread-2022463-1-1.html)
(https://www.52pojie.cn/forum.php?mod=viewthread&tid=2079058&highlight=jsvmp)
[关于JSVMP纯算的一些取巧方式 - 吾爱破解 - 52pojie.cn](https://www.52pojie.cn/forum.php?mod=viewthread&tid=2073641&highlight=jsvmp)
[某q音乐新版txjsvmp分析 - 吾爱破解 - 52pojie.cn](https://www.52pojie.cn/forum.php?mod=viewthread&tid=1969992&highlight=jsvmp)
参考视频:
b站直接搜jsvmp,公开课都看一遍就行了。 本帖最后由 涛之雨 于 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.toString`、`arguments.callee`等检测到。 | 中等。离线分析不涉及运行时检测,但还原出的代码可能与实际执行环境有关(如依赖浏览器API)。 | **强**。底层插桩对JS层透明,除非VMP主动探测环境指纹(如`navigator.plugins`),否则难以察觉。 |
| **风险与副作用** | 插桩可能破坏原代码时序或引入异常,导致运行失败。 | 还原的代码可能因环境差异而无法直接执行(如缺少DOM、WebAPI)。 | 编译的定制浏览器可能被反调试检测到,且日志量极大,需专门处理。 |
| **代表工具/参考** | 无特定工具,手动编写脚本,如使用`@babel/traverse`。 | 参考文章作者自研的AST翻译器。 | (https://github.com/linn0x/xtrace)、(https://github.com/wspr-ncsu/visiblev8)、(https://github.com/ShellWen/v8_killer)等开源项目。 |
📊 总结建议
- **快速出结果** → 选方案①(动态插桩),成本最低,适合临时破解单个参数。
- **彻底攻克固定VMP** → 选方案②(静态还原),适合长期维护的场景。
- **应对复杂/未知VMP,或需要全面审计** → 选方案③(魔改V8),虽成本极高,但覆盖最全,且能洞察一切JS执行细节。
如果资源有限,可以先尝试方案①,若效果不佳再考虑方案②;方案③通常用于企业级安全研究或产品化对抗场景。 如此巨大的信息量 慢慢看
哇,好东西啊,干货 讲解的很详细,这类文章太少了,谢谢分享。 太强了。值得好好学习AST. 小白一个,虽然看不懂,但是看着就很复杂~ 太复杂了 有用 mark 信息量很大,慢慢消化