Skip to content

实现原理:JARM 指纹

对应代码:pkg/jarm.go · 入口 JARMScan(target) · 基于 Salesforce JARM 规范

JARM 是服务端 TLS 实现的指纹——同一款软件(Nginx、Cloudflare、Cobalt Strike)配置相同,JARM 相同。本页讲那 60 个字符是怎么算出来的。

10 个探针

JARM 的精髓:不是发一个 ClientHello,而是发 10 个精心设计的不同 ClientHello,看服务器对每个的响应。

探针精确配置表

#Version套件数ALPNSendSNISupportedVersionsSupportedGroups
0TLS1.26{}trueX25519,P256,P384
1TLS1.26http/1.1trueX25519,P256,P384
2TLS1.28h2,http/1.1trueX25519,P256,P384
3TLS1.29{}true{TLS1.3}X25519,P256,P384
4TLS1.29http/1.1true{TLS1.3}X25519,P256,P384
5TLS1.211h2,http/1.1true{TLS1.3}X25519,P256,P384
6TLS1.29h2,http/1.1true{TLS1.3}X25519,P256,P384
7TLS1.22{}trueX25519,P256
8TLS1.22http/1.1trueX25519,P256
9TLS1.22h2,http/1.1trueX25519,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
}

用途:服务识别

JARM 相同 ≠ 同一服务

同款软件默认配置会撞同一个 JARM。JARM 命中 c2 类别也不一定是 C2——需结合置信度、上下文与其他证据综合判断。详见 指纹库匹配应急取证工作流

下一步