1
2
title: TLS指纹综述
categories: web

TLS指纹综述

一、ja3指纹综述

​ TLS指纹是利用TLS过程中的字段所得到的hash指纹,称为ja3,其项目已经于github开源:

​ 然而,网络上普遍使用用于获取ja3指纹网址https://ja3er.com/form已经寄了,我们可以选用目前还在活跃的网址如下:https://tls.peet.ws/

image-20230507164313008

​ 使用浏览器访问网址时,获取到本机的JA3指纹,这是从服务端对于用户端进行TLS指纹的探测,可以在某种程度上探测用户的操作系统、以及使用的浏览器版本。反之,通过jarm脚本,可以对于对应服务器的JARM,即ja3指纹进行探测,如下所示对百度进行探测。

image-20230508111650118

​ github官方仓库中提到,使用JARM可与i对于C2服务器进行识别,并列出了几种较为常见的渗透工具(病毒)C2服务器的JARM指纹,如下所示:

image-20230508105727673

​ 仓库中继续提到,当从 abuse.ch 编制的列表中扫描 Trickbot 恶意软件 C2 时,列表中 80% 的实时 IP 产生了相同的 JARM 指纹。将此 JARM 指纹与 Alexa 排名前 100 万的网站进行比较时,没有重叠。

​ 通过端口 443 扫描了整个互联网,数十亿个 IP,并发现了以下内容:

image-20230508112421118

​ 如果与 Cobalt Strike JARM 匹配的服务器在一年多的时间里属性没有变化,则更有可能是合法的误报,而与 2 个月前不存在的 Cobalt Strike JARM 匹配的服务器则更有可能是恶意的真阳性。

二、ja3&ja3s

​ ja3一般指的是所谓服务端的指纹信息;ja3s使用的

​ 那么,JA3算法中的哈希值是如何得到的呢,通过本地的信息进行解释:

最终的JA3指纹:4558520b679631d89e69d41095530283

实际是由字符串:771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,11-43-16-45-23-13-10-65281-17513-0-27-18-35-51-5-21-41,29-23-24,0 进行md5得到的hash值。

后者可以分为五段,分别具有以下含义:

1
2
3
4
5
TLSVersion:771
Ciphers:4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53
Extensions:11-43-16-45-23-13-10-65281-17513-0-27-18-35-51-5-21-41
EllipticCurves:29-23-24
EllipticCurvePointFormats:0

实际情况如下所示:

image-20230521182921438

​ 其中,可以看到随机插入的TLS_GREASE值,其定义如下:

GREASE是Generate Random Extensions And Sustain Extensibility的缩写,在RFC8701[2]中被正式定义,主要的用处是通过设计一种机制,来防止TLS协议在将来进行扩展的时候受到阻碍。

​ 即,在TLS的字段中插入了一些无意义的值:

1
{0x0A,0x0A}, {0x1A,0x1A}, {0x2A,0x2A}, {0x3A,0x3A}, {0x4A,0x4A}, {0x5A,0x5A}, {0x6A,0x6A}, {0x7A,0x7A}, {0x8A,0x8A}, {0x9A,0x9A}, {0xAA,0xAA}, {0xBA,0xBA}, {0xCA,0xCA}, {0xDA,0xDA}, {0xEA,0xEA}, {0xFA,0xFA}

​ 这些值可以由客户端在发送Client Hello时随机插入,通时,服务端必须忽略这些值。假设服务端的消息中出现了GREASE值,则客户端必须断开连接。说白了就是使得开发者们不那么懒惰,对生态进行维护。

知乎作者在文中这样写道:

以TLS为例,如果不通过GREASE对实现方式进行约束,软件逻辑有可能被实现为 CipherSuites,Extension 只支持目前协议中出现的值,如果发现了不在协议中的值就抛出异常。这样实现在当前版本的使用中不会碰到任何问题,但是如果IETF要对TLS协议进行升级,要引入更多CipherSuites值,那样当一个新版本TLS库与旧版本TLS库交互的时候,就会抛出异常,影响业务,这样新版本协议的部署就会遇到很大的阻碍。

​ 回归到TLS指纹来讲,客户端的ja3在实际情况下的计算过程中,会被忽略掉(否则每一次的指纹就都不一样了)。

​ 创建JA3之后,我们可以利用同样的方法对TLS握手的服务器端进行指纹识别,即对TLS Server Hello消息进行指纹识别,字段顺序如下:

1
TLSVersion,Cipher,Extensions

​ 而后计算其hash值即可,那么,这个值有什么意义呢?

​ 由于同一台服务器会根据Client Hello消息及其内容以不同的方式创建其Server Hello消息,因此无法通过一次应答的指纹标记服务器的身份。尽管服务器对不同的客户端的响应是不同的,他们对于同一个客户端的响应却总是一致的:

image-20230521210943819

​ 由上图,可以看到,使用a.py请求后,对待测指纹服务器进行请求多次,对应的Server指纹是不变的,如果更换请求端指纹,响应端指纹也会随之变化。也就是,在服务器和客户端都不变的情况下,会有一套对应的JA3、JA3s指纹对。

