一直有听说jsvmp的大名,但这玩意具体是什么呢?

首先是vmp,即 Virtual Machine Protection 虚拟机保护技术 原理这里有篇介绍:

VMP加壳(二):VMP的虚拟化原理 - 第七子007 - 博客园

jsvmp,顾名思义,就是用来保护js代码的,通过自定义一个js虚拟机,将原始的指令转化为自定义的指令,运行时将该函数放到虚拟机中运行,从而整体逻辑无法被跟踪与逆向

这样说你可能还是一头雾水,还是边实战边体验虚拟机的奇妙之处吧

题目地址:https://match.yuanrenxue.com/match/18

这里只有v参数是不确定的,追栈追到cback函数,很明显这里是处理拿到后的数据,生成一些html节点

我们直接在 xml.open 函数这里打上断点,然后F11进入函数内部看一下

不出意外的话,就是这里面生成了v和t两个参数,F10一步一步向下执行,直到生成完整的链接

调试到这里的时候,v和t就已经生成完毕了,所以可以判断生成点在这里

注意到函数的末尾,有一大串乱码的字符串,那其实就是 jsvmp,很多重要的逻辑都在那一大串里面,想调试几乎不可能

进入函数内部调试的时候,发现一个可疑的地方,就是AES加密算法

大胆猜测题目是使用了AES对v参数进行了加密,这里就需要hook一下AES函数了,直接在控制台输入下面的代码:

1
2
3
4
5
6
7
8
9
10
aes_encrypt_old = _[1][0]['CryptoJS']['AES']['encrypt']
_[1][0]['CryptoJS']['AES']['encrypt'] = function (a, b, c, d, e) {
debugger;
var result = aes_encrypt_old(a, b, c, d, e);
console.log(result.toString())
debugger;
return result;
}

// 需要等step into函数内部的时候,再进行hook操作

a, b, c, d, e五个参数中d和e其实用不到,传入的时候是undefined,只是为了囊获尽可能多的参数

然后F8跳到下一个断点,就自动hook住了

通过对比,发现result就是v参数

到这里有两个地方是确定的,AES采用了 CBC模式,PKCS7填充,还有三个地方不确定:

  • 待加密的原文
  • 密钥 key
  • 偏移量 iv

还记得我们hook的时候传入的a b c三个参数吗?其实答案就在里面

乍一看三个参数可能有点懵,不是字符串而是数组的形式,这里其实是经过了 enc.Utf8.parse 函数,把原文转换成了十六进制数,才能够用于 AES加密,不信做一个对比:

注意全部都有 sigBytes 属性

还原的话,就需要用到 enc.Utf8.stringify 函数了,鼠标右键,然后clear清空一下控制台

一般来说 key和iv都是16位,所以可以根据长度来判断各自的类型

通过测试发现:

  • key和iv每次都是变动的,且两者保持一致

  • text则是 page| 加上未知字符串的形式,且字符串每次都是变动的

F11 step out function 看一下源码里面是哪里调用了 encrypt

鼠标停留在这里,加上一个 log breakpoint 打印所有经过的参数和结果

然后随便点击一页数据,控制台输出日志

这里已经看到最终的生成结果了

1
['3|323d323,323d323,323u323,323u323', '62c014b562c014b5'] 'Yh8TT+T4fF6EaJBp3/JH4lAYtdwYDj76HX92lwwYPsM2XxdErZLVdruPowLe2S3s'

分别对应 原文、密钥、密文,观察日志看看它们都是怎么生成的

首先是密钥:

大致过程就是生成10位的时间戳,然后两次转换成十六进制,把两次的结果做一个拼接

用python代码验证一下:

1
2
3
4
In [3]: a = 1656755381

In [4]: format(a, "x")
Out[4]: '62c014b5'

再来看一下原文,注意到控制台在鼠标移动的时候,一直都有输出,怀疑是检测鼠标的轨迹

而原文又正好跟轨迹的最后一行是一致的,如图:

因此可以推测,原文就是 page|鼠标轨迹 的形式,看日志轨迹是随机变动的,没什么规律可循,所以这里我们直接写死就行

代码整理

之前有封装过AES的工具类,直接拿来用就行:

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
38
39
40
41
42
43
44
45
46
47
48
import base64

from Crypto.Cipher import AES


class AESTool:
def __init__(self, iv, key):
self.key = key.encode("utf-8")
self.iv = iv.encode("utf-8")

def pkcs7padding(self, text):
"""
明文使用PKCS7填充
"""
bs = 16
length = len(text)
bytes_length = len(text.encode("utf-8"))
padding_size = length if (bytes_length == length) else bytes_length
padding = bs - padding_size % bs
padding_text = chr(padding) * padding
self.coding = chr(padding)
return text + padding_text

def encrypt(self, content: str):
"""
AES加密
"""
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
content_padding = self.pkcs7padding(content)
encrypt_bytes = cipher.encrypt(content_padding.encode("utf-8"))
result = str(base64.b64encode(encrypt_bytes), encoding="utf-8")
return result

def decrypt(self, content: str):
"""
AES解密
"""
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
content = base64.b64decode(content)
text = cipher.decrypt(content).decode("utf-8")
return self.pkcs7padding(text)


key = iv = '62c014b562c014b5'
text = '3|323d323,323d323,323u323,323u323'

aes_tool = AESTool(iv, key)
print(aes_tool.encrypt(text))

很好,输出结果和网站的一致,证明没问题,爬虫代码如下:

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
import requests
import time
import base64

from Crypto.Cipher import AES

yuanrenxue_headers = {'User-Agent': 'yuanrenxue.project',
'Cookie': 'sessionid=s4v1l75430r6xvg0ra6011hcdhnp381w'}


def get_v(t, page):
x = format(t, 'x')
key = iv = 2 * x
text = str(page) + '|323d323,323d323,323u323,323u323'
aes_tool = AESTool(iv, key)
return aes_tool.encrypt(text)


def main():
total = 0
for page in range(1, 6):
t = int(time.time())
url = 'https://match.yuanrenxue.com/match/18data'
params = {'page': page, 't': t, 'v': get_v(t, page)}
res = requests.get(url, params=params,
headers=yuanrenxue_headers)
data = res.json()['data']

for d in data:
total += d['value']

print ('总和为:', total)


main()

拿到需要的数据:

通关: