JS逆向实战:前端渲染

引言

还是原来的爬虫练习平台,我看比我之前写博客的时候更新了一些前端渲染的内容,本文的重点是 JS 逆向中的前端渲染。

spa7

spa7 地址:https://spa7.scrape.center/

spa7 说明如下:

NBA 球星数据网站,数据纯前端渲染,Token 经过加密处理,适合基础 JavaScript 模拟分析。

打开浏览器的网络标签页,查看网络请求。

image-20250112190647381

发现在 main.js 后面就开始加载图片了,细想一下,只有在获取到了球星的数据之后,才能通过数据拿到对应的图片地址,然后加载图片,那么我们可以大胆猜测,main 这个 js 里面大概率就是我们需要的数据。

打开它看看:

image-20250112190821565

猜对了,里面的确是我们想要的数据,那么获取它的数据就简单了,直接请求这个 js 就可以了,虽然这个说明中提到了 token 加密,但是我实际上没有发现 token 有关的内容,这里就先忽略吧。

至于请求 js 如何拿到数据,很简单,正则表达式,或者本地执行 js 或者其他方案都可以,只要能拿到数据即可,我这里使用正则的方案。顺便推荐一个好用的正则表达式测试网站,可以很方便的调试正则表达式,再也不用一步步断点调试了。

image-20250112192929352

上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json
import re

import requests

res = requests.get('https://spa7.scrape.center/js/main.js').text
content = re.findall('players = (\[[\s\S]+\])', res)[0] # type:str
json_content = json.loads(content.replace('\'', '"')
.replace('name', '"name"')
.replace('image', '"image"')
.replace('birthday', '"birthday"')
.replace('height', '"height"')
.replace('weight', '"weight"')
.replace('},\n', '}\n'))
for a in json_content:
print(a)

详细代码可以看 Github:https://github.com/libra146/learnscrapy/tree/main/js

spa8

spa8 说明:

NBA 球星数据网站,数据纯前端渲染,Token 经过加密处理,JavaScript 代码一行混入 HTML 代码,防止直接调试,适合 JavaScript 逆向分析。

查看网络请求,发现只是把 js 放到了 html 源码中,spa7 的代码可以复用,基本上不用改,所以这里就不重复写了。至于为什么说明中写了 js 只有一行,但是我看 js 是格式化好的,不知道为啥,暂时忽略它。

spa9

spa9 说明:

NBA 球星数据网站,数据纯前端渲染,Token 经过加密处理, JavaScript 经过 eval混淆,适合 JavaScript 逆向分析。

image-20250112200018940

