Created
February 23, 2024 20:32
-
-
Save anthonykasza/2a0da8deb7ba13d782e31afd876df77b to your computer and use it in GitHub Desktop.
Python calls tshark on a pcap to generate ClientHello fingerprints
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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