【WAX云钱包】Cloudflare反爬虫突破(SSL指纹识别)

WAX云钱包

在之前的多篇文章中,我们使用【Python】+【Selenium】来实现WAX链游脚本,主要是因为很多玩家一开始都是用WAX云钱包注册的账号,而WAX云钱包的私钥托管在云端,我们没法拿到私钥放到【eosjs】中去直接调用智能合约,所以最终的脚本需要借助【Selenium】和【Chrome】浏览器来运行,因为我们需要在【Chrome】中注入javascript去调用WAX云钱包提供的SDK【waxjs】,这个【waxjs】有一个【login】方法和一个【api.transact】方法,调用的时候会弹出一个授权窗口,来完成交易签署和提交。
在这里插入图片描述

正是由于这个小小的弹窗,【waxjs】没法直接脱离浏览器,在【Python】的【js2py】或【nodejs】中执行,自然就背上了【Chrome】这个沉重的玩意,使得一台电脑多开的数量大大减少。

为了无窗口、静默的运行脚本,我们当然可以使用【Anchor】钱包创建WAX原生账号,私钥拿在手里,就可以为所欲为,但是对于只支持WAX云钱包的链游,以及已有的WAX云钱包账号,还是得想办法来解决。

那么一个思路就是阅读【waxjs】的源代码,搞清楚它的【login】和【api.transact】方法,通过抓包和调试js搞清楚这个小弹窗做了什么事情,然后直接通过【Python】去发送HTTP请求来实现登录和签署交易的操作,也可以脱离【Chrome】来运行。

我们最近就在做这个事情,但是遇到了一些问题。

Sorry, you have been blocked

我们使用【Python】+【resquests】学着浏览器的样子给WAX云钱包服务端发送HTTP请求的时候,出现了【403 Forbidden】的错误,以下面简单的代码为例:

import requests
resp = requests.get("https://public-wax-on.wax.io/wam/sign")
print(resp.text)

正常情况下,这个GET请求应该返回一个json,可以在【Chrome】浏览器中或【Postman】中直接请求这个地址以验证这一点。
在这里插入图片描述
但是我们使用【Python】发送请求后,却收到了【403 Forbidden】错误,并且返回一个这样的页面:
在这里插入图片描述

Please enable cookies.

Sorry, you have been blocked

You are unable to access wax.io

Why have I been blocked?

This website is using a security service to protect itself from online attacks. The action you just performed triggered the security solution. There are several actions that could trigger this block including submitting a certain word or phrase, a SQL command or malformed data.

What can I do to resolve this?

You can email the site owner to let them know you were blocked. Please include what you were doing when this page came up and the Cloudflare Ray ID found at the bottom of this page.
 
Cloudflare Ray ID: 711e5eaf6819980c • Your IP: * • Performance & security by Cloudflare 

显然,WAX云钱包使用了Cloudflare做CDN加速,而我们被Cloudflare的反爬虫机制制裁了。

这种情况很常见,我们第一时间想到的就是【Python】发出的HTTP请求头和【Chrome】发出的请求头内容有差异,尤其是【User-Agent】,于是我们修改代码来模拟这个请求头:

import requests
client = requests.Session()
client.headers["User-Agent"] = "Mozilla/5.0"
resp = client.get("https://public-wax-on.wax.io/wam/sign")
print(resp.text)

但无济于事,还是被【403 Forbidden】制裁

于是我们直接上【Fiddler】抓包,分别用【Python】和【Chrome】发送请求,仔细检查每一个请求头的差异,并且不断的修改【Python】代码,让【Python】代码发出的HTTP请求和【Chrome】发出的请求完全一致,一个字符都不差,通常这种方法可以解决大部分的此类问题。

但奇怪的是,当我们开启【Fiddler】的HTTPS解密功能,并让【Python】代码走【Fiddler】代理访问后,居然就正常了!

import requests
client = requests.Session()
client.headers["User-Agent"] = "Mozilla/5.0"
client.proxies = {
        "http": "http://127.0.0.1:8888",
        "https": "http://127.0.0.1:8888",
    }
resp = client.get("https://public-wax-on.wax.io/wam/sign",verify=False)
print(resp.text)

此时【Python】代码发送的HTTP请求,正常拿到了json内容,而没有被【Cloudflare】用【403 Forbidden】拦截

What!难道是【Fiddler】作为中间人代理,修改了我HTTP请求的内容?或是调整了一些Header的顺序?或是修改了Header的内容?多年的经验告诉我,不应该呀,【Fiddler】代理默认是不可能修改HTTP请求中的任何内容的。

为了验证这一点,我们用【Python】写了一个简单的https server,来对比【Python】直接发HTTP请求与通过【Fiddler】发送HTTP请求的内容,究竟有什么差异。

