Skip to content

Commit

Permalink
add 20240116.md
Browse files Browse the repository at this point in the history
  • Loading branch information
gowsp committed Jan 21, 2024
1 parent 08105e7 commit 21fc3e5
Showing 1 changed file with 205 additions and 0 deletions.
205 changes: 205 additions & 0 deletions source/_posts/2024/20240116.md
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 这个库为我带来这一次宝贵的学习经验,也希望这篇文章可以帮到你。

0 comments on commit 21fc3e5

Please sign in to comment.