任何动态的事物都是基于静态产生的,所以只要找到静态点,那么动态就会坍缩为静态。

刚入行的时候,从https://www.52pojie.cn/thread-1208999-1-1.html 里知道了这句,顿时惊为天人,感悟良多。

滑块只能seleinum过,大环境就这样

这句是来自金角大王的,老实说滑块用自动化过是真舒服,但前提是得解决自动化检测,当遇到难的反爬时,不仔细分析代码都不知道咋检测的。

嘲讽金角,理解金角,成为金角,超越金角

金角就是上面的金角大王,也泛指自动化。刚入行时看不上自动化,现在越来越觉得自动化舒服。之前遇到阿里v2滑块时,搞代码得好久,自动化解决下轨迹就完事了。

不是学了点ast基础你就能搞出某验,某数。。能搞出来的不用ast也能搞出来,甚至不用花时间去写复杂的还原控制流等

AST是真舒服,刚入行的时候都是对着混淆代码调试的,人都调试麻了。后来从蔡老板那里学了AST还原混淆之后,调试起来舒服的很,极大提高生产力。所以这句话就挺莫名其妙。

因为你的不礼貌,我说说我的想法。你我只是开发领域不一样,但你尽然不能理解作为程序员时间成本概念我到感到很惊讶我很清楚我的需求是什么才来联系你,我有自己非常擅长的领域,所以我没那么多时间去自学-门很成熟的技术,我要的是最快时间能解决我的需求,达到我的目的。手上有一堆项目技术问题需要处理,联系你只是一个偶然,你自己看低你的客户价值,那是你自己问题。如果你的爬虫技术满足我们项目的需求,我更愿意你卖接口服务的方式深度合作。既然你关上了让我了解你的门,那没必要聊下去。我想说你299的价格,我一个月薪水可以买断你好多年提供的教程,不可能花精力去深度学习你的经验。这个思想我在开始跟你聊就委婉表达出来,只是你没看懂。你的无知应该是最可笑的。

这句话估计在爬虫圈流传最广。

联系作者

最近听道友说一个颇具难度的某数样本又更新了,补环境跑着跑着通过率会变得很低,不知道错在哪里。于是只好尝试算法方案,尝试之后发现生成核心45位数组的算法有一点变化,之前的方法不能用了,而因为之前没有认真跟过这部分代码,一时半会估计是搞不定了,于是只好先请大佬出山。

跑起来之后,还是得自己研究下这部分逻辑,发现关键点还是一个8位数组,也就是时光在瑞数vmp算法还原流程分析里生成wIlwQR28aVgbT参数的大概步骤里步骤4中说到的8位数组,从打印的日志可以看到这个数组会从 [0, 1, 2, 3, 4, 5, 6, 7] 经过一系列运算生成如 [6, 155, 3, 0, 5, 2, 7, 4] 这样的数组。生成的逻辑在vmp运算里,因为不会还不会反编译vmp,调试了老半天都看不出来生成逻辑,头皮发麻。

后续有时间了打算尝试用补环境方案生成核心45位数组后给算法方案使用,希望能成。也可以排查下补环境为啥会从通过率100%变成20%,解决了这个问题就更舒服了。

不得不感叹,在没有进行vmp反编译的情况下,分析日志还原vmp算法的大佬都太有毅力了。

联系作者

听圈子里的人说,现在都人均rs了,rs不再像20年那会那样,像一座大山。但我看了一些补环境方案,发现很多补环境方案连ActiveXObject对象还没搞明白,只是走了神奇的window.ActiveXObject=undefined 这个bug, 这种方案只能说能过,所以对人均rs我有些怀疑。

问了公司的配置开发,他们能解决4代和5代,不能解决vmp版本。公司的配置开发能力已相当不错, 会解决常用加密如AES,DES等,一些入门加密如 jsl, hexin-v也能做,有一些能力强的还学了下AST,但遇到vmp版本时就没法拿捏。

有一些人用了开源的sdenv, 不不的cynode, 挽风的node-sandbox,就觉得rs简单,但这只是因为工具的能力,并不代表自己会,得知其然,知其所以然才行。

