Skip to content

Instantly share code, notes, and snippets.

@anthonykasza
Created February 23, 2024 20:32
Show Gist options
  • Save anthonykasza/2a0da8deb7ba13d782e31afd876df77b to your computer and use it in GitHub Desktop.
Save anthonykasza/2a0da8deb7ba13d782e31afd876df77b to your computer and use it in GitHub Desktop.
Python calls tshark on a pcap to generate ClientHello fingerprints
# Python calls tshark on a pcap to generate ClientHello fingerprint
# This script only supports TLS, not SSL
import argparse
from publicsuffix2 import get_tld
from hashlib import sha256
import subprocess
import sys
GREASERS = [
0x0a0a, 0x1a1a, 0x2a2a,
0x3a3a, 0x4a4a, 0x5a5a,
0x6a6a, 0x7a7a, 0x8a8a,
0x9a9a, 0xaaaa, 0xbaba,
0xcaca, 0xdada, 0xeaea,
0xfafa
]
VERSIONS = {
1: "s1",
2: "s2",
0x0300: "s3",
0x0301: "10",
0x0302: "11",
0x0303: "12",
0x0304: "13"
}
def get_clienthellos(pcap_fn):
cmd = ["tshark", "-nnr", pcap_fn, "-Y", "tls.handshake.type==1", "-T", "fields", "-e", "frame.number"]
output = subprocess.check_output(cmd)
output = output.decode('ascii').strip().replace("\n", ",")
return output.split(',')
def get_field(pcap_fn, frames, field, grease_trap=True):
cmd = ["tshark", "-nnr", pcap_fn]
frame_filter = ""
for number in frames:
frame_filter += "frame.number=={}||".format(number)
frame_filter = frame_filter.strip('||')
cmd += ["-Y", frame_filter]
cmd += ["-Y", "tls.handshake.type==1", "-T", "fields", "-e", field]
output = subprocess.check_output(cmd)
output = output.decode('ascii').strip().split("\n")
if grease_trap:
return_me = []
foo = []
for each in output:
if field=="tls.handshake.extension.type":
foo = [int(code) for code in each.split(',') if code]
else:
foo = [int(cs_code, 16) for cs_code in each.split(',') if cs_code]
foo = [cs_code for cs_code in foo if cs_code not in GREASERS]
return_me.append(foo)
return return_me
return output
def main():
parser = argparse.ArgumentParser(description=("make the fingerprints"))
parser.add_argument("pcap_fn", help="pcap to chew on")
parser.add_argument("-r", "--raw_output", required=False, action="store_true", help="raw fingerprint")
parser.add_argument("-o", "--original_ordering", required=False, action="store_true", help="original ordering")
try:
args = parser.parse_args()
except:
print(parser.print_help())
DELIMITER = "_"
frames = get_clienthellos(args.pcap_fn)
# TODO test with non-tcp. quic? gquic? no lo so.
protocols = get_field(args.pcap_fn, frames, "tcp", grease_trap=False)
ciphers = get_field(args.pcap_fn, frames, "tls.handshake.ciphersuites")
versions = get_field(args.pcap_fn, frames, "tls.handshake.extensions.supported_version")
snis = get_field(args.pcap_fn, frames, "tls.handshake.extensions_server_name", grease_trap=False)
alpns = get_field(args.pcap_fn, frames, "tls.handshake.extensions_alpn_str", grease_trap=False)
alpns = [each.split(",") for each in alpns]
extensions = get_field(args.pcap_fn, frames, "tls.handshake.extension.type")
sign_algos = get_field(args.pcap_fn, frames, "tls.handshake.sig_hash_alg")
dst_ip = get_field(args.pcap_fn, frames, "ip.dst", grease_trap=False)
A = []*len(frames)
for idx in range(len(frames)):
A.append("")
if protocols[idx]:
A[idx] += "t"
else:
A[idx] += "q"
if len(versions[idx]) == 0:
versions = get_field(args.pcap_fn, frames, "tls.handshake.version")
A[idx] += VERSIONS[max(versions[idx])]
try:
if len(snis) > 0 and len(snis[idx]) > 0 and len(snis[idx][0]) > 0 and snis[idx][0] != "0" and snis[idx][0] != dst_ip[idx]:
A[idx] += "d"
else:
A[idx] += "i"
except:
A[idx] += "i"
try:
if len(ciphers) < 100:
A[idx] += "{:02d}".format(len(ciphers[idx]))
else:
A[idx] += "99"
except:
A[idx] += "00"
try:
if len(extensions[idx]) < 100:
A[idx] += "{:02d}".format(len(extensions[idx]))
else:
A[idx] += "99"
except:
A[idx] += "00"
try:
if len(alpns[idx]) > 0 and len(alpns[idx][0]) > 0:
A[idx] += alpns[idx][0][0]+alpns[idx][0][-1]
else:
A[idx] += "00"
except:
A[idx] += "00"
B = []*len(frames)
for idx in range(len(frames)):
B.append("")
c_string = ""
if args.original_ordering:
c_string = ",".join( ["{:04x}".format(each) for each in ciphers[idx]] )
else:
c_string = ",".join( ["{:04x}".format(each) for each in sorted(ciphers[idx])] )
if args.raw_output:
B[idx] += c_string
else:
B[idx] += sha256(c_string.encode('utf-8')).hexdigest()[:12]
C = []*len(frames)
for idx in range(len(frames)):
C.append("")
e_string = ""
filtered_extensions = [code for code in extensions[idx] if code not in [0x0000, 0x0010]]
if args.original_ordering:
e_string = ",".join( ["{:04x}".format(each) for each in filtered_extensions] )
else:
e_string = ",".join( ["{:04x}".format(each) for each in sorted(filtered_extensions)] )
e_string += DELIMITER
e_string += ",".join( ["{:04x}".format(each) for each in sign_algos[idx]] )
if args.raw_output:
C[idx] += e_string
else:
C[idx] += sha256(e_string.encode('utf-8')).hexdigest()[:12]
for idx in range(len(frames)):
print(A[idx] + DELIMITER + B[idx] + DELIMITER + C[idx])
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment