深入解析 SUI Testnet Faucet:探讨 TLS 指纹的用途与绕过

在平时测试网交互的过程中,许多项目要求用户首先获取测试代币,以进行后续操作。然而,由于对测试代币的滥用现象日益严重,越来越多的项目采取了严格的措施,以防止机器人通过自动化方法获取测试代币。这些措施包括但不限于增加验证码、IP/设备频率限制等。虽然这些限制措施比较常见,但如果您有兴趣,我会在以后的文章中再单独描述如何绕过这些措施。

然而,我注意到 Sui 的测试网代币领取接口使用了一种比较新奇的几乎无感知的验证方式,可以很准确地区分正常用户和机器人,并拒绝机器人请求。本文将对这种验证方式的实现原理和可能的绕过方式进行简要分析。

经过多方搜索,发现 Sui 使用的是 cloudflare 的 WAF 防火墙,并且该识别技术在 2020 年就已经有实践,归根结底就是使用了 TLS 指纹技术(如 JA3 等)。

那么什么是 TLS 指纹技术?

TLS 指纹技术(如 JA3 等)是通过在TLS握手阶段识别ClientHello数据包生成一个唯一的指纹,从而识别客户端应用程序或代理工具。这种指纹可以被 WAF 等安全设备用于识别潜在的恶意流量或代理工具。

ClientHello 是 TLS 协议中的第一个客户端发起的消息,用于与服务器进行握手。它包含了客户端的一些配置信息,如支持的TLS版本、加密算法、压缩算法等。ClientHello 数据包的构成通常如下:

  1. 版本号(Protocol Version):表示客户端支持的TLS协议版本,如TLS 1.2、TLS 1.3等。

  2. 随机数(Random):包含32字节的随机数,由客户端生成,用于生成会话密钥。

  3. 会话ID(Session ID):用于恢复之前的会话状态,减少握手延迟。如果客户端之前与服务器建立过会话,会在ClientHello中包含会话ID。

  4. 加密套件(Cipher Suites):表示客户端支持的加密算法和密钥交换算法。通常是一个列表,按照优先级排序。

  5. 压缩方法(Compression Methods):表示客户端支持的压缩算法。通常是一个列表,按照优先级排序。

  6. 扩展(Extensions):包含了一些可选的扩展信息,如支持的应用层协议(ALPN)、密钥分享(Key Share)等。

这些信息一起构成了ClientHello数据包,客户端将其发送给服务器,作为TLS握手的第一步。服务器在收到ClientHello后,会根据其中的信息选择合适的加密套件和协议版本,并生成相应的响应消息发送给客户端,开始TLS握手过程。

由于相同的客户端工具所使用的上述信息,除随机数外总是保持不变的,那么就可以通过一些算法屏蔽某些客户端工具。

那么现在我们知道了大概原理,那么我们如何绕过这道防护呢?目前我能想到如下两条路径:

  1. 尝试改写客户端工具,以使得其伪装成为正常浏览的浏览器;

  2. 使用无头浏览器等类似工具,伪造请求;

经过深入研究,发现第一条路的实现路径过于硬核,在没有掌握相关知识的情况下,无法快速实现绕过目标,其主要原因是因为市面上大多数库均依赖系统的 openssl 或者 libssl.so 底层库实现,而此类库没有合适的自定义参数可以在使用时传入,导致需要自己实现 tls 握手或者重编译才可以实现上述目标。由于时间比较紧迫,以及技术要求过高,暂时没有深入研究,在此提供几个搜索出来比较有意思的仓库供大家参考:

  1. CycleTLS 该仓库使用 go 编写了一套自己的 tls 握手协议,可谓是实现方式硬核,并且提供 js 调用 sdk,简单容易上手

  2. curl-impersonate 该仓库魔改了 curl 代码,支持 ciphers 以及 curves 的修改。使用与浏览器相同的 ssl 组件来模拟浏览器的 extionsions,但是目前看无法伪造任意 ja3