import ssl
import socket
import hashlib

context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('test.pypig.com.pem', 'test.pypig.com.key')

with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
    sock.bind(('0.0.0.0', 443))
    sock.listen(5)
    with context.wrap_socket(sock, server_side=True) as ssock:
        while True:
            conn, addr = ssock.accept()
            data = conn.read()
            print(data)
            md5 = hashlib.md5()
            md5.update(data)
            print(md5.hexdigest())
            conn.send("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nHello, world!\r\n".encode())
            conn.close()

这个https server直接用ssl socket来写,打印出每个HTTP请求最原始的请求内容,同时给出md5值,以确定两次http请求的内容是否一个字节都不差。

然后用【Python】+【resquests】发起请求,先直接请求:

import requests
client = requests.Session()
client.headers["User-Agent"] = "Mozilla/5.0"
resp = client.get("https://test.pypig.com/sign")
print(resp.text)

然后挂【Fiddler】代理来请求:

import requests
client = requests.Session()
client.headers["User-Agent"] = "Mozilla/5.0"
client.proxies = {
        "http": "http://127.0.0.1:8888",
        "https": "http://127.0.0.1:8888",
    }
resp = client.get("https://test.pypig.com/sign", verify=False)
print(resp.text)

在这里插入图片描述
结果我们的微型https server告诉我们,挂不挂【Fiddler】代理,服务端收到的HTTP请求,一个字节都不差。

What! 那为什么挂上【Fiddler】代理,Cloudflare就不拦截我们了,直接访问,Cloudflare就要拦截我们?

在排除IP问题以后,我们可以确定我们发给Cloudflare的数据包,在HTTP协议层上的数据一个字节都不差,那么有可能的,就是HTTP协议层下面的数据包的差异了。

SSL指纹识别

经过一番搜索,我们发现了Cloudflare很有可能是通过SSL指纹识别来认定我们的HTTP请求,不是从正常的浏览器发出的,而是Python之类的脚本程序,从而拦截了我们。

SSL指纹是什么呢?简单来说,客户端发起https的请求的第一步就是向服务器发送tls握手请求,这其中就包含了客户端的一些特征,比如:

SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat

我们使用【Wireshark】来抓包看看究竟是什么特征:
在这里插入图片描述
我们可以看到,TLS握手阶段的【Client Hello】中包含了一些我们客户端SSL Library的特征,这取决于我们客户端用的openssl版本,编译openssl的时候选择支持的Cipher suite等信息。Cloudflare有一个SSL指纹特征库,再结合【User-Agent】很容易判断出你的这个HTTP请求,根本不是来自于【Chome】或【Firefox】浏览器,于是把你拒绝。

其实Cloudflare已经公开了他们的SSL指纹识别工具及特征库:
https://github.com/cloudflare/mitmengine

关于SSL指纹识别,这里有一些好的资料可以参考:
https://blog.cloudflare.com/monsters-in-the-middleboxes
https://github.com/salesforce/ja3
https://www.buaq.net/go-69547.html
https://ares-x.com/2021/04/18/SSL-%E6%8C%87%E7%BA%B9%E8%AF%86%E5%88%AB%E5%92%8C%E7%BB%95%E8%BF%87/

修改特征

很显然,我们的【Python】+【resquests】发起的HTTP请求被Cloudflare拦截就是因为Cloudflare已经记录了常见【Python】版本自带的【openssl】的特征。但由于我们下载使用的都是【Python】官方编译好的Release版本,我们已经没法去修改替换【Python】自带的的ssl library,一种可行的办法就是自己从源码重新编译【Python】,编译的时候自行调整【openssl】的版本和编译参数,但显然都太麻烦了,而且不利于代码的分发。

但我们找到了一些偏门的方法,间接的影响【Python】发出的HTTPS请求中的TLS握手特征,从而逃过Cloudflare的特征库识别。

方法一:

import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context

class CipherAdapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        context = create_urllib3_context(ciphers='DEFAULT:@SECLEVEL=2')
        kwargs['ssl_context'] = context
        return super(CipherAdapter, self).init_poolmanager(*args, **kwargs)

    def proxy_manager_for(self, *args, **kwargs):
        context = create_urllib3_context(ciphers='DEFAULT:@SECLEVEL=2')
        kwargs['ssl_context'] = context
        return super(CipherAdapter, self).proxy_manager_for(*args, **kwargs)

client = requests.Session()
client.mount('https://public-wax-on.wax.io', CipherAdapter())
client.headers["User-Agent"] = "Mozilla/5.0"
resp = client.get("https://public-wax-on.wax.io/wam/sign")
print(resp.text)

