-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
205 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
--- | ||
title: Go 绕过 Cloudflare 防护 | ||
date: 2024-01-16 | ||
updated: 2024-01-16 | ||
tags: | ||
- 逆向 | ||
--- | ||
|
||
书接上文,在用了简单的界面模拟后chatgpt用是能用了,但受限制条件太多,终究不是长久之法,还是比不上逆向接口来的高效实用。经过一番折腾最后成功实现接口的调用,这期就来先探究下为何网页操作可以,而在代码里模拟接口却被403拒绝的问题。 | ||
|
||
<!--more--> | ||
|
||
## TLS Fingerprinting | ||
|
||
上文说到逆向受阻主要是受到 Cloudflare 的反爬虫防护,经过查找发现 [tls-client](https://github.com/bogdanfinn/tls-client) 这个库竟然可以绕过 Cloudflare,在一番上手使用后效果是有的,但有几点让我极其难以忍受 | ||
|
||
- 对 `net/http` 侵入性过强,丧失对标准库 `net/http` 使用 | ||
- 构建出的程序体积上大了好几兆,触发了我的强迫症 | ||
|
||
于是继续探究这个库究竟用了什么魔法绕过 Cloudflare。在其 Readme.md 有这么一段内容引起了我的注意 | ||
|
||
> Some people think it is enough to change the user-agent header of a request to let the server think that the client requesting a resource is a specific browser. Nowadays this is not enough, because the server might use a technique to detect the client browser which is called TLS Fingerprinting. | ||
这段内容大概意思是:仅仅用 `User-Agent` 来伪装浏览器是不够的,服务器可能会通过 `TLS 指纹识别` 来判断请求。随后引用了 [TLS 指纹识别如何工作](https://httptoolkit.tech/blog/tls-fingerprinting-node-js/#how-does-tls-fingerprinting-work)这篇文章来介绍 `TLS 指纹识别`。 | ||
|
||
根据文章的说法在 TLS 握手期间 `Client Hello` 会发送大量的客户端信息,这些的信息会被防护厂家做甄别过滤,防止恶意访问,信息的详细解读可以参考 [tls13.xargs.org](https://tls13.xargs.org) ,目前常用的客户端标记方法是 [ja3](https://github.com/salesforce/ja3#how-it-works),它通过计算下列信息的 MD5 来标识 | ||
|
||
``` | ||
SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat | ||
``` | ||
|
||
知道大概的原理了,那么 tls-client 是如何解决的呢?查看 [tls-client的依赖](https://github.com/bogdanfinn/tls-client/blob/master/go.mod) 会发现了一个关于 tls 的依赖 `github.com/bogdanfinn/utls`,去查看 `bogdanfinn/utls` 介绍里写着 Fork 的 `https://gitlab.com/yawning/utls`,再去看 `yawning/utls` 好家伙这介绍里写的 Fork 的 `https://github.com/refraction-networking/utls`,去到 `refraction-networking/utls` 这回对了,star 数和介绍都相当到位,这个才是真正处理 `TLS 指纹识别`。 | ||
|
||
关于客户端请求的代码在[官方示例](https://github.com/refraction-networking/utls/blob/master/examples/old/examples.go)有详细介绍,这里仿照写一段测试代码 | ||
|
||
```go | ||
func TestUtls(t *testing.T) { | ||
req, _ := http.NewRequest(http.MethodGet, "https://chat.openai.com/", nil) | ||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") | ||
config := tls.Config{ServerName: req.URL.Hostname()} | ||
// 设置网络魔法 | ||
u, _ := url.Parse("socks5://127.0.0.1:1080") | ||
d, _ := proxy.FromURL(u, proxy.Direct) | ||
dialConn, err := d.Dial("tcp", "chat.openai.com:443") | ||
if err != nil { | ||
return | ||
} | ||
uTlsConn := tls.UClient(dialConn, &config, tls.HelloChrome_Auto) | ||
defer uTlsConn.Close() | ||
|
||
err = uTlsConn.Handshake() | ||
if err != nil { | ||
return | ||
} | ||
resp, err := httpGetOverConn(req, uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) | ||
if err != nil { | ||
return | ||
} | ||
val, _ := httputil.DumpResponse(resp, true) | ||
fmt.Println(string(val)) | ||
} | ||
// 处理请求 | ||
func httpGetOverConn(req *http.Request, conn net.Conn, alpn string) (*http.Response, error) { | ||
switch alpn { | ||
case "h2": | ||
req.Proto = "HTTP/2.0" | ||
req.ProtoMajor = 2 | ||
req.ProtoMinor = 0 | ||
|
||
tr := http2.Transport{} | ||
cConn, err := tr.NewClientConn(conn) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return cConn.RoundTrip(req) | ||
case "http/1.1", "": | ||
req.Proto = "HTTP/1.1" | ||
req.ProtoMajor = 1 | ||
req.ProtoMinor = 1 | ||
|
||
err := req.Write(conn) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return http.ReadResponse(bufio.NewReader(conn), req) | ||
default: | ||
return nil, fmt.Errorf("unsupported ALPN: %v", alpn) | ||
} | ||
} | ||
``` | ||
|
||
就在我以为十拿九稳的时候,请求之后得到结果却给我了一盆冷水 403 | ||
|
||
``` | ||
HTTP/2.0 403 Forbidden | ||
Alt-Svc: h3=":443"; ma=86400 | ||
Cache-Control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0 | ||
Cf-Mitigated: challenge | ||
Cf-Ray: 848c6ee97eddce6c-SJC | ||
``` | ||
|
||
这让我意识到一定有什么东西在 tls-client 库里我没注意到,于是我经过一系列的查找终于让我找了关键信息 `HTTP/2 Fingerprinting` | ||
|
||
## HTTP/2 Fingerprinting | ||
|
||
说起来找到这个关键信息在于我某次调整 `http.Transport` 意外强制使用了 `HTTP/1.1` 然后得到了 200 的响应码,让我意识到也许问题不在 `TLS 指纹识别` 而在于 `HTTP/2` 也存在某种类似的指纹机制,谷歌一下 `http2 Fingerprinting` 第一篇 [http2-fingerprinting](https://lwthiker.com/networks/2022/06/17/http2-fingerprinting.html) 就是我们想要的答案。 | ||
|
||
在这篇文章里介绍到 `http2 Fingerprinting` 它可以识别浏览器类型和版本,或者是否使用脚本。该方法依赖于 `HTTP/2` 协议的内部机制,与简单的前身 HTTP/1.1 相比,该协议不太为人所知。 | ||
|
||
先来介绍下 `HTTP/2` 协议,HTTP/2 是一种二进制协议,与文本协议 `HTTP/1.1` 不同。 `HTTP/2` 中的消息由帧组成,有十种类型的帧服务于不同的目的。在请求的交互中 通常会有几种帧可能会被用来做浏览器与脚本的区分 | ||
|
||
- SETTINGS | ||
- WINDOW_UPDATE | ||
- HEADERS | ||
- PRIORITY | ||
|
||
这里着重介绍其中最关键的 `SETTINGS` 帧,通过 `SETTINGS` 帧,客户端向服务器通知其 `HTTP/2` 配置项。客户端可以通过六种不同的设置来控制参数,例如并发流的最大数量、HTTP 标头的最大数量、默认窗口大小以及是否支持服务器推送功能。我们可以通过 https://browserleaks.com/http2 ,来观察所用的浏览器用的相关 `HTTP/2` 帧信息,我用的 Chrome `SETTINGS` 帧参数大概如下 | ||
|
||
``` | ||
SETTINGS_HEADER_TABLE_SIZE: 65536 | ||
SETTINGS_ENABLE_PUSH: 0 | ||
SETTINGS_INITIAL_WINDOW_SIZE: 6291456 | ||
SETTINGS_MAX_HEADER_LIST_SIZE: 262144 | ||
``` | ||
|
||
由于每个 `HTTP/2` 客户端都有自己独特的 `SETTINGS` 帧设置。且不受到实际的 HTTP 请求影响,最主要的是 `SETTINGS` 帧设置通常被认为是繁琐的,一般会被软件厂商封装,用户通常难以设置这些值,这便给安全厂商提供了一个绝佳的机制来验证请求。 | ||
|
||
很明显 Go 的 `http.Client` 也一定是有一套不同于浏览器的设置,那么该如何改变这些值伪装成浏览器呢?在上面的例子中 `tr.NewClientConn(conn)` 点进去会发现源码中有这么一段 | ||
|
||
```go | ||
func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) { | ||
// 省略 | ||
initialSettings := []Setting{ | ||
{ID: SettingEnablePush, Val: 0}, | ||
{ID: SettingInitialWindowSize, Val: transportDefaultStreamFlow}, | ||
} | ||
if max := t.maxFrameReadSize(); max != 0 { | ||
initialSettings = append(initialSettings, Setting{ID: SettingMaxFrameSize, Val: max}) | ||
} | ||
if max := t.maxHeaderListSize(); max != 0 { | ||
initialSettings = append(initialSettings, Setting{ID: SettingMaxHeaderListSize, Val: max}) | ||
} | ||
if maxHeaderTableSize != initialHeaderTableSize { | ||
initialSettings = append(initialSettings, Setting{ID: SettingHeaderTableSize, Val: maxHeaderTableSize}) | ||
} | ||
cc.bw.Write(clientPreface) | ||
cc.fr.WriteSettings(initialSettings...) | ||
cc.fr.WriteWindowUpdate(0, transportDefaultConnFlow) | ||
cc.inflow.init(transportDefaultConnFlow + initialWindowSize) | ||
cc.bw.Flush() | ||
// 省略 | ||
return cc, nil | ||
} | ||
``` | ||
|
||
if 中的方法 `t.maxFrameReadSize()` `t.maxHeaderListSize()` 对应了 `http2.Transport` 中的配置,结果测试微调上面的代码 `http2.Transport{}` 改成如下内容 | ||
|
||
```go | ||
tr := http2.Transport{MaxHeaderListSize: 262144} | ||
``` | ||
|
||
即可获取到非 403 的响应头,表明通过鉴权。 | ||
|
||
## 原生 net/http 适配 | ||
|
||
最难的一环通过检测是完成了,但是如何优雅的集成到原生的 `net/http` 库里呢?要知道折腾这么老半天可就是想要替换掉 tls-client 这个臃肿的三方库,为了集成只能接着啃源码。根据观察发现在 `http` 源码中有个 `(t *Transport) onceSetNextProtoDefaults()` 函数,这个函数调用了`http2configureTransports` 配置 `http2.Transport`,最关键的一点来了我直接贴上源码 | ||
|
||
|
||
```go | ||
t2, err := http2configureTransports(t) | ||
if err != nil { | ||
log.Printf("Error enabling Transport HTTP/2 support: %v", err) | ||
return | ||
} | ||
t.h2transport = t2 | ||
|
||
// Auto-configure the http2.Transport's MaxHeaderListSize from | ||
// the http.Transport's MaxResponseHeaderBytes. They don't | ||
// exactly mean the same thing, but they're close. | ||
// | ||
// TODO: also add this to x/net/http2.Configure Transport, behind | ||
// a +build go1.7 build tag: | ||
if limit1 := t.MaxResponseHeaderBytes; limit1 != 0 && t2.MaxHeaderListSize == 0 { | ||
const h2max = 1<<32 - 1 | ||
if limit1 >= h2max { | ||
t2.MaxHeaderListSize = h2max | ||
} else { | ||
t2.MaxHeaderListSize = uint32(limit1) | ||
} | ||
} | ||
``` | ||
|
||
看到了么,源码中也有设置 `MaxHeaderListSize` 这一流程,可以通过 `MaxResponseHeaderBytes` 来关联设置 `MaxHeaderListSize`,最后总结一下关于原生 绕过 Cloudflare 的设置 | ||
|
||
```go | ||
h1 := (http.DefaultTransport).(*http.Transport).Clone() | ||
h1.MaxResponseHeaderBytes = 262144 | ||
client := &http.Client{Transport: h1} | ||
``` | ||
|
||
可能有的人会问 `TLS 指纹识别` 呢,这里经过测试 Cloudflare 没有用这个验证,只用了 `HTTP/2` 验证,个人猜测是担心浏览器的更新升级之后变更了 `TLS 指纹` 影响访问。 | ||
|
||
## 写在最后 | ||
|
||
最初只是对第三方库侵入 `net/http` 而感到不爽,当抽丝剥茧 找到最后的答案时竟然让我感到了一丝滑稽,原来我们找一圈来绕过 Cloudflare 的方式竟然如此简单。当然非常感谢 tls-client 这个库为我带来这一次宝贵的学习经验,也希望这篇文章可以帮到你。 |