如何在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
相关产品推荐
相关产品推荐