就拿sdenv来说,如果你只会用sdenv, 万一遇到 aHR0cDovL2Nob25ncWluZy5jaGluYXRheC5nb3YuY24veHhna3h0L3BhZ2VzL3F5d2gvemR3ZmFqY3guaHRtbCAK,aHR0cDovL3d3dy5jemNlLmNvbS5jbi9jbi9qeXNqL3lkanloei9INzcwMzE1aW5kZXhfMS5odG0K 等站点怎么办?更不用说aHR0cHM6Ly96eGdrLmNvdXJ0Lmdvdi5jbi94Z2wvCg==,aHR0cHM6Ly93d3cuY2Vid20uY29tL3dlYWx0aC9qZ2xjMzkvaW5kZXguaHRtbCA=等站点了。

联系作者

之前在神奇的window.ActiveXObject=undefined里写过,加上window.ActiveXObject=undefined后,某数的校验就降级了,很多环境校验就都没走了。但神奇的是,在我的一个补环境方案里,去掉这行后,很多环境校验一样没走,一样能过反爬,于是我懵,我困惑了,这到底是什么鬼bug啊。

去网上查了下ActiveXObject这个对象,仅支持微软 Internet Explorer 浏览器,在其它现代浏览器(如 Chrome、Firefox、Safari 等)中无法使用,包括微软的 Edge 浏览器(Chromium 内核)也无法使用。也就是说,只有在微软 Internet Explorer 浏览器里,’ActiveXObject’ in window才是true, 在其它现代浏览器里,’ActiveXObject’ in window 都是false。

而在补环境的时候,userAgent 是Chrome, Edge等浏览器时,如果加上 window.ActiveXObject = undefined; 这行,’ActiveXObject’ in window 就会变成true了,就不符合上述的结果,是可以被检测出来的。

很多人补环境不严格,补环境代码里有 window.ActiveXObject = undefined; userAgent是 Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0,这种是Chromium 内核的,换成是我做反爬,就要干它了,悄咪咪的把它记下来,然后在夜深人静的时候给它返回点假数据。

参考资料:

https://webaim.org/blog/user-agent-string-history/

联系作者

一个月前有道友反馈,过不去aHR0cHM6Ly93d3cuY2Vid20uY29tL3dlYWx0aC9qZ2xjMzkvaW5kZXguaHRtbCA=这个样本获取token的接口,于是掏出祖传补环境代码去测试,首页能过,接口竟然过不去,于是把所有的备用方案拿去测试,竟然都过不去,我惊了,这是难得一见的补环境过不去的网站了。

此时只好拿出算法方案,意外发现以前用的找20取4位数组的方法竟然失效了。于是拿出尘封已久的AST代码,也没办法还原控制流混淆了,估计是控制流混淆有点变化。于是索性不还原,直接在vmp循环里打日志慢慢调试,搞了一天,愣是没搞定。

于是求助小黄鱼大佬们,问了一圈补环境方案,竟然没找到一个能搞定的,看来大家的补环境水平都差不多了。于是求助算法大佬们,终于找到解决的办法。其实算法没变化,只是我用的20取4位数组的办法还不够通用,在这个站点上正好失效了。

有了算法方案后,对比之后发现这个站点上之所以过不去是因为校验指纹数组了,于是想起2年前曾研究过这个指纹数组,找到2年前的补环境,发现竟然能过,收工。

头疼的是,第二天补环境就要跑不动了,测试通过率只有60%,算法方案才能稳稳的跑通。结果前几天,补环境和算法方案都跑不动了,仔细一看,url加密,表单加密,响应加密都上了,这属实是全家桶了,头皮发麻。

联系作者

一般反爬在生成加密参数的时候,都会取navigator里的userAgent属性,然后生成加密参数。在使用requests库的时候,爬虫也会设置下headers里的User-Agent值。但很多时候爬虫并没有保持这两个值一致。

在使用补环境生成参数的时候,如果要支持动态传入userAgent,就得在调用参数生成接口的时候增加userAgent参数,并传到补环境代码里设置navigator.userAgent,爬虫偷懒的时候就会省掉这一步,直接固定写死navigator.userAgent。

