某点爬虫

前言:本人对各方向的知识了解浅薄,所以想通过实战来借此学习一些知识,因此在本篇的过程中,本人会较为详细地介绍各个流程与细节点,并在实战过程中进行自学习

实际上,对于小说网站的实际爬虫应用并没有我下面的如此复杂,一些现成的开源爬虫工具已经满足了大部分的需求。但是为了追求技术上的提升,我们还是按照游戏规则来进行

抓包分析

首先我们任意打开一本小说,并对其进行抓包,可以发现关键的信息只有前三条,当打开第三条请求的响应体,可以发现包含文本的节点已经被返回,因此我们只需要对这三次请求进行分析

image-20260120172330081

第一条请求的响应是202,也就是接收到了请求,但是需要进一步处理,这里可以看到加载了一个名为probe.js的文件,该文件是我们后面分析的重点

image-20260120172624572

第二条请求则是向probe.js发送了请求,返回了js代码,很明显有js混淆

image-20260120172953314

第三条请求,可以发现请求头中携带了一个陌生的数据w_tsfp,而返回中则包含我们需要的文本,通过提取就可以完成我们的目标

image-20260120173517716

很明显我们的任务就关注在了probe.js上,目标应该是如何通过分析该文件后,自动生成w_tsfp参数

分析probe.js的主干部分

在这里重点推荐这位大佬的帖子jsvmp编译与反编译详解 (3)——某讯新版vmp反编译 - dream的小站,因为我们后续的核心也是对vmp进行反编译,而我们所获得的probe.js代码与这位大佬演示过程中的代码是一致的,除了符号由于混淆存在不同。大树底下好乘凉

我们将获得的probe.js文件:

  1. 保存在本地,你可以手动进行复制粘贴,或者直接在浏览器的Network中找到并保存,或者在抓包软件中进行保存
  2. 进行格式化,我这里是使用Babel将其进行了转换
  3. 按照函数进行折叠,vscode中的折叠函数快捷键为(Ctrl + K, Ctrl + 0)

最终可以得到如下一系列函数,我们可以看到最后是调用了

1
__TENCENT_CHAOS_VM("xxx", false)(13447, [], window, ["Promise", void 0, "apply", ...], void 0)();

image-20260120174947385

这里可以将连续调用进行拆解

1
2
3
var a = __TENCENT_CHAOS_VM("xxx", false) # 首先通过__TENCENT_CHAOS_VM执行得到一个函数a
var b = a(13447, [], window, ["Promise", void 0, "apply", ...], void 0) # 然后通过执行函数a得到另一个函数b
b() # 最终执行函数b

而又因为var __TENCENT_CHAOS_VM = function () ,可以得知__TENCENT_CHAOS_VM的实际函数为最终的return g(g,I)

image-20260120184828735

我们再跟踪进g函数,注意,由于代码混淆,所以可以看到上面也存在g函数,而我此时指的是箭头所指的g函数。部分符号存在是为了混淆,所以请读者包括后续分清我所代指的符号

这里进入到g(g,I)函数中后可以发现,存在两个新的 g(g, I, o, Y, r)函数,分别赋值给了Ca,可以看到此时的 g(g, I, o, Y, r)函数的参数为五个,这与第二段中的参数列表吻合(13447, [], window, ["Promise", void 0, "apply", ...], void 0)

g(g,I)函数函数的返回值由 return I ? C : a;进行控制,而Ig(g,I)传入的参数,此时传入的为false,也就是返回a所对应的函数,那么Ca函数有什么区别呢

image-20260120185253503

可以通过文本比对工具发现,两者仅有"use strict";的区别,即是否开启js的严格模式

image-20260120190057973

至此,整个对于__TENCENT_CHAOS_VM函数的调用逻辑已经清晰了:

1
2
执行__TENCENT_CHAOS_VM自身的初始化函数 -> 嵌套传参调用第一个函数("xxx", false)
-> 嵌套传参调用第二个函数(13447, [], window, [...], void 0) -> 最终执行返回值

分析probe.js的枝叶部分

既然我们已经理顺了probe.js的调用流程,于是我们开始跟着主干的调用流程,分析每个子步骤中的函数与功能,可以看到在__TENCENT_CHAOS_VM中有四个函数与四个变量,我们开始逐一进行分析

image-20260120191116059

g函数

该函数接收三个参数,最终返回一个由A起始,总共有B步,每步步长为g的等差数组。最终生成的数学表达式如下

[ A₀ + g, A₀ + 2g, A₀ + 3g, ..., A₀ + B*g ]

1
2
3
4
5
6
7
8
var g = function A(A, B, g) { //A是基
var I = [],
E = 0;
while (E++ < B) {
I.push(A += g);
}
return I;
};

I函数

base64编码,但注意此时的B并没有被使用,而后面使用的是外部变量a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var I = function A(A) {
var B = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("");
var g = String(A).replace(/[=]+$/, ""),
I = g.length,
E,
C,
Q = 0,
G = 0,
o = [];
for (; G < I; G++) {
C = a[g.charCodeAt(G)];
~C && (E = Q % 4 ? 64 * E + C : C, Q++ % 4) ? o.push(255 & E >> (-2 * Q & 6)) : 0;
}
return o;
};

E函数

ZigZag编码

1
2
3
var E = function A(A) {
return A >> 1 ^ -(1 & A);
};

C函数

Varint编码

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
var C = function A(A) {
var B = [];
var g = "undefined" != typeof Int8Array ? new Int8Array(I(A)) : I(A);
var C = g.length;
var Q = 0;
while (C > Q) {
var G = g[Q++];
var a = 127 & G;
if (G >= 0) {
B.push(E(a));
continue;
}
G = g[Q++];
a |= (127 & G) << 7;
if (G >= 0) {
B.push(E(a));
continue;
}
G = g[Q++];
a |= (127 & G) << 14;
if (G >= 0) {
B.push(E(a));
continue;
}
G = g[Q++];
a |= (127 & G) << 21;
if (G >= 0) {
B.push(E(a));
continue;
}
G = g[Q++];
a |= G << 28;
B.push(E(a));
}
return B;
};

当然,我们此处可以不用对这些编码进行深入分析,合理猜测这部分是将传入的数据进行解码

此处的a对应了上面的自定义编码表,由g函数辅助生成,然后是一些变量的初始化,一直最后虚拟机启动的函数h()

image-20260121194301760

可以看到此时的E作为字节码数字,而F充当PC

image-20260121194428616

因此在前面的解码部分,实际上就是解码得到了我们的初始化输出,于是我们便可以直接使用他原来的代码制作成接口,将得到的数据进行返回即可。接下来我们将重点关注虚拟机部分

image-20260121194851054

分析VMP

首先我们将原本的switch-case逻辑转变为函数调用的逻辑,这里使用@babel/traverse · Babel来设定规则并提取函数

  • 函数提取
  • 增加翻译
  • 将原本字节码翻译成自定义的汇编
  • 通过程序运行状态验证汇编是否正确
  • 优化汇编 APPCH优化

本文作者:River Cygnus

本文链接:http://example.com/2026/02/08/Web/%E7%88%AC%E8%99%AB/%E6%9F%90%E7%82%B9%E7%88%AC%E8%99%AB/

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

ESC 关闭 | 导航 | Enter 打开
输入关键词开始搜索