TLS指纹识别

TLS 指纹可以用来作为一些常见语言的网络请求库识别特征。什么是TLS 指纹指纹呢?在TLS 建立链接的时候,会发送一个client hello 的请求,请求会告诉对方,自己支持哪些加密算法、参数等。由于这些参数比较多,并且和顺序无关,有人就想到可以通过这些参数来计算hash 值昨晚指纹。

因为常见的Python,nodejs 等,底层实现中有默认的TLS 参数配置,大版本内几乎很少变更,普通用户也很少回去变更这些配置。所以,可以通过这些指纹,识别出常见的网络库。

一个普通的面向用户的普通,理论上真实用户不会使用nodejs, python 等脚本访问,可以达到识别和反爬的作用。

例如我们实现一个函数,查看下nodejs v18 的TLS 指纹。

function fetchAndPrintData(url) {
    https.get(url, (response) => {
        let data = '';

        // A chunk of data has been received.
        response.on('data', (chunk) => {
            data += chunk;
        });

        // The whole response has been received.
        response.on('end', () => {
            console.log(data);
        });

    }).on('error', (err) => {
        console.error(`Error: ${err.message}`);
    });
}

const url = 'https://tls.browserleaks.com/json';
fetchAndPrintData(url);
// node 18.1

{
  "user_agent": "",
  "ja3_hash": "0cce74b0d9b7f8528fb2181588d23793",
  "ja3_text": "771,4866-4867-4865-49199-49195-49200-49196-158-49191-103-49192-107-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-162-49326-49324-49314-49310-49244-49248-49238-49234-49188-106-49187-64-49162-49172-57-56-49161-49171-51-50-157-49313-49309-49233-156-49312-49308-49232-61-60-53-47-255,0-11-10-35-22-23-13-43-45-51,29-23-30-25-24-256-257-258-259-260,0-1-2",
  "ja3n_hash": "2cdc372ba33ad43cbb1c09aad0566191",
  "ja3n_text": "771,4866-4867-4865-49199-49195-49200-49196-158-49191-103-49192-107-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-162-49326-49324-49314-49310-49244-49248-49238-49234-49188-106-49187-64-49162-49172-57-56-49161-49171-51-50-157-49313-49309-49233-156-49312-49308-49232-61-60-53-47-255,0-10-11-13-22-23-35-43-45-51,29-23-30-25-24-256-257-258-259-260,0-1-2",
  "akamai_hash": "",
  "akamai_text": ""
}

ja3 的指纹计算规则:

The field order is as follows:

SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat

把版本,加密套件,扩展等内容按顺序排列然后计算hash值,便可得到一个客户端的TLS FingerPrint,waf防护规则其实就是整理提取一些常见的非浏览器客户端requests,curl的指纹然后在客户端发起https请求时进行识别并拦截。

https://tls.browserleaks.com/json 这个接口可以查看你的指纹信息。

其中的akamai_hash 是HTTP2 的指纹,因为我用的http1 访问,所以没有。在HTTP2 中,

绕过的方法也很简单,调整一下Ciphers顺序或者删除、添加一些不重要的参数就可以。

Node.js 14 中的默认密码集是:

> require('crypto').constants.defaultCoreCipherList.split(':')
[
  'TLS_AES_256_GCM_SHA384',
  'TLS_CHACHA20_POLY1305_SHA256',
  'TLS_AES_128_GCM_SHA256',
  'ECDHE-RSA-AES128-GCM-SHA256',
  'ECDHE-ECDSA-AES128-GCM-SHA256',
  'ECDHE-RSA-AES256-GCM-SHA384',
  'ECDHE-ECDSA-AES256-GCM-SHA384',
  'DHE-RSA-AES128-GCM-SHA256',
  'ECDHE-RSA-AES128-SHA256',
  'DHE-RSA-AES128-SHA256',
  'ECDHE-RSA-AES256-SHA384',
  'DHE-RSA-AES256-SHA384',
  'ECDHE-RSA-AES256-SHA256',
  'DHE-RSA-AES256-SHA256',
  'HIGH',
  '!aNULL',
  '!eNULL',
  '!EXPORT',
  '!DES',
  '!RC4',
  '!MD5',
  '!PSK',
  '!SRP',
  '!CAMELLIA'
]
const tls = require('tls');
const https = require('https');

const defaultCiphers = tls.DEFAULT_CIPHERS.split(':');
const shuffledCiphers = [
    defaultCiphers[0],
    // Swap the 2nd & 3rd ciphers:
    defaultCiphers[2],
    defaultCiphers[1],
    ...defaultCiphers.slice(3)
].join(':');