基于需求,就要用第二种路径实现请求伪造,即使用无头浏览器绕过。

如何使用无头浏览器绕过 tls 指纹防护

首先介绍一下本次要用的库 Puppeteer

Puppeteer 是一个由 Google 开发的 Node.js 库,用于控制和自动化 Chrome 或 Chromium 浏览器。它提供了一组高级 API,可以通过 JavaScript 脚本与浏览器进行交互,实现网页的自动化操作,例如网页截图、网页内容提取、表单填写、模拟用户输入、页面导航等。

Puppeteer 基于 Headless Chrome,即无界面的 Chrome 浏览器,可以在后台运行,不会显示图形界面。它提供了一种简单而强大的方式来进行网页自动化,可以在测试、爬虫、网页截图、性能分析等领域中被广泛应用。

Puppeteer 具有丰富的功能,包括页面操作、DOM操作、事件处理、网络请求拦截、Cookies管理、页面截图、PDF生成等,同时还支持异步操作和Promise风格的API,使得编写自动化脚本更加方便和灵活。

因此 Puppteer 可以被广泛应用于自动化测试、爬虫等项目中。

废话不多说,下面直接上代码,该代码目前可以被测试通过并成功领取 sui 的测试网代币。

const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth')
// 钱包地址合集
const walletAddresses = ["address1", "address2"];
// 代理连接
const proxy_url = "http://aaa.example";
// 代理验证用户名
const proxy_username = "username";
// 代理验证密码
const proxy_password = "password";
// 开始钱包的下标
const start_index = 0;
// 结束钱包的下标
const max_index = 100;

async function faucetRequest(walletAddress) {
  // 设置 puppeteer 使用 Stealth 插件
  puppeteer.use(StealthPlugin())
  // 启动无头浏览器,启动参数为代理连接,此处主要为了每次请求都发送自不同 ip,如果为测试可以去掉 args 中的参数。
  const browser = await puppeteer.launch({
    // headless: false,
    args: [
      `--proxy-server=${proxy_url}`
    ],
  });
  // 打开一个新页面
  const page = await browser.newPage();
  // 输入代理的账号密码
  await page.authenticate({username: proxy_username, password: proxy_password})
  // 设置页面的 UserAgent,如果不需要变化的话可以注释掉
  await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36');
  // 打开一个页面,由于浏览器无法直接发送 post 请求,故需要打开一个页面之后使用 evaluate 方法发送相关 post 请求
  await page.goto('https://faucet.testnet.sui.io')
  // Post 请求体,参数主要来自于 sui 插件内的领水按钮
  await page.evaluate((walletAddress) => {
    const url = 'https://faucet.testnet.sui.io/gas';
    console.log(walletAddress)
    const body = {
      "FixedAmountRequest": {
        "recipient": walletAddress
      }
    }
    return fetch(url, {
      method: 'POST',
      headers: {
        "Authority": "faucet.testnet.sui.io",
        "Accept": "*/*",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Content-Type": "application/json",
        "Origin": "chrome-extension://opcgpfmipidbgpenhmajoajpbobppdil",
        "Accept-Encoding": "gzip"
      },
      body: JSON.stringify(body)
    })
  }, walletAddress).catch((error) => {
    console.log('sui Error: ', error.message);

    faucetRequest(walletAddress)
  });
  // 关闭浏览器
  await browser.close();
}

// 主循环代码,执行完领水后自动判断是否到达领取上限,并自动领水
async function faucet(index) {
  const walletAddress = walletAddresses[index];
  console.log(walletAddress)
  await faucetRequest(walletAddress);
  if (index < max_index - 0) {
    faucet(index + 1);
  }
}

faucet(start_index - 0);

如果您对薅羊毛这一话题感兴趣,欢迎关注我的 Twitter 和 Mirror。我将持续分享我在薅羊毛过程中踩过的坑以及如何成功解决这些问题。

Subscribe