以github上开源的攻击框架pupy为例:

image-20230521211952270

​ 其加密算法被硬编码在ssl通信程序中,因此,其将会具有一个独一无二的Client hello中的Cipher Suites字段,即一个独一无二的JA3。此时,攻击者如果注意到了这一点,并尝试改变这一点,比如使用一些公共的库,或者系统套接字进行通信,则可以引入ja3s作为辅助:

​ 由于C2服务器对该客户端应用程序的响应方式与Internet上的普通服务器对该套接字的响应方式相比来说是独一无二的,因此,如果我们结合JA3+JA3,那么我们就能够识别这种恶意通信:

  • Metasploit Win10 至Kali:
    • JA3=72a589da586844d7f0818ce684948eea -> JA3S=70999de61602be74d4b25185843bd18e
  • Cobalt Strike Win10 至 Kali:
    • JA3=72a589da586844d7f0818ce684948eea -> JA3S=b742b407517bac9536a77a7b0fee28e9

三、指纹的修改与随机化

1. 浏览器端对于TLS指纹的改动

​ 一般来说,正常情况下的指纹应该是不变的,使用JA3算法就可以唯一的标识一台客户端主机,然而,在进行深入的测试中,发现了一些问题:

image-20230521115819100

image-20230521115851537

image-20230521120946850

​ 在Chrome 109版本(大概在2023年初开始计划实装此功能)后,TLS extensions变为随机顺序的字段,使得JA3的最终哈希值发生变化,经过查证,这一现象是被确认的:

image-20230521174020233

​ 在这一更改的说明中提到,使用固定的拓展顺序,鼓励了服务器端针对chrome客户端的指纹识别。Chrome此举并不是为了防止TLS的指纹,而是为了增进TLS的生态…,告知内容链接如下:

https://groups.google.com/a/chromium.org/g/blink-dev/c/zdmNs2rTyVI/m/iN_kZokCAwAJ?utm_medium=email&utm_source=footer&pli=1

​ github对这个问题进行了讨论:https://github.com/net4people/bbs/issues/220,其中,有人指出,在专业进行TLS指纹检测的网址TLSFingerprint.io 中,其对于浏览器指纹迅速进行了Normalized(也就是排序)处理:

image-20230521231734759

Normalized后的Chrome又具有了相同的指纹,但是存在两个问题:

  • 丢弃字段顺序信息后,损失了大量的信息,哈希可能的最终值大大减少了;
  • 不同TLS指纹监测站的标准亟待统一;

2. 使用脚本获得随机指纹

​ 当然,由于每一次建立连接的就是客户端本身和服务器本身,其是可以对自己的TLS指纹进行伪装的,以下为github上开源的TLS指纹随机化项目:

https://github.com/netskopeoss/jarm_randomizer

​ 其实现了27种JA3s指纹的随机化方式,项目中提到如下的可能进展:

image-20230521235631201

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import os
import socket
import ssl
from datetime import datetime, timedelta
from OpenSSL import crypto


def generate_self_signed_cert(key_path, cert_path):
# 生成私钥
key = crypto.PKey()
key.generate_key(crypto.TYPE_RSA, 2048)

# 生成证书请求
req = crypto.X509Req()
req.get_subject().CN = "localhost" # 自签名证书的通用名称
req.set_pubkey(key)
req.sign(key, "sha256")

# 生成自签名证书
cert = crypto.X509()
cert.set_subject(req.get_subject())
cert.set_serial_number(1000)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(365 * 24 * 60 * 60) # 有效期一年
cert.set_issuer(cert.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(key, "sha256")

# 将私钥和证书保存到文件
with open(key_path, "wb") as key_file:
key_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
with open(cert_path, "wb") as cert_file:
cert_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))


def learn_tls_config(url):
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_context = ssl.create_default_context()
client_socket.connect((url, 443))
ssl_socket = ssl_context.wrap_socket(client_socket, server_hostname=url)

# 获取并储存Server Hello
learned_server_hello = ssl_socket.recv(4096)

print(learned_server_hello)


def simulate_tls_config(url, key_path, cert_path):
# 加载之前学习的Server Hello消息,这里假设存储在文件中
with open("server_hello.bin", "rb") as file:
server_hello = file.read()

server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.bind(("localhost", 8443))
server_sock.listen(1)

while True:
# 等待客户端连接
client_sock, addr = server_sock.accept()

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)

if os.path.exists(key_path) and os.path.exists(cert_path):
# 使用提供的私钥和证书
context.load_cert_chain(certfile=cert_path, keyfile=key_path)
else:
# 生成自签名证书
generate_self_signed_cert(key_path, cert_path)
context.load_cert_chain(certfile=cert_path, keyfile=key_path)

with context.wrap_socket(client_sock, server_side=True) as ssock:
ssock.sendall(server_hello)

client_sock.close()