我们可以指定不同的SSL安全级别,来影响TLS握手【Client Hello】中的内容,从而改变特征。
使用上面的代码以不同的SSL安全级别来发送HTTP请求,然后用【Wireshark】来抓包对比观察差异:
在这里插入图片描述
分析两次TLS握手【Client Hello】中的内容,可以看到,修改了SSL安全级别后,很多特征发生了变化,尤其是密码套件【Cipher Suite】的数量都发生了变化,而【Cipher Suite】的特征,正是Cloudflare记录的特征之一,另外一些【SSLExtension】也发生了变化,这讲导致我们修改后的SSL指纹特征发生较大变化,和Cloudflare的指纹库匹配不上,自然就把我们放过了。

方法二:

import requests
import cloudscraper

scraper = cloudscraper.create_scraper(browser={'browser': 'firefox', 'platform': 'windows', 'mobile': False})
resp = scraper.get("https://public-wax-on.wax.io/wam/sign")
print(resp.text)

另一种方法是直接使用别人写好的反检测对抗库
【cloudscraper】:https://github.com/VeNoMouS/cloudscraper

使用前需要先安装:

pip install cloudscraper

另外,还有一个类似的开源库也是做这个事情的:
【cloudflare-scrape】:https://github.com/Anorov/cloudflare-scrape

这两个库都很强大,他们最有用的部分还不只是过SSL指纹识别检测,而是处理一些需要执行JavaScript的反机器人页面,以及一些识别人类还是机器人的验证码。

终极方案

上面的方法对这个地址是有效的:

https://public-wax-on.wax.io/wam/sign

但很多读者并不是要访问这个地址,很多读者是在开发其它网站的爬虫的时候,遇到了这个Cloudflare反爬问题。由于不同的站点使用的Cloudflare防火墙的反爬参数和级别各不相同,有的站点即使使用【cloudscraper】仍然无法解决问题。

那么,有两个解决该问题的思路是可行的:

1.【selenium】:我们可以借助selenium启动一个Chrome浏览器(甚至可以以headless模式启动,隐藏Chrome窗口),然后自己写一个简单的HTML文件,把 axios 库导入进来,写几行javascript简单包装一下,比如写两个function,一个get一个post,然后借助selenium,用 python 调用这个 get / post 方法即可。此方案本质上相当于 python 调用 javascript 调用 axios,然后通过 Chrome 浏览器发出 HTTP 请求,这样SSL指纹完全就是Chrome的指纹,Cloudflare检查不出区别。此方案我已经验证过可行,只是一个进程需要启动一个Chrome,稍有些臃肿和低效,但是在非商用场合已经足够了。

这个思路已经有一个开源项目做好了,开箱即用:
【undetected-chromedriver】https://github.com/ultrafunkamsterdam/undetected-chromedriver

2.【WinHTTP】:思考,Cloudflare不会墙Chrome,当然也不会墙 IE 和 Edge ,那么 IE 和 Edge 的底层 HTTP 库是什么?当然就是 WinHTTP。我们没有时间精力从Chrome中提取出底层的 HTTP 库或 SSL 库来为我所用,但是微软的IE 和 Edge底层的这套东西,是提供SDK给你用的啊。

如果我们不用python的requests来发送HTTP请求,而是在windows平台上使用WinHTTP来发送HTTP请求,再带上IE 的 user-agent,那么对于Cloudflare来说,不就无法分辨是不是IE发出的请求。

此方案我们已经验证过可行,得益于早年间经常使用C++开发WEB爬虫,经常使用WinHTTP这个库,比较熟悉,很快就上手了。但由于时间精力有限,暂没有时间把它封装成Python模块。所以需要大家自行处理了。

一个思路是借助pywin32来调用WinHTTP的C/C++ API,一个思路是仍然是借助pywin32,但调用WinHTTP的COM接口,COM接口比较适合脚本语言调用,处理类型转换问题比Python调用C/C++ API接口简单。另一个思路是写C++代码,把WinHTTP的接口包装一下,通过pybind11编译成一个python模块,这样对python用户来说是最友好的,到时候发布到pypi上让广大开发者收益,只可惜我们暂无时间精力来做这个事情。

WinHTTP文档:https://learn.microsoft.com/en-us/windows/win32/winhttp/about-winhttp
两种接口:https://learn.microsoft.com/en-us/windows/win32/winhttp/choosing-a-winhttp-interface
pybind11:https://pybind11.readthedocs.io/en/stable

顺利进行

至此,我们脱离【waxjs】直接和WAX云钱包服务端交互来登录和签署交易的脚本得以顺利进行,成功脱离【Chrome】等浏览器,直接使用WAX云钱包账号来签署交易。提高了脚本运行效率,降低了内存占用,使得一台电脑上的多开数量大大增加。

  • 15
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 22
    评论
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值