request = require('https').get('https://en.zalando.de/api/navigation', {
    ciphers: shuffledCiphers
}).on('response', (res) => {
    console.log(res.statusCode); // Prints 200
});

当然,很多用户还会使用proxy 来访问目标网站,这需要注意。

使用第三方代理更换 IP 地址时,TLS 指纹的变化取决于代理服务器的配置和实现方式。以下是一些可能的情况:

  1. 透明代理
    • 如果代理服务器只是简单地转发请求而不修改任何 TLS 参数,那么您的客户端 TLS 配置(包括加密套件、TLS 版本等)仍然会被服务器看到,TLS 指纹不会变化。
  2. HTTPS 代理
    • 如果代理服务器终止了 TLS 连接并与目标服务器重新建立一个新的 TLS 连接(即代理服务器作为中间人重新发起请求),那么代理服务器的 TLS 配置将决定新的 TLS 指纹。在这种情况下,您的客户端 TLS 配置不会被目标服务器看到,TLS 指纹会变化。
  3. SOCKS 代理
    • 如果您使用的是 SOCKS 代理,TLS 连接通常会直接从您的客户端到达目标服务器,因此您的客户端 TLS 配置仍然会被目标服务器看到,TLS 指纹不会变化。
const axios = require('axios');
const HttpsProxyAgent = require('https-proxy-agent');
const https = require('https');

const cipherSuites = [
  'ECDHE-RSA-AES128-GCM-SHA256',
  'ECDHE-RSA-AES256-GCM-SHA384',
  'ECDHE-ECDSA-AES128-GCM-SHA256',
  'ECDHE-ECDSA-AES256-GCM-SHA384',
  'ECDHE-RSA-AES128-SHA',
  'ECDHE-RSA-AES256-SHA',
  'ECDHE-ECDSA-AES128-SHA',
  'ECDHE-ECDSA-AES256-SHA',
  'DHE-RSA-AES128-GCM-SHA256',
  'DHE-RSA-AES256-GCM-SHA384',
  'DHE-RSA-AES128-SHA',
  'DHE-RSA-AES256-SHA',
  'AES128-GCM-SHA256',
  'AES256-GCM-SHA384',
  'AES128-SHA',
  'AES256-SHA',
  'DES-CBC3-SHA',
  'RSA+AESGCM',
  'RSA+AES',
  'RSA+SHA',
];

const tlsVersions = ['TLSv1.2', 'TLSv1.3'];

function getRandomElement(arr) {
  return arr[Math.floor(Math.random() * arr.length)];
}

function createHttpsAgent() {
  const selectedCipher = getRandomElement(cipherSuites);
  const selectedTlsVersion = getRandomElement(tlsVersions);

  return new https.Agent({
    ciphers: selectedCipher,
    minVersion: selectedTlsVersion,
    maxVersion: selectedTlsVersion,
  });
}

async function makeRequest(url, options = {}) {
  const agent = createHttpsAgent();

  // 使用代理地址
  const proxy = 'http://proxy.example.com:8080';

  const proxyAgent = new HttpsProxyAgent(proxy);
  proxyAgent.options = {
    ...proxyAgent.options,
    ...agent.options,
  };

  const config = {
    ...options,
    httpsAgent: proxyAgent,
    timeout: options.timeout || 5000, // 默认超时时间为5秒
    headers: {
      'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', // 默认User-Agent
      ...options.headers,
    },
  };

  try {
    const response = await axios.get(url, config);
    return response.data;
  } catch (error) {
    if (error.response) {
      // 服务器返回的错误
      console.error(`Error: ${error.response.status} ${error.response.statusText}`);
      console.error(`Response data: ${error.response.data}`);
    } else if (error.request) {
      // 请求发送但没有收到响应
      console.error('Error: No response received from the server');
      console.error(error.request);
    } else {
      // 其他错误
      console.error('Error:', error.message);
    }
    throw error; // 重新抛出错误,以便调用者可以处理
  }
}

// 示例用法
(async () => {
  const urls = [
    'https://example.com',
    'https://another-example.com',
    // 添加更多URL进行测试
  ];

  for (const url of urls) {
    try {
      const data = await makeRequest(url, {
        headers: { 'Custom-Header': 'HeaderValue' },
      });
      console.log(`Response from ${url}:`, data);
    } catch (error) {
      console.error(`Failed to fetch ${url}:`, error.message);
    }
  }
})();

绕过TLS/akamai指纹护盾

绕过 Cloudflare 指纹护盾

SSL 指纹识别和绕过

Fighting TLS fingerprinting with Node.js

Leave a Comment

邮箱地址不会被公开。 必填项已用*标注