还有在使用jsdom补环境生成参数的时候,会错误设置userAgent。如果像以下代码一样设置userAgent就是错的

1
2
3
4
5
6
7
8
9
10
11
const { JSDOM } = require('jsdom');

// 自定义的 userAgent 字符串
const myUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3';

// 创建 jsdom 实例并设置 userAgent
const dom = new JSDOM(``, {
userAgent: myUserAgent
});

console.log(dom.window.navigator.userAgent); // 输出类似Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/19.0.0 这种包含jsdom的userAgent

jsdom里正确的做法是使用ResourceLoader设置userAgent

1
2
3
4
5
6
7
8
9
10
11
12
13
const jsdom = require("jsdom");  // 引入 jsdom
const { JSDOM } = jsdom; // 引出 JSDOM 类, 等同于 JSDOM = jsdom.JSDOM
const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
const resourceLoader = new jsdom.ResourceLoader({
userAgent: userAgent
});
const dom = new JSDOM(``, {
url: "https://example.org/",
referrer: "https://example.com/",
resources: resourceLoader,
});
window = dom.window
console.log(window.navigator.userAgent) // 输出是Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36

有一些反爬在不严格的时候即便userAgent不一致也让爬虫通过,但风控严格的时候userAgent不一致又不让爬虫通过,让爬虫摸不着头脑,得排查老半天。所以爬虫为了稳妥起见,还是得保持navigator里的userAgent属性和headers里的User-Agent值一致。

联系作者

通常我们写好一个接口后,需要做接口性能测试,知道接口的QPS(Queries Per Second),百分位响应时间,我们可以用Python库Locust来做

pip install locust库后,新建locustfile.py,写入要测试的接口,demo如下

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
from locust import HttpUser, task, between
import json

class MyUser(HttpUser):
# 设置用户的等待时间(模拟用户在操作之间的思考时间)
wait_time = between(1, 2) # 每次任务之间等待 1~2 秒

@task
def post_request(self):
# 定义请求的 URL 和数据

url = 'https://www.baidu.com'

payload = {
"url": url,
}
headers = {
"Content-Type": "application/json"
}

# 发起 POST 请求
with self.client.post('http://%s/api/generate_cookies' % '127.0.0.1:5009', json=payload, headers=headers, catch_response=True) as response:
# 验证响应状态码是否为 200
if response.status_code == 200:
try:
# 如果需要验证返回内容,可以解析 JSON 数据
response.success() # 标记请求成功
except ValueError:
response.failure("Response is not valid JSON")
else:
# 如果状态码不是 200,标记请求失败
response.failure(f"Status code: {response.status_code}")

locust -f locustfile.py –web-port=8089,之后浏览器里打开 http://localhost:8089/ 就可以新建一个测试

测试结果中,RPS就是我们常说的QPS,95%ile 指的是 95百分位响应时间 (Percentile Response Times),也就是95%的请求的响应时间不超过某个值。

联系作者

JavaScript逆向时一些奇奇怪怪的检测里,我们写过一个使用Node v8内置函数来过 document.all 补环境检测的demo, 代码如下

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
const v8 = require("v8");
const vm = require("vm");

// 允许使用 V8 内置函数(需启用 --allow-natives-syntax 标志)
v8.setFlagsFromString('--allow-natives-syntax');

// 创建不可检测对象
let undetectable = vm.runInThisContext('%GetUndetectable()');

// 恢复标志禁用(可选)
v8.setFlagsFromString('--no-allow-natives-syntax');


function HTMLAllCollection() {
return undetectable
};
Object.defineProperties(HTMLAllCollection.prototype, {
[Symbol.toStringTag]: {
value: 'HTMLAllCollection',
configurable: true
}
});
undetectable.__proto__ = HTMLAllCollection.prototype;
document = {}
document.all = new HTMLAllCollection()

length = 3;
for (let i = 0; i < length; i++) {
document.all[i] = '1';
}
debugger;
document.all.length = length;
console.log(typeof document.all)
console.log(document.all)
console.log(document.all.length)

