You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Node.js中获取客户端的JA3与JA4指纹?

用Node.js提取TLS ClientHello并计算JA3指纹

核心步骤

要实现这个需求,你需要完成抓包过滤TLS握手包、解析TCP/TLS协议层、提取ClientHello字段、按JA3规则生成哈希这几个环节,以下是具体实现方案:


1. 依赖安装

除了pcap抓包库,你可以用手动解析字节的方式避免依赖第三方TLS解析库,也可以用tls-parser简化解析:

npm install pcap

2. 完整实现代码

方案一:手动解析ClientHello(无额外依赖)

const pcap = require('pcap');
const crypto = require('crypto');

// 维护TCP流缓冲区,处理分片包
const tcpStreams = new Map();

// 解析ClientHello字节流
function parseClientHello(handshakePayload) {
  let offset = 0;
  // 验证握手类型为ClientHello(0x01)
  if (handshakePayload.readUInt8(offset++) !== 0x01) return null;
  
  // 读取握手消息长度(3字节)
  const handshakeLength = (handshakePayload.readUInt16BE(offset) << 8) | handshakePayload.readUInt8(offset + 2);
  offset += 3;
  
  // 提取TLS版本
  const version = handshakePayload.readUInt16BE(offset);
  offset += 2;
  
  // 跳过随机数(32字节)和会话ID
  offset += 32;
  offset += handshakePayload.readUInt8(offset++);
  
  // 提取密码套件
  const cipherSuitesLength = handshakePayload.readUInt16BE(offset);
  offset += 2;
  const cipherSuites = [];
  for (let i = 0; i < cipherSuitesLength; i += 2) {
    cipherSuites.push(handshakePayload.readUInt16BE(offset + i));
  }
  offset += cipherSuitesLength;
  
  // 跳过压缩方法
  offset += handshakePayload.readUInt8(offset++);
  
  // 提取扩展列表、椭圆曲线、点格式
  const extensionsLength = handshakePayload.readUInt16BE(offset);
  offset += 2;
  const extensions = [];
  const ellipticCurves = [];
  const pointFormats = [];
  
  while (offset < 5 + handshakeLength) { // 5是握手头长度
    const extType = handshakePayload.readUInt16BE(offset);
    offset += 2;
    const extLength = handshakePayload.readUInt16BE(offset);
    offset += 2;
    const extData = handshakePayload.slice(offset, offset + extLength);
    offset += extLength;
    
    extensions.push(extType);
    
    // 解析椭圆曲线扩展(类型0x0a)
    if (extType === 0x0a && extLength >= 2) {
      const curvesLength = extData.readUInt16BE(0);
      for (let i = 2; i < curvesLength + 2; i += 2) {
        ellipticCurves.push(extData.readUInt16BE(i));
      }
    }
    
    // 解析椭圆曲线点格式扩展(类型0x0b)
    if (extType === 0x0b && extLength >= 1) {
      const formatsLength = extData.readUInt8(0);
      for (let i = 1; i < formatsLength + 1; i++) {
        pointFormats.push(extData.readUInt8(i));
      }
    }
  }
  
  return { version, cipherSuites, extensions, ellipticCurves, pointFormats };
}

// 初始化抓包会话,过滤TLS握手包
const filter = 'tcp port 443 and tcp[((tcp[12:1] & 0xf0) >> 2):1] = 0x16';
const session = pcap.createSession('', filter);

session.on('packet', (rawPacket) => {
  const packet = pcap.decode.packet(rawPacket);
  const ip = packet.payload.payload;
  const tcp = ip.payload;
  
  if (!tcp.data || tcp.data.length === 0) return;
  
  // 生成TCP流唯一标识
  const streamKey = `${ip.saddr}:${tcp.sport}-${ip.daddr}:${tcp.dport}`;
  let stream = tcpStreams.get(streamKey);
  
  if (!stream) {
    stream = { buffer: Buffer.from([]), seq: tcp.seq };
    tcpStreams.set(streamKey, stream);
  }
  
  // 拼接TCP分片(简化处理,仅按顺序拼接,实际需处理乱序)
  if (tcp.seq === stream.seq) {
    stream.buffer = Buffer.concat([stream.buffer, tcp.data]);
    stream.seq += tcp.data.length;
    
    // 循环解析缓冲区中的完整TLS记录
    while (stream.buffer.length >= 5) {
      const contentType = stream.buffer.readUInt8(0);
      const recordLength = stream.buffer.readUInt16BE(3);
      
      if (stream.buffer.length < 5 + recordLength) break;
      
      // 提取完整TLS记录并从缓冲区移除
      const tlsRecord = stream.buffer.slice(0, 5 + recordLength);
      stream.buffer = stream.buffer.slice(5 + recordLength);
      
      // 仅处理TLS握手记录(0x16)
      if (contentType !== 0x16) continue;
      
      const clientHello = parseClientHello(tlsRecord.slice(5));
      if (!clientHello) continue;
      
      // 生成JA3字符串
      const ja3String = [
        clientHello.version,
        clientHello.cipherSuites.join(','),
        clientHello.extensions.join(','),
        clientHello.ellipticCurves.join(','),
        clientHello.pointFormats.join(',')
      ].join(',');
      
      // 计算MD5哈希得到JA3指纹
      const ja3Hash = crypto.createHash('md5').update(ja3String).digest('hex');
      
      console.log(`JA3指纹: ${ja3Hash}`);
      console.log(`JA3原始字符串: ${ja3String}`);
      
      // 处理完ClientHello后移除该流(避免重复解析)
      tcpStreams.delete(streamKey);
    }
  }
});

方案二:使用tls-parser简化解析

如果你不想手动处理字节解析,可以安装tls-parser:

npm install pcap tls-parser

替换解析部分的代码:

const { parseTlsRecord } = require('tls-parser');

// 在packet事件的流处理部分,替换解析逻辑:
const tlsRecord = parseTlsRecord(tlsRecordBuffer);
if (tlsRecord.contentType !== 'handshake') continue;

const clientHello = tlsRecord.handshakeMessages.find(msg => msg.type === 'clientHello');
if (!clientHello) continue;

const ja3String = [
  clientHello.version.major * 256 + clientHello.version.minor,
  clientHello.cipherSuites.map(cs => cs.id).join(','),
  clientHello.extensions.filter(ext => ext.data).map(ext => ext.type).join(','),
  (clientHello.extensions.find(ext => ext.type === 0x0a)?.data?.namedGroups || []).join(','),
  (clientHello.extensions.find(ext => ext.type === 0x0b)?.data?.pointFormats || []).join(',')
].join(',');

const ja3Hash = crypto.createHash('md5').update(ja3String).digest('hex');

关键注意事项

  • TCP流重组:ClientHello可能被拆分成多个TCP分片,必须维护流缓冲区拼接完整后再解析,上面的代码实现了基础的顺序拼接,生产环境需考虑乱序、重传等情况。
  • 端口范围:示例过滤了443端口,实际场景中可能需要扩展到其他TLS端口(如8443),修改过滤规则即可。
  • JA3规则细节:JA3仅提取ClientHello中的指定字段,严格按照顺序拼接后做MD5哈希,不要随意调整字段顺序或过滤规则。

内容的提问来源于stack exchange,提问作者sathish kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 14:04:54