实现原理:JARM 指纹
对应代码:
pkg/jarm.go· 入口JARMScan(target)· 基于 Salesforce JARM 规范
JARM 是服务端 TLS 实现的指纹——同一款软件(Nginx、Cloudflare、Cobalt Strike)配置相同,JARM 相同。本页讲那 60 个字符是怎么算出来的。
10 个探针
JARM 的精髓:不是发一个 ClientHello,而是发 10 个精心设计的不同 ClientHello,看服务器对每个的响应。
探针精确配置表
| # | Version | 套件数 | ALPN | SendSNI | SupportedVersions | SupportedGroups |
|---|---|---|---|---|---|---|
| 0 | TLS1.2 | 6 | {} | true | — | X25519,P256,P384 |
| 1 | TLS1.2 | 6 | http/1.1 | true | — | X25519,P256,P384 |
| 2 | TLS1.2 | 8 | h2,http/1.1 | true | — | X25519,P256,P384 |
| 3 | TLS1.2 | 9 | {} | true | {TLS1.3} | X25519,P256,P384 |
| 4 | TLS1.2 | 9 | http/1.1 | true | {TLS1.3} | X25519,P256,P384 |
| 5 | TLS1.2 | 11 | h2,http/1.1 | true | {TLS1.3} | X25519,P256,P384 |
| 6 | TLS1.2 | 9 | h2,http/1.1 | true | {TLS1.3} | X25519,P256,P384 |
| 7 | TLS1.2 | 2 | {} | true | — | X25519,P256 |
| 8 | TLS1.2 | 2 | http/1.1 | true | — | X25519,P256 |
| 9 | TLS1.2 | 2 | h2,http/1.1 | true | — | X25519,P256 |
关键设计点:
- 所有探针
Version都是tls.VersionTLS12(0x0303)——TLS1.3 通过SupportedVersions: {TLS1.3}表达,不是直接设 Version - 所有探针
SendSNI=true——都发 SNI RecordVersion全部为 0(零值),代码用probe.Version作 Min/MaxVersion- 套件组合差异:6(ECDHE)/ 8(+RSA)/ 9(+TLS1.3 三件套)/ 11(全)/ 2(极简)
为什么这 10 个组合?不同 TLS 实现对"非典型但合法"的 ClientHello 响应不同——有的严格按 RFC,有的宽容,有的会降级。这些差异构成指纹。
jarmProbe 结构体字段:
go
type jarmProbe struct {
Version uint16
CipherSuites []uint16
ALPN []string
RecordVersion uint16
SendSNI bool
SupportedVersions []uint16
SupportedGroups []tls.CurveID
}采集每个探针的响应
响应指纹格式(jarmProbeServer L317-324):
parts[0] = fmt.Sprintf("%04x", state.Version) // 如 0303
parts[1] = fmt.Sprintf("%04x", state.CipherSuite) // 如 1301
parts[2] = alpn // 仅当 NegotiatedProtocol != "" 才追加
// Join 用 "|"例:0303|cca8|h2。无 ALPN 协商时只有前两段。
最终 60 字符指纹
go
func buildJARMFingerprint(responses []string) string {
forward := sha256(strings.Join(responses, ","))
reverse := sha256(strings.Join(reverse(responses), ","))
return forward[:30] + reverse[:30] // 60 字符
}为什么正反两次哈希?防止某些位置的特征被掩盖——正序偏重前几个探针,反序偏重后几个。
RawHash(640 hex)
buildJARMRawHash(L331-342)对每个响应单独 SHA-256:
10 段 × 64 hex = 640 hex RawHash,便于精细比对。空响应填 strings.Repeat("0", 64)。
错误处理
- 单探针失败:
responses[i]=""后 continue - 全空:
result.Error填字符串,仍返回*JARMResult, nil(不返回 error) lastTLSVersion/lastCipherSuite仅在非空时覆盖
输出
go
type JARMResult struct {
JARMHash string // 60 字符
RawHash string // 640 字符
TLSVersion string
CipherSuite string
Error string
}用途:服务识别
下一步
- 客户端指纹 → JA3/JA3S 指纹
- 指纹库匹配 → 指纹库匹配
- 测绘里怎么用 → 网络空间测绘