有道友说这种检测已经算简单了,可以回去看看document.all(), 测试了下这函数也没啥啊,就返回null, 后来去看了下document.all的特性,才发现它有和form表单一样的特性,例子如下

1
2
3
4
5
6
7
8
9
10
var input = document.createElement('input')
input.name = 'aaa'
document.body.append(input)
input = document.createElement('input')
input.id = 'bbb'
document.body.append(input)
document.all.aaa
document.all('aaa')
document.all.bbb
document.all('bbb')

这就令人头秃了。想来安全人员为了兼容还是留了一手了,要不然真的难搞了。以前刚学补环境的时候,就像拿到个锤子,总是锤来锤去,后面锤多了才发现越来越难锤,慢慢就回归到自动化了。毕竟算法,补环境,自动化都是工具,什么好用就用啥了。

联系作者

平时使用requests和gevent写爬虫的时候,是如下demo, 在代码最前面加上monkey.patch_all()即可

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
import gevent
from gevent import pool, monkey
monkey.patch_all()
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import time
import requests
from gevent.lock import RLock

key_lock = RLock()
key = 0


def get_key():
global key
key_lock.acquire()
key += 1
key_lock.release()
return key

def test_request():
print("start ")
time.sleep(2)
print(get_key())
session = requests.session()
response = session.get('https://www.baidu.com', verify=False, timeout=10)
print(response.status_code, response.url)
time.sleep(3)
print('end')


if __name__ == "__main__":
n = 3
gevent_poll = pool.Pool(n)
jobs = []
for i in range(n):
job = gevent_poll.spawn(test_request)
jobs.append(job)
gevent.joinall(jobs)

输出结果里 0 1 2 是连续的, 请求是并发的

1
2
3
4
5
6
0
1
2
200 https://www.baidu.com/
200 https://www.baidu.com/
200 https://www.baidu.com/

为了过ja3指纹,我们可以使用curl_cffi这个库,demo如下

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
import gevent
from gevent import pool, monkey
monkey.patch_all()
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import time
import requests
from curl_cffi import requests as cffi_requests
from gevent.lock import RLock

key_lock = RLock()
key = 0


def get_key():
global key
key_lock.acquire()
key += 1
key_lock.release()
return key

def test_request():
print("start ")
time.sleep(2)
print(get_key())
session = cffi_requests.Session()
response = session.get('https://www.baidu.com', verify=False, timeout=10)
print(response.status_code, response.url)
time.sleep(3)
print('end')


if __name__ == "__main__":
n = 3
gevent_poll = pool.Pool(n)
jobs = []
for i in range(n):
job = gevent_poll.spawn(test_request)
jobs.append(job)
gevent.joinall(jobs)

但这样并不能实现并发,输出结果里0 1 2是分开的,请求是顺序的

1
2
3
4
5
6
0
200 https://www.baidu.com/
1
200 https://www.baidu.com/
2
200 https://www.baidu.com/

解决办法是在创建session时加上thread参数,指定gevent即可,像session = cffi_requests.Session(thread=’gevent’)这样创建即可,最终代码如下

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
from gevent import pool, monkey
monkey.patch_all()
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
import time
import requests
from curl_cffi import requests as cffi_requests
from gevent.lock import RLock

key_lock = RLock()
key = 0


def get_key():
global key
key_lock.acquire()
key += 1
key_lock.release()
return key

def test_request():
print("start ")
time.sleep(2)
print(get_key())
session = cffi_requests.Session(thread='gevent')
response = session.get('https://www.baidu.com', verify=False, timeout=10)
print(response.status_code, response.url)
time.sleep(3)
print('end')


if __name__ == "__main__":
n = 3
gevent_poll = pool.Pool(n)
jobs = []
for i in range(n):
job = gevent_poll.spawn(test_request)
jobs.append(job)
gevent.joinall(jobs)

结果里0 1 2 是顺序的,请求是并发的。

1
2
3
4
5
6
0
1
2
200 https://www.baidu.com/
200 https://www.baidu.com/
200 https://www.baidu.com/