查看网络请求和 html 源代码,发现 js 数据的确被混淆了,先拿到 js 看看长什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
eval(function(p, a, c, k, e, r) {
e = function(c) {
return (c < 62 ? '' : e(parseInt(c / 62))) + ((c = c % 62) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
}
;
if ('0'.replace(0, e) == 0) {
while (c--)
r[e(c)] = k[c];
k = [function(e) {
return r[e] || e
}
];
e = function() {
return '[0-9a-zA-D]'
}
;
c = 1
}
;while (c--)
if (k[c])
p = p.replace(new RegExp('\\b' + e(c) + '\\b','g'), k[c]);
return p
}('g h=[{0:\'凯文-杜兰特\',4:\'durant.5\',1:\'b-09-c\',2:\'i\',3:\'108.j\'},{0:\'勒布朗-詹姆斯\',4:\'james.5\',1:\'k-12-30\',2:\'206cm\',3:\'113.l\'},{0:\'斯蒂芬-库里\',4:\'curry.5\',1:\'b-7-14\',2:\'m\',3:\'83.j\'},{0:\'詹姆斯-哈登\',4:\'harden.5\',1:\'1989-n-26\',2:\'196cm\',3:\'99.8\'},{0:\'扬尼斯-安特托昆博\',4:\'antetokounmpo.5\',1:\'o-12-d\',2:\'p\',3:\'109.8\'},{0:\'拉塞尔-威斯布鲁克\',4:\'westbrook.5\',1:\'b-11-12\',2:\'m\',3:\'90.7KG\'},{0:\'凯里-欧文\',4:\'irving.5\',1:\'1992-7-23\',2:\'q\',3:\'r.9\'},{0:\'安东尼-戴维斯\',4:\'davis.5\',1:\'1993-7-11\',2:\'i\',3:\'114.8\'},{0:\'乔尔-恩比德\',4:\'embiid.5\',1:\'o-7-16\',2:\'s\',3:\'127.0KG\'},{0:\'克雷-汤普森\',4:\'thompson.5\',1:\'t-u-n\',2:\'198cm\',3:\'97.9\'},{0:\'考瓦伊-莱昂纳德\',4:\'leonard.5\',1:\'1991-d-c\',2:\'201cm\',3:\'102.1KG\'},{0:\'达米安-利拉德\',4:\'lillard.5\',1:\'t-07-15\',2:\'q\',3:\'r.9\'},{0:\'卡梅罗-安东尼\',4:\'anthony.5\',1:\'k-v-c\',2:\'203cm\',3:\'108KG\'},{0:\'尼科拉-约基奇\',4:\'jokic.5\',1:\'w-u-19\',2:\'s\',3:\'128.8\'},{0:\'卡尔-安东尼-唐斯\',4:\'towns.5\',1:\'w-11-15\',2:\'p\',3:\'112.9\'},{0:\'克里斯-保罗\',4:\'paul.5\',1:\'1985-v-d\',2:\'185cm\',3:\'79.l\'},];new Vue({el:\'#app\',data:function(){x{h,a:\'NAhwcEVLEnRoJA7acv6eZGvXWjtijppyHXh\'}},methods:{getToken(y){e a=6.f.z.A(this.a);g{0,1,2,3}=y;e B=6.f.Base64.stringify(6.f.z.A(0));e C=6.DES.encrypt(`${B}${1}${2}${3}`,a,{D:6.D.ECB,padding:6.pad.Pkcs7});x C.toString()}}})', [], 40, 'name|birthday|height|weight|image|png|CryptoJS|03|8KG|5KG|key|1988|29|06|let|enc|const|players|208cm|9KG|1984|4KG|191cm|08|1994|211cm|188cm|88|213cm|1990|02|05|1995|return|player|Utf8|parse|base64Name|encrypted|mode'.split('|'), 0, {}))

可以看到 eval 中就是一个 js 自执行函数,有好多参数,但是基本上可以看出来一些数据,看起来只是做了一些字符串变换的操作,简单调试一下看看。

image-20250112200817628

在第一行打个断点,可以看到函数的参数,整个函数有几个函数组成,单步走一下。

image-20250112201319936
首先是定义了几个函数,看起来是对 r 变量进行填充,内容是通过 e 函数进行变换之后的内容,可以理解为解密或者解码。

解码完成以后,通过 replace 函数动态的替换单个字符,使用 r 变量的内容。

走到后面发现 p 变量就是之前我们看到的初始化的 js 代码了,这个 p 变量的内容会被当成 js 代码执行,流程就和之前是一样的了。但是要如何还原混淆过的字符串呢,我这里尝试使用 Python 来解码字符串试试。

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
49
50
51
52
53
54
55
56
57
58
59
60
import json
import re

import requests


# 转成 36 进制
def to_base_36(num):
digits = "0123456789abcdefghijklmnopqrstuvwxyz"
if num == 0:
return "0"
r = ""
while num > 0:
num, remainder = divmod(num, 36)
r = digits[remainder] + r
return r


# 原 js 的解码函数,稍微去掉了一些无用的逻辑
def ee(p, a, c, k, e, r):
def e1(cc):
result = ""
if cc >= 62:
result += e1(cc // 62)
cc %= 62
if cc > 35:
result += chr(cc + 29)
else:
result += to_base_36(cc)
return result

c -= 1
while c > 0:
c -= 1
r[e1(c)] = k[c]

return re.sub(r'\b[0-9a-zA-D]\b', lambda e: r.get(e.group(0), e.group(0)), p)


res = requests.get('https://spa9.scrape.center/')
res.encoding = 'utf-8' # 不加可能会乱码

js_content, c, k = re.findall(r"\}\('(.*?)',\[],(\d\d),'(.*?),0,\{}\)", res.text)[0]
# 提取出来的 js 解码函数的参数,调用自己实现的解码函数
decode_content = ee(js_content, [], int(c), k[:-11].split('|'), 0, {})

print(decode_content.replace('\\', ''))

result = re.findall('players=(\[[\s\S]+\])', decode_content.replace('\\', ''))[0] # type:str
json_content = json.loads(result.replace('\'', '"')
.replace('name', '"name"')
.replace('image', '"image"')
.replace('birthday', '"birthday"')
.replace('height', '"height"')
.replace('weight', '"weight"')
.replace('},]', '}]'))

for a in json_content:
print(a)

代码比较丑哈哈,因为要先将 eval 执行的 js 代码提取出来,然后使用 Python 方法来解码它,解码之后就是一堆 js 代码了,可以用 spa7 中的代码来提取数组内容,然后 json loads 加载遍历输出即可。

完整代码见:https://github.com/libra146/learnscrapy/tree/main/js

总结

这几个网站的难度不大,但是都比较繁琐,因为要使用正则的方式来各种提取 js 代码,还要使用 Python 重新实现 js 的解码函数,属实是比较难调试。不过这里可以考虑使用 AI,AI 可以将 js 代码直接翻译成 Python 代码,还是比较方便的。

或者可以直接使用 nodejs,可以更加方便的获取 js 执行的结果,不用使用正则提取数据了。

本文章首发于个人博客 LLLibra146’s blog
本文作者:LLLibra146
更多文章请关注:qrcode
版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!
本文链接
https://blog.d77.xyz/archives/498014e0.html