0%

JSVMP补环境

实现原理

JSVMP 的核心是在 JavaScript 代码保护过程中引入代码虚拟化思想,实现源代码的虚拟化过程,将目标代码转换成自定义的字节码,这些字节码只有特殊的解释器才能识别,隐藏目标代码的关键逻辑。服务器端读取 JavaScript 代码 —> 词法分析 —> 语法分析 —> 生成AST语法树 —> 生成私有指令 —> 生成对应私有解释器,将私有指令加密与私有解释器发送给浏览器,然后一边解释,一边执行。

模拟JSVMP执行流程

准备数据

1
2
3
4
5
var a = '丽丽'
var b = '菲菲'
var c = '莹莹'
var d = a+b
var e = c + d

第一次数据转换

1
2
3
4
5
6
7
8
9
10
11
12
var a ;
a = '丽丽'
var b;
b = '菲菲'
var c;
c = '莹莹'
var d;
a+b;
d = ?;
var e;
c+d;
e = ?;

第二次转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 我们假设赋值指令为 1, 加和指令为 2,声明指令为 3
// 如果按照从上到下的顺序,我们就可以将他们的操作变成指令性的[用|分割左侧和右侧]
// 1 赋值
// 2 加
// 3 声明

3 --- var | a
1 --- a | '丽丽'
3 --- var | b
1 --- b | '菲菲'
3 --- var | c
1 --- c | '莹莹'
3 --- var | d
2 --- a | b -----> ?
1 --- d | ?
3 --- var | e
2 --- d | c -----> ?
1 --- e | ?(此处的d 与 c的和)

将数据压缩到数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_stack = [
[3, 'var', 'a'],
[1, 'a', '丽丽'],
[3, 'var', 'b'],
[1, 'b', '菲菲'],
[3, 'var', 'c'],
[1, 'c', '莹莹'],
[3, 'var', 'd'],
[2, 'a', 'b'],
[1, 'd', '?'],
[3, 'var', 'e'],
[2, 'd', 'c'],
[1, 'e', '?'],
]

通过自执行函数,对数据进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
!function(_stack) {
var register; // 这个就当做是问号的存储位置
var variable = {}; // 这个就当做是var变量的存储位置。由于没有其他声明方式的存在,所就不写其他的了
for (let i = 0; i < _stack.length; i++) {
instruct = _stack[i][0];
left = _stack[i][1];
right = _stack[i][2];
if (instruct === 3) {
variable[right] = ''
}
if (instruct === 1) {
if (right === '?') {
variable[left] = register
} else {
variable[left] = right
}
}
if (instruct === 2) {
register = variable[left] + variable[right]
}
};
console.log(variable)
} ([
[3, 'var', 'a'],
[1, 'a', '丽丽'],
[3, 'var', 'b'],
[1, 'b', '菲菲'],
[3, 'var', 'c'],
[1, 'c', '莹莹'],
[3, 'var', 'd'],
[2, 'a', 'b'],
[1, 'd', '?'],
[3, 'var', 'e'],
[2, 'd', 'c'],
[1, 'e', '?'],
]
)

实际jsvmp会更加的复杂,这个是基本的逻辑,就行自己写一个解释器来解释自己的代码

关于jsvmp的解法一般有3种,补环境,和插桩扣逻辑,jsrpc,当然还有自动化等方式可自行研究试试

实战1

https://www.toutiao.com/?wid=1742104764742

尝试逆向_signature参数

image-20250316142102485

直接全局找一下参数名称

image-20250316142235018

找到疑似加密点,接着在这里做一些断点分析,可以看出来是P函数进行加密的

image-20250316142423097

梳理一下逻辑,就是将完整的url(https://www.toutiao.com/toutiao/api/pc/info)传递给`window.byted_acrawler.sign`函数进行签名

image-20250316142628415

跟进到这个方法,可以往文件头部去看一下

image-20250316163543028

很明显的jsvmp的结构,上面的方法是他用来翻译代码的解释器,调用的时候就把对应的参数传递,他会进行解析,后面还有一些环境判断

这种情况直接把整个JS文件复制到本地,执行一下

image-20250316163800264

referrer对应的键没有定义,尝试去断点,但是这里有很多地方都会调用到这里,不太好找,采用日志断点

image-20250316163919713

刷新一下,观察日志中有referrer的地方

image-20250316163946231

可以看到是document对象,Node环境中并没有,所以定义一个,顺便也把window全局对象也定义了

1
2
window = global;
document = {};

再次运行

image-20250316164857540

没有相应的方法,而这种一般是通过exports导入的,我们看一下最后的环境判断

image-20250316165016202

浏览器中exports是undefined

image-20250316165130921

所以我们直接将"undefined" != typeof exports ? exports : void 0修改为void 0

再次运行

image-20250316171228213

这个就没法通过日志去看了,因为日志中有很多length的定义,我们观察一下代码S[R] = S[R][A],出现这个报错肯定是S[R]为undefined,而S[R]是上一次循环被赋值的,所以我们只需要找到length上一次的定义逻辑即可

通过调试可以发现,当通过protocol赋值之后,S[R]就变为了undefined

image-20250316171546096 image-20250316171648082

而第一张图里面的Object正是我们的document对象(在出现找不到href属性时通过日志找到的),那么我们再加上一个protocol属性即可

image-20250316171852803

再次运行

image-20250316171917394

可以发现长度有点过于短了,我们可以在这里加一个输出,看还有哪些参数是需要的

image-20250316172214040 image-20250316172303792

可以看到最后还有个cookie的值,我们尝试把浏览器的cookie值复制进去(cookie一般是在document对象中,其实在之前的日志调试也可以看到)

image-20250316174610703

document.cookie这个前面和后面都放一下吧,这里是放在后面才成功的….(可能是document还没有初始化的原因)