这又是一个小细节,被坑了。正常以为会像requests库一样用,但结果不是。文档 https://curl-cffi.readthedocs.io/en/latest/advanced.html#using-with-eventlet-gevent 里有说到这个用法,但没仔细去看了,这个库API和requests太像了,以为可以无缝衔接。

联系作者

JavaScript逆向时一些奇奇怪怪的检测

之前和同事讨论过一些检测,同事说可以记录下来,能帮到新人,于是有了这篇文章。

document.all

这是在浏览器里被特殊处理的一个特性,也就是typeof document.all是undefined, 但却document.all却能取到值。要在Node中实现这个特性,现在有两种方案,一种是Node C++插件,另一种是V8内置函数。一般场景用V8内置函数即可,移植性更高。Node C++插件和系统有关,也和Node版本绑定,移植性差。

要实现这个特性,GPT给出如下答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const v8 = require("v8");
const vm = require("vm");

// 允许使用 V8 内置函数(需启用 --allow-natives-syntax 标志)
v8.setFlagsFromString('--allow-natives-syntax');

// 创建不可检测对象
let undetectable = vm.runInThisContext('%GetUndetectable()');

// 恢复标志禁用(可选)
v8.setFlagsFromString('--no-allow-natives-syntax');

// 测试对象行为
undetectable.aaa = 'bbb';
console.log('typeof undetectable:', typeof undetectable); // 输出 'undefined'
console.log('undetectable.aaa:', undetectable.aaa); // 输出 'bbb'
console.log('undetectable instanceof Object:', undetectable instanceof Object); // 输出 'true'

修修改改,即可实现document.all

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
const v8 = require("v8");
const vm = require("vm");

// 允许使用 V8 内置函数(需启用 --allow-natives-syntax 标志)
v8.setFlagsFromString('--allow-natives-syntax');

// 创建不可检测对象
let undetectable = vm.runInThisContext('%GetUndetectable()');

// 恢复标志禁用(可选)
v8.setFlagsFromString('--no-allow-natives-syntax');


function HTMLAllCollection() {
return undetectable
};
Object.defineProperties(HTMLAllCollection.prototype, {
[Symbol.toStringTag]: {
value: 'HTMLAllCollection',
configurable: true
}
});
undetectable.__proto__ = HTMLAllCollection.prototype;
document = {}
document.all = new HTMLAllCollection()

length = 3;
for (let i = 0; i < length; i++) {
document.all[i] = '1';
}
debugger;
document.all.length = length;
console.log(typeof document.all)
console.log(document.all)
console.log(document.all.length)

eval(‘!new function(){eval(“console.log(this);this.a=2”)}().a’)

这个主要用来检测vm2环境,在浏览器和Node里都是false, 在vm2里是true, 测试代码如下

1
2
3
4
5
6
7
8
9
10
11
const { VM } = require("vm2");
const vm = new VM();

vm.run(`
const obj = new function() {
eval("console.log(this);this.a=2");
}();
console.log(obj); // 输出空对象 {}
console.log(obj.a); // 输出 undefined
console.log(!obj.a); // 输出 true
`);

window[“Object”][“getOwnPropertyNames”](Function.prototype.toString)

这个主要考察严格模式和非严格模式, 正常用以下方法重定义

1
2
Function.prototype.toString  = function toString() {};
console.log(Object.getOwnPropertyNames(Function.prototype.toString));

会返回[‘length’, ‘name’, ‘arguments’, ‘caller’, ‘prototype’]

用以下方法重定义

1
2
Function.prototype.toString = {toString(){}}.toString
console.log(Object.getOwnPropertyNames(Function.prototype.toString));

则返回 [‘length’, ‘name’]

form表单特性

1
2
3
4
5
6
7
8
9
10
11
12
let form = document.createElement('form');
form.action = 'https://www.baidu.com/';
let input = document.createElement('input');
input.name = 'action';
form.appendChild(input)
input = document.createElement('input');
input.name = 'textContent';
input.id = 'password';
form.appendChild(input)
console.log(form.action)
console.log(form.textContent)
console.log(form.password)

正常form.action是一个url链接,但这里变成了HTMLInputElement。

参考资料

零点的 JavaScript toString 检测对抗

联系作者