mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-02-13 00:22:49 +00:00
352 lines
13 KiB
Go
352 lines
13 KiB
Go
package clienthellod
|
|
|
|
import (
|
|
"crypto/sha1" // skipcq: GSC-G505
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
"github.com/gaukas/clienthellod/internal/utils"
|
|
"github.com/gaukas/godicttls"
|
|
tls "github.com/refraction-networking/utls"
|
|
"golang.org/x/crypto/cryptobyte"
|
|
)
|
|
|
|
type ClientHello struct {
|
|
raw []byte
|
|
|
|
TLSRecordVersion uint16 `json:"tls_record_version"` // TLS record version (major, minor)
|
|
TLSHandshakeVersion uint16 `json:"tls_handshake_version"` // TLS handshake version (major, minor)
|
|
|
|
CipherSuites []uint16 `json:"cipher_suites"`
|
|
CompressionMethods utils.Uint8Arr `json:"compression_methods"`
|
|
Extensions []uint16 `json:"extensions"` // extension IDs in original order
|
|
ExtensionsNormalized []uint16 `json:"extensions_normalized"` // sorted extension IDs
|
|
|
|
ServerName string `json:"server_name"` // server_name(0)
|
|
NamedGroupList []uint16 `json:"supported_groups"` // supported_groups(10) a.k.a elliptic_curves
|
|
ECPointFormatList utils.Uint8Arr `json:"ec_point_formats"` // ec_point_formats(11)
|
|
SignatureSchemeList []uint16 `json:"signature_algorithms"` // signature_algorithms(13)
|
|
ALPN []string `json:"alpn"` // alpn(16)
|
|
CertCompressAlgo []uint16 `json:"compress_certificate"` // compress_certificate(27)
|
|
RecordSizeLimit utils.Uint8Arr `json:"record_size_limit"` // record_size_limit(28)
|
|
SupportedVersions []uint16 `json:"supported_versions"` // supported_versions(43)
|
|
PSKKeyExchangeModes utils.Uint8Arr `json:"psk_key_exchange_modes"` // psk_key_exchange_modes(45)
|
|
KeyShare []uint16 `json:"key_share"` // key_share(51)
|
|
ApplicationSettings []string `json:"application_settings"` // application_settings(17513) a.k.a ALPS
|
|
|
|
UserAgent string `json:"user_agent,omitempty"` // User-Agent header, set by the caller
|
|
|
|
NID int64 `json:"nid,omitempty"` // NID of the fingerprint
|
|
NormNID int64 `json:"norm_nid,omitempty"` // Normalized NID of the fingerprint
|
|
ID string `json:"id,omitempty"` // ID of the fingerprint (hex string)
|
|
NormID string `json:"norm_id,omitempty"` // Normalized ID of the fingerprint (hex string)
|
|
|
|
// below are ONLY used for calculating the fingerprint (hash)
|
|
lengthPrefixedSupportedGroups []uint16
|
|
lengthPrefixedEcPointFormats []uint8
|
|
lengthPrefixedSignatureAlgos []uint16
|
|
alpnWithLengths []uint8
|
|
lengthPrefixedCertCompressAlgos []uint8
|
|
keyshareGroupsWithLengths []uint16
|
|
// _nid int64
|
|
// norm_nid int64
|
|
|
|
// QUIC-only
|
|
qtp *QUICTransportParameters
|
|
}
|
|
|
|
// ReadClientHello reads a ClientHello from a connection (io.Reader)
|
|
// and returns a ClientHello struct.
|
|
//
|
|
// It will return an error if the reader does not give a stream of bytes
|
|
// representing a valid ClientHello. But all bytes read from the reader
|
|
// will be stored in the ClientHello struct to be rewinded by the caller.
|
|
func ReadClientHello(r io.Reader) (ch *ClientHello, err error) {
|
|
ch = new(ClientHello)
|
|
// Read a TLS record
|
|
// Read exactly 5 bytes from the reader
|
|
ch.raw = make([]byte, 5)
|
|
if _, err = io.ReadFull(r, ch.raw); err != nil {
|
|
return
|
|
}
|
|
|
|
// Check if the first byte is 0x16 (TLS Handshake)
|
|
if ch.raw[0] != 0x16 {
|
|
err = errors.New("not a TLS handshake record")
|
|
return
|
|
}
|
|
|
|
// Read exactly length bytes from the reader
|
|
ch.raw = append(ch.raw, make([]byte, binary.BigEndian.Uint16(ch.raw[3:5]))...)
|
|
_, err = io.ReadFull(r, ch.raw[5:])
|
|
return
|
|
}
|
|
|
|
func (ch *ClientHello) Raw() []byte {
|
|
return ch.raw
|
|
}
|
|
|
|
// ParseClientHello parses the raw bytes of a ClientHello into a ClientHello struct.
|
|
func (ch *ClientHello) ParseClientHello() error {
|
|
// Call uTLS to parse the raw bytes into ClientHelloSpec
|
|
fingerprinter := tls.Fingerprinter{
|
|
AllowBluntMimicry: true, // we will need all the extensions even when not recognized
|
|
}
|
|
chs, err := fingerprinter.RawClientHello(ch.raw)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse ClientHello, (*tls.Fingerprinter).RawClientHello(): %w", err)
|
|
}
|
|
|
|
// ch.TLSRecordVersion = chs.TLSVersMin // won't work for TLS 1.3
|
|
// ch.TLSHandshakeVersion = chs.TLSVersMax // won't work for TLS 1.3
|
|
ch.CipherSuites = chs.CipherSuites
|
|
ch.CompressionMethods = chs.CompressionMethods
|
|
|
|
// parse extensions
|
|
ch.parseExtensions(chs)
|
|
|
|
// Call uTLS to parse the raw bytes into ClientHelloMsg
|
|
chm := tls.UnmarshalClientHello(ch.raw[5:])
|
|
if chm == nil {
|
|
return errors.New("failed to parse ClientHello, (*tls.ClientHelloInfo).Unmarshal(): nil")
|
|
}
|
|
ch.ServerName = chm.ServerName
|
|
|
|
// In the end parse extra information from raw
|
|
return ch.parseExtra()
|
|
}
|
|
|
|
func (ch *ClientHello) parseExtensions(chs *tls.ClientHelloSpec) {
|
|
for _, ext := range chs.Extensions {
|
|
switch ext := ext.(type) {
|
|
case *tls.SupportedCurvesExtension:
|
|
for _, curve := range ext.Curves {
|
|
ch.NamedGroupList = append(ch.NamedGroupList, uint16(curve))
|
|
}
|
|
ch.lengthPrefixedSupportedGroups = append(ch.lengthPrefixedSupportedGroups, 2*uint16(len(ch.NamedGroupList)))
|
|
ch.lengthPrefixedSupportedGroups = append(ch.lengthPrefixedSupportedGroups, ch.NamedGroupList...)
|
|
case *tls.SupportedPointsExtension:
|
|
ch.ECPointFormatList = ext.SupportedPoints
|
|
ch.lengthPrefixedEcPointFormats = append(ch.lengthPrefixedEcPointFormats, uint8(len(ext.SupportedPoints)))
|
|
ch.lengthPrefixedEcPointFormats = append(ch.lengthPrefixedEcPointFormats, ext.SupportedPoints...)
|
|
case *tls.SignatureAlgorithmsExtension:
|
|
for _, sig := range ext.SupportedSignatureAlgorithms {
|
|
ch.SignatureSchemeList = append(ch.SignatureSchemeList, uint16(sig))
|
|
}
|
|
ch.lengthPrefixedSignatureAlgos = append(ch.lengthPrefixedSignatureAlgos, 2*uint16(len(ch.SignatureSchemeList)))
|
|
ch.lengthPrefixedSignatureAlgos = append(ch.lengthPrefixedSignatureAlgos, ch.SignatureSchemeList...)
|
|
case *tls.ALPNExtension:
|
|
ch.ALPN = ext.AlpnProtocols
|
|
// we will get alpnWithLengths from raw
|
|
case *tls.UtlsCompressCertExtension:
|
|
for _, algo := range ext.Algorithms {
|
|
ch.CertCompressAlgo = append(ch.CertCompressAlgo, uint16(algo))
|
|
}
|
|
ch.lengthPrefixedCertCompressAlgos = append(ch.lengthPrefixedCertCompressAlgos, 2*uint8(len(ch.CertCompressAlgo)))
|
|
ch.lengthPrefixedCertCompressAlgos = append(
|
|
ch.lengthPrefixedCertCompressAlgos,
|
|
utils.Uint16ToUint8(ch.CertCompressAlgo)...,
|
|
)
|
|
case *tls.FakeRecordSizeLimitExtension:
|
|
ch.RecordSizeLimit = append(ch.RecordSizeLimit, uint8(ext.Limit>>8), uint8(ext.Limit))
|
|
case *tls.SupportedVersionsExtension:
|
|
for _, ver := range ext.Versions {
|
|
ch.SupportedVersions = append(ch.SupportedVersions, uint16(ver))
|
|
}
|
|
case *tls.PSKKeyExchangeModesExtension:
|
|
ch.PSKKeyExchangeModes = ext.Modes
|
|
case *tls.KeyShareExtension:
|
|
for _, ks := range ext.KeyShares {
|
|
ch.KeyShare = append(ch.KeyShare, uint16(ks.Group))
|
|
// get below from raw instead
|
|
// ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, uint16(ks.Group))
|
|
// ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, uint16(len(ks.Data)))
|
|
}
|
|
case *tls.ApplicationSettingsExtension:
|
|
ch.ApplicationSettings = ext.SupportedProtocols
|
|
case *tls.GenericExtension:
|
|
if ext.Id == godicttls.ExtType_quic_transport_parameters {
|
|
ch.qtp = ParseQUICTransportParameters(ext.Data)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseExtra parses extra information from raw bytes which couldn't be parsed by uTLS.
|
|
func (ch *ClientHello) parseExtra() error {
|
|
// parse alpnWithLengths and Extensions
|
|
s := cryptobyte.String(ch.raw)
|
|
var recordVersion uint16
|
|
if !s.Skip(1) || !s.ReadUint16(&recordVersion) || !s.Skip(2) { // skip TLS record header
|
|
return errors.New("failed to parse TLS header, cryptobyte.String().Skip(): false")
|
|
}
|
|
ch.TLSRecordVersion = recordVersion
|
|
var handshakeVersion uint16
|
|
if !s.Skip(1) || // skip Handshake type
|
|
!s.Skip(3) || // skip Handshake length
|
|
!s.ReadUint16(&handshakeVersion) || // parse ClientHello version
|
|
!s.Skip(32) { // skip ClientHello random
|
|
return errors.New("failed to parse ClientHello, cryptobyte.String().Skip(): false")
|
|
}
|
|
ch.TLSHandshakeVersion = handshakeVersion
|
|
|
|
var ignoredSessionID cryptobyte.String
|
|
if !s.ReadUint8LengthPrefixed(&ignoredSessionID) {
|
|
return errors.New("unable to read session id")
|
|
}
|
|
|
|
var ignoredCipherSuites cryptobyte.String
|
|
if !s.ReadUint16LengthPrefixed(&ignoredCipherSuites) {
|
|
return errors.New("unable to read ciphersuites")
|
|
}
|
|
|
|
var ignoredCompressionMethods cryptobyte.String
|
|
if !s.ReadUint8LengthPrefixed(&ignoredCompressionMethods) {
|
|
return errors.New("unable to read compression methods")
|
|
}
|
|
|
|
if s.Empty() {
|
|
return nil // no extensions
|
|
}
|
|
|
|
var extensions cryptobyte.String
|
|
if !s.ReadUint16LengthPrefixed(&extensions) {
|
|
return errors.New("unable to read extensions data")
|
|
}
|
|
|
|
err := ch.parseExtensionsExtra(extensions)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse extensions, parseExtensionsExtra(): %w", err)
|
|
}
|
|
|
|
// sort ch.Extensions and put result to ch.ExtensionsNormalized
|
|
ch.ExtensionsNormalized = make([]uint16, len(ch.Extensions))
|
|
copy(ch.ExtensionsNormalized, ch.Extensions)
|
|
sort.Slice(ch.ExtensionsNormalized, func(i, j int) bool {
|
|
return ch.ExtensionsNormalized[i] < ch.ExtensionsNormalized[j]
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *ClientHello) parseExtensionsExtra(extensions cryptobyte.String) error {
|
|
var extensionIDs []uint16
|
|
for !extensions.Empty() {
|
|
var extensionID uint16
|
|
var extensionData cryptobyte.String
|
|
if !extensions.ReadUint16(&extensionID) {
|
|
return errors.New("unable to read extension ID")
|
|
}
|
|
if !extensions.ReadUint16LengthPrefixed(&extensionData) {
|
|
return errors.New("unable to read extension data")
|
|
}
|
|
|
|
extensionID, err := ch.parseExtensionExtra(extensionID, extensionData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse extension, parseExtensionExtra(): %w", err)
|
|
}
|
|
extensionIDs = append(extensionIDs, extensionID) // extension ID might need to be overridden by parseExtensionExtra() in case of GREASE
|
|
}
|
|
ch.Extensions = extensionIDs
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ch *ClientHello) parseExtensionExtra(extensionID uint16, extensionData cryptobyte.String) (uint16, error) {
|
|
switch extensionID {
|
|
case 16: // ALPN
|
|
ch.alpnWithLengths = extensionData
|
|
case 51: // keyshare
|
|
if !extensionData.Skip(2) {
|
|
return 0, errors.New("unable to skip keyshare total length")
|
|
}
|
|
for !extensionData.Empty() {
|
|
var group uint16
|
|
var length uint16
|
|
if !extensionData.ReadUint16(&group) || !extensionData.ReadUint16(&length) {
|
|
return 0, errors.New("unable to read keyshare group")
|
|
}
|
|
if utils.IsGREASEUint16(group) {
|
|
group = tls.GREASE_PLACEHOLDER
|
|
}
|
|
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, group)
|
|
ch.keyshareGroupsWithLengths = append(ch.keyshareGroupsWithLengths, length)
|
|
|
|
if !extensionData.Skip(int(length)) {
|
|
return 0, errors.New("unable to skip keyshare data")
|
|
}
|
|
}
|
|
default:
|
|
if utils.IsGREASEUint16(extensionID) {
|
|
return tls.GREASE_PLACEHOLDER, nil
|
|
}
|
|
}
|
|
|
|
return extensionID, nil
|
|
}
|
|
|
|
// FingerprintNID calculates fingerprint Numerical ID of ClientHello.
|
|
// Fingerprint is defined by
|
|
func (ch *ClientHello) FingerprintNID(normalized bool) int64 {
|
|
if normalized && ch.NormNID != 0 {
|
|
return ch.NormNID
|
|
}
|
|
|
|
if !normalized && ch.NID != 0 {
|
|
return ch.NID
|
|
}
|
|
|
|
h := sha1.New() // skipcq: GO-S1025, GSC-G401,
|
|
binary.Write(h, binary.BigEndian, uint16(ch.TLSRecordVersion))
|
|
binary.Write(h, binary.BigEndian, uint16(ch.TLSHandshakeVersion))
|
|
|
|
updateArr(h, utils.Uint16ToUint8(ch.CipherSuites))
|
|
updateArr(h, ch.CompressionMethods)
|
|
if normalized {
|
|
updateArr(h, utils.Uint16ToUint8(ch.ExtensionsNormalized))
|
|
} else {
|
|
updateArr(h, utils.Uint16ToUint8(ch.Extensions))
|
|
}
|
|
updateArr(h, utils.Uint16ToUint8(ch.lengthPrefixedSupportedGroups))
|
|
updateArr(h, ch.lengthPrefixedEcPointFormats)
|
|
updateArr(h, utils.Uint16ToUint8(ch.lengthPrefixedSignatureAlgos))
|
|
updateArr(h, ch.alpnWithLengths)
|
|
updateArr(h, utils.Uint16ToUint8(ch.keyshareGroupsWithLengths))
|
|
updateArr(h, ch.PSKKeyExchangeModes)
|
|
updateArr(h, utils.Uint16ToUint8(ch.SupportedVersions))
|
|
updateArr(h, ch.lengthPrefixedCertCompressAlgos)
|
|
updateArr(h, ch.RecordSizeLimit)
|
|
|
|
out := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8]))
|
|
|
|
if normalized {
|
|
// ch.norm_nid = out
|
|
ch.NormNID = out
|
|
} else {
|
|
// ch._nid = out
|
|
ch.NID = out
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// FingerprintID calculates fingerprint ID of ClientHello and
|
|
// represents it as hexadecimal string.
|
|
func (ch *ClientHello) FingerprintID(normalized bool) string {
|
|
nid := ch.FingerprintNID(normalized)
|
|
hid := make([]byte, 8)
|
|
binary.BigEndian.PutUint64(hid, uint64(nid))
|
|
|
|
id := hex.EncodeToString(hid)
|
|
if normalized {
|
|
ch.NormID = id
|
|
} else {
|
|
ch.ID = id
|
|
}
|
|
return id
|
|
}
|