​ 使用如上脚本进行JA3s脚本的学习,并伪装成对应服务器的JA3脚本。依然存在的问题是:不能够绕过大型网站对于自动化脚本的检查机制。

四、Network Protocol Fingerprinting (NPF)与排序

1. 其他指纹

​ 网络中除了针对SSL/TLS协议可以进行如上的指纹进行探测,还可以对于多种,具有丰富可选配置的其他协议进行指纹的探测与记录工作,如上,列出了一些已有的,较为热门的指纹。

​ 然而,一种指纹探测就使用一个程序来完成,始终缺少一个统一的规则集合,就像TLS指纹,多个指纹库要实现信息之间的同步,必须要对于指纹生成方式有一个统一的认知,这就是为什么我们需要NPF。

2.npf

npf被制定为一种针对网络中协议进行指纹提取的规则表示方法,下面以上文介绍过的TLS指纹进行简单的解释:

image-20230522005410166

​ 使用tls/1表示基于tls修正一次后的标准,如上图,区别在于使用了大括号表示排序,下文给出了Extension完整的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
DEGREASE(x) = 0x0a0a if x is in TLS_GREASE, and x otherwise.
TLS_GREASE = {
0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a,
0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa
}

TLS_EXT_FIXED = {
0x0001, 0x0005, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000d,
0x000f, 0x0010, 0x0011, 0x0018, 0x001b, 0x001c, 0x002b, 0x002d,
0x0032, 0x5500
}.
TLS_Extension = extension
if DEGREASE(extension[0:2]) is in TLS_EXT_FIXED, and DEGREASE(extension[0:2]) otherwise.

​ 实际上对于tls的定义就是一个类,然后,在实际情况下,任意对象可以使用如下语法嵌入这个类,如下图为修改前的tls某次消息的指纹对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tls/
(0303)
(130213031301c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c0130033009d009c003d003c0035002f00ff)
(
(0000)
(000b000403000102)
(000a000c000a001d0017001e00190018)
(0023)
(0016)
(0017) (000d0030002e040305030603080708080809080a080b080408050806040105010601030302030301020103020202040205020602)
(002b0009080304030303020301)
(002d00020101)
(0033)
)

修改后的,如下所示,使用字典序进行了排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tls/1/
(0303)
(130213031301c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c0130033009d009c003d003c0035002f00ff)
[
(0000)
(000a000c000a001d0017001e00190018)
(000b000403000102)
(000d0030002e040305030603080708080809080a080b080408050806040105010601030302030301020103020202040205020602)
(0016)
(0017)
(0023)
(002b0009080304030303020301)
(002d00020101)
(0033)
]

下面是http、openvpn和tcp/ip协议的指纹定义:

1
2
3
4
5
"http/" (method) (version) ((selected-header)*)

"tcp/" (IP_Version)(IP_ID)(IP_TTL)(TCP_Window)((TCP_Option)*)

"openvpn/" (ip_proto) (num_pkt) ({opcode}{key_id}) (HMAC_len) (TLS_Ciphersuites) ((TLS_Extension)*)

​ 通过npf规则约束,可以构建基于不同协议的指纹库,并指定对应的规则。

排序后的TLS指纹

排序带来的影响?

那么,排序是否影响了TLS的实际作用呢?即,进行排序后,到底对于TLS jarm指纹的影响有多大?

image-20230522114829449

​ hnull的研究者提到,在源本包含2149个指纹种类的集合中,将tls进行排序后,指纹数量下降为2123个,即98.8%的指纹在排序后依然保持独立。

普遍采用随机化可能导致的安全问题?

  • 给代码带了了复杂性(如心脏出血漏洞),并不关键
  • 可能作为C&C消息通道,秘密传输盗窃信息

如下为元素数量,排列数,与能传输信息长度的关系

n(元素数量) _ (排列数) lg( n !) (位数)
5个 120 6.9
10 3628800 21.8
15 1307674368000 40.3
20 2432902008176640000 61.1
25 15511210043330985984000000 83.7
30 265252859812191058636308480000000 107.7
35 10333147966386144929666651337523200000000 132.9

​ Chrome 常见的 18 个 TLS 扩展的单个排列可以用作攻击者进行信息传输的通道,可以泄漏 lg(18!) = 52.5 位信息,这已经是一个非常大的数量了。假设其他ssl应用也跟随进行随机化,可能为攻击者提供新的信息通道。多次使用这一通道,可以泄露相当长的秘密信息,假设要传递的秘密信息是呈不自然序列的,可以先进行加密,而后在传递后解密;反之,假设秘密信息趋向于均匀随机,则可以直接进行传递。(这一安全问题建立在服务端有内鬼的假设上)

​ 另一个问题是,服务端可能使用TLS随机化的方式对用户进行跟踪,如下,RFC提到,用户对于会话票据的重用可能会使用服务器跟踪用户。特别设计的随机化算法可以类似session一样标记用户。

image-20230522123055484

https://hnull.org/2022/12/01/sorting-out-randomized-tls-fingerprints/#effectiveness