diff --git a/docker/fatt/Dockerfile b/docker/fatt/Dockerfile index bf257481..dcb898eb 100644 --- a/docker/fatt/Dockerfile +++ b/docker/fatt/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.17 +FROM alpine:3.18 # # Get and install dependencies & packages RUN apk -U --no-cache add \ @@ -21,11 +21,8 @@ RUN apk -U --no-cache add \ git clone https://github.com/0x4D31/fatt && \ cd fatt && \ git checkout c29e553514281e50781f86932b82337a5ada5640 && \ - #git checkout 45cabf0b8b59162b99a1732d853efb01614563fe && \ - #git checkout 314cd1ff7873b5a145a51ec4e85f6107828a2c79 && \ mkdir -p log && \ - # pyshark >= 0.4.3 breaks fatt - pip3 install pyshark==0.4.2.11 && \ + pip3 install pyshark==0.6 && \ # # Setup configs chgrp fatt /usr/bin/dumpcap && \ @@ -37,6 +34,9 @@ RUN apk -U --no-cache add \ python3-dev && \ rm -rf /root/* /var/cache/apk/* /opt/fatt/.git # +# Workaround issue fatt/#9 +COPY dist/fatt.py /opt/fatt/ +# # Start fatt STOPSIGNAL SIGINT ENV PYTHONPATH /opt/fatt diff --git a/docker/fatt/dist/fatt.py b/docker/fatt/dist/fatt.py new file mode 100644 index 00000000..d6710095 --- /dev/null +++ b/docker/fatt/dist/fatt.py @@ -0,0 +1,1045 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019, Adel "0x4d31" Karimi. +# All rights reserved. +# Licensed under the BSD 3-Clause license. +# For full license text, see the LICENSE file in the repo root +# or https://opensource.org/licenses/BSD-3-Clause + +# fatt. Fingerprint All The Things +# Supported protocols: SSL/TLS, SSH, RDP, HTTP, gQUIC + +import argparse +import pyshark +import os +import json +import logging +import struct +from hashlib import md5 +from collections import defaultdict + +__author__ = "Adel '0x4D31' Karimi" +__version__ = "1.0" + + +CAP_BPF_FILTER = ( + 'tcp port 22 or tcp port 2222 or tcp port 3389 or ' + 'tcp port 443 or tcp port 993 or tcp port 995 or ' + 'tcp port 636 or tcp port 990 or tcp port 992 or ' + 'tcp port 989 or tcp port 563 or tcp port 614 or ' + 'tcp port 3306 or tcp port 80 or udp port 80 or ' + 'udp port 443') +DISPLAY_FILTER = ( + 'tls.handshake.type == 1 || tls.handshake.type == 2 ||' + 'ssh.message_code == 20 || ssh.protocol || rdp ||' + '(quic && tls.handshake.type == 1) || gquic.tag == "CHLO" ||' + 'http.request.method || data-text-lines' +) +DECODE_AS = { + 'tcp.port==2222': 'ssh', 'tcp.port==3389': 'tpkt', + 'tcp.port==993': 'tls', 'tcp.port==995': 'tls', + 'tcp.port==990': 'tls', 'tcp.port==992': 'tls', + 'tcp.port==989': 'tls', 'tcp.port==563': 'tls', + 'tcp.port==614': 'tls', 'tcp.port==636': 'tls'} +HASSH_VERSION = '1.0' +RDFP_VERSION = '0.3' + + +class ProcessPackets: + + def __init__(self, fingerprint, jlog, pout): + self.logger = logging.getLogger() + self.fingerprint = fingerprint + self.jlog = jlog + self.pout = pout + self.protocol_dict = {} + self.rdp_dict = defaultdict(dict) + + def process(self, packet): + record = None + proto = packet.highest_layer + sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src + destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst + + # Clear the dictionary used for extracting ssh protocol strings + # and rdp cookies/negotiateRequests + if len(self.protocol_dict) > 100 and proto != 'SSH': + self.protocol_dict.clear() + if len(self.rdp_dict) > 100 and proto != 'RDP': + self.rdp_dict.clear() + + # [ SSH ] + if proto == 'SSH' and ('ssh' in self.fingerprint or + self.fingerprint == 'all'): + # Extract SSH identification string and correlate with KEXINIT msg + if 'protocol' in packet.ssh.field_names: + key = '{}:{}_{}:{}'.format( + sourceIp, + packet.tcp.srcport, + destinationIp, + packet.tcp.dstport) + self.protocol_dict[key] = packet.ssh.protocol + if 'message_code' not in packet.ssh.field_names: + return + if packet.ssh.message_code != '20': + return + # log the anomalous / retransmission packets + if ("analysis_retransmission" in packet.tcp.field_names or + "analysis_spurious_retransmission" in packet.tcp.field_names): + event = event_log(packet, event="retransmission") + if record and self.jlog: + self.logger.info(json.dumps(event)) + return + # Client HASSH + if int(packet.tcp.srcport) > int(packet.tcp.dstport): + record = self.client_hassh(packet) + # Print the result + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [SSH] hassh={hassh} client={client}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + client=record['ssh']['client'], + hassh=record['ssh']['hassh'], + ) + print(tmp) + # Server HASSH + elif int(packet.tcp.srcport) < int(packet.tcp.dstport): + record = self.server_hassh(packet) + # Print the result + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [SSH] hasshS={hasshs} server={server}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + server=record['ssh']['server'], + hasshs=record['ssh']['hasshServer'], + ) + print(tmp) + if record and self.jlog: + self.logger.info(json.dumps(record)) + return + + # [ TLS ] + # TODO: extract tls certificates + elif proto == 'TLS' and ('tls' in self.fingerprint or + self.fingerprint == 'all'): + if 'record_content_type' not in packet.tls.field_names: + return + # Content Type: Handshake (22) + if packet.tls.record_content_type != '22': + return + # Handshake Type: Client Hello (1) / Server Hello (2) + if 'handshake_type' not in packet.tls.field_names: + return + htype = packet.tls.handshake_type + if not (htype == '1' or htype == '2'): + return + # log the anomalous / retransmission packets + if ("analysis_retransmission" in packet.tcp.field_names or + "analysis_spurious_retransmission" in packet.tcp.field_names): + event = event_log(packet, event="retransmission") + # JA3 + if htype == '1': + record = self.client_ja3(packet) + # Print the result + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [TLS] ja3={ja3} serverName={sname}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + sname=record['tls']['serverName'], + ja3=record['tls']['ja3'], + ) + print(tmp) + elif htype == '2': + record = self.server_ja3(packet) + # Print the result + if self.pout: + tmp = ( + '{sip}:{sp} -> {dip}:{dp} [TLS] ja3s={ja3s}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + ja3s=record['tls']['ja3s'], + ) + print(tmp) + if record and self.jlog: + self.logger.info(json.dumps(record)) + return + + # [ RDP ] + elif proto == 'RDP' and ('rdp' in self.fingerprint or + self.fingerprint == 'all'): + # Extract RDP cookie & negotiate request and correlate with ClientData msg + key = None + if 'rt_cookie' or 'negreq_requestedprotocols': + key = '{}:{}_{}:{}'.format( + sourceIp, + packet.tcp.srcport, + destinationIp, + packet.tcp.dstport) + if 'rt_cookie' in packet.rdp.field_names: + cookie = packet.rdp.rt_cookie.replace('Cookie: ', '') + self.rdp_dict[key]["cookie"] = cookie + if 'negreq_requestedprotocols' in packet.rdp.field_names: + req_protos = packet.rdp.negreq_requestedprotocols + self.rdp_dict[key]["req_protos"] = req_protos + # TLS/CredSSP (not standard RDP security protocols) + if req_protos != "0x00000000": + record = { + "timestamp": packet.sniff_time.isoformat(), + "sourceIp": sourceIp, + "destinationIp": destinationIp, + "sourcePort": packet.tcp.srcport, + "destinationPort": packet.tcp.dstport, + "protocol": "rdp", + "rdp": { + "requestedProtocols": req_protos + } + } + if self.pout: + tmp = ( + '{sip}:{sp} -> {dip}:{dp} [RDP] req_protocols={proto}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + proto=record['rdp']['requestedProtocols'] + ) + print(tmp) + if self.jlog: + self.logger.info(json.dumps(record)) + if 'clientdata' not in packet.rdp.field_names: + return + if ("analysis_retransmission" in packet.tcp.field_names or + "analysis_spurious_retransmission" in packet.tcp.field_names): + event = event_log(packet, event="retransmission") + if self.jlog: + self.logger.info(json.dumps(event)) + return + # Client RDFP + record = self.client_rdfp(packet) + # Print the result + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [RDP] rdfp={rdfp} cookie="{cookie}" req_protocols={proto}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + rdfp=record['rdp']['rdfp'], + cookie=record['rdp']['cookie'], + proto=record['rdp']['requestedProtocols'] + ) + print(tmp) + if record and self.jlog: + self.logger.info(json.dumps(record)) + return + + # [ HTTP ] + elif (proto == 'HTTP' or proto == 'DATA-TEXT-LINES') and \ + ('http' in self.fingerprint or self.fingerprint == 'all'): + if 'request' in packet.http.field_names: + record = self.client_http(packet) + # Print the result + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [HTTP] hash={hash} userAgent="{ua}"') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + hash=record['http']['clientHeaderHash'], + ua=record['http']['userAgent'], + ) + print(tmp) + elif 'response' in packet.http.field_names: + record = self.server_http(packet) + # Print the result + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [HTTP] hash={hash} server={server}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + hash=record['http']['serverHeaderHash'], + server=record['http']['server'], + ) + print(tmp) + if record and self.jlog: + self.logger.info(json.dumps(record)) + return + + # [ GQUIC ] + elif proto == 'GQUIC' and ('gquic' in self.fingerprint or + self.fingerprint == 'all'): + if 'tag' in packet.gquic.field_names: + if packet.gquic.tag == 'CHLO': + record = self.client_gquic(packet) + # Print the result + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [GQUIC] UAID="{ua}" SNI={sn} AEAD={ea} KEXS={kex}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + ua=record['gquic']['uaid'], + sn=record['gquic']['sni'], + ea=record['gquic']['aead'], + kex=record['gquic']['kexs'] + ) + print(tmp) + if record and self.jlog: + self.logger.info(json.dumps(record)) + return + + # [ QUIC ] + elif proto == 'QUIC' and ('quic' in self.fingerprint or + self.fingerprint == 'all'): + + if packet.quic.tls_handshake_type == '1': + record = self.client_quic(packet) + + if self.pout: + tmp = ('{sip}:{sp} -> {dip}:{dp} [QUIC] serverName="{sn}" VER={ver}') + tmp = tmp.format( + sip=record['sourceIp'], + sp=record['sourcePort'], + dip=record['destinationIp'], + dp=record['destinationPort'], + ver=record['quic']['ver'], + sn=record['quic']['sni'] + ) + print(tmp) + if record and self.jlog: + self.logger.info(json.dumps(record)) + return + + return + + def client_hassh(self, packet): + """returns HASSH (i.e. SSH Client Fingerprint) + HASSH = md5(KEX;EACTS;MACTS;CACTS) + """ + protocol = None + sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src + destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst + key = '{}:{}_{}:{}'.format( + sourceIp, + packet.tcp.srcport, + destinationIp, + packet.tcp.dstport) + if key in self.protocol_dict: + protocol = self.protocol_dict[key] + # hassh fields + ckex = ceacts = cmacts = ccacts = "" + if 'kex_algorithms' in packet.ssh.field_names: + ckex = packet.ssh.kex_algorithms + if 'encryption_algorithms_client_to_server' in packet.ssh.field_names: + ceacts = packet.ssh.encryption_algorithms_client_to_server + if 'mac_algorithms_client_to_server' in packet.ssh.field_names: + cmacts = packet.ssh.mac_algorithms_client_to_server + if 'compression_algorithms_client_to_server' in packet.ssh.field_names: + ccacts = packet.ssh.compression_algorithms_client_to_server + # Log other kexinit fields (only in JSON) + clcts = clstc = ceastc = cmastc = ccastc = cshka = "" + if 'languages_client_to_server' in packet.ssh.field_names: + clcts = packet.ssh.languages_client_to_server + if 'languages_server_to_client' in packet.ssh.field_names: + clstc = packet.ssh.languages_server_to_client + if 'encryption_algorithms_server_to_client' in packet.ssh.field_names: + ceastc = packet.ssh.encryption_algorithms_server_to_client + if 'mac_algorithms_server_to_client' in packet.ssh.field_names: + cmastc = packet.ssh.mac_algorithms_server_to_client + if 'compression_algorithms_server_to_client' in packet.ssh.field_names: + ccastc = packet.ssh.compression_algorithms_server_to_client + if 'server_host_key_algorithms' in packet.ssh.field_names: + cshka = packet.ssh.server_host_key_algorithms + # Create hassh + hassh_str = ';'.join([ckex, ceacts, cmacts, ccacts]) + hassh = md5(hassh_str.encode()).hexdigest() + record = { + "timestamp": packet.sniff_time.isoformat(), + "sourceIp": sourceIp, + "destinationIp": destinationIp, + "sourcePort": packet.tcp.srcport, + "destinationPort": packet.tcp.dstport, + "protocol": 'ssh', + "ssh": { + "client": protocol, + "hassh": hassh, + "hasshAlgorithms": hassh_str, + "hasshVersion": HASSH_VERSION, + "ckex": ckex, + "ceacts": ceacts, + "cmacts": cmacts, + "ccacts": ccacts, + "clcts": clcts, + "clstc": clstc, + "ceastc": ceastc, + "cmastc": cmastc, + "ccastc": ccastc, + "cshka": cshka + } + } + return record + + def server_hassh(self, packet): + """returns HASSHServer (i.e. SSH Server Fingerprint) + HASSHServer = md5(KEX;EASTC;MASTC;CASTC) + """ + protocol = None + sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src + destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst + key = '{}:{}_{}:{}'.format( + sourceIp, + packet.tcp.srcport, + destinationIp, + packet.tcp.dstport) + if key in self.protocol_dict: + protocol = self.protocol_dict[key] + # hasshServer fields + skex = seastc = smastc = scastc = "" + if 'kex_algorithms' in packet.ssh.field_names: + skex = packet.ssh.kex_algorithms + if 'encryption_algorithms_server_to_client' in packet.ssh.field_names: + seastc = packet.ssh.encryption_algorithms_server_to_client + if 'mac_algorithms_server_to_client' in packet.ssh.field_names: + smastc = packet.ssh.mac_algorithms_server_to_client + if 'compression_algorithms_server_to_client' in packet.ssh.field_names: + scastc = packet.ssh.compression_algorithms_server_to_client + # Log other kexinit fields (only in JSON) + slcts = slstc = seacts = smacts = scacts = sshka = "" + if 'languages_client_to_server' in packet.ssh.field_names: + slcts = packet.ssh.languages_client_to_server + if 'languages_server_to_client' in packet.ssh.field_names: + slstc = packet.ssh.languages_server_to_client + if 'encryption_algorithms_client_to_server' in packet.ssh.field_names: + seacts = packet.ssh.encryption_algorithms_client_to_server + if 'mac_algorithms_client_to_server' in packet.ssh.field_names: + smacts = packet.ssh.mac_algorithms_client_to_server + if 'compression_algorithms_client_to_server' in packet.ssh.field_names: + scacts = packet.ssh.compression_algorithms_client_to_server + if 'server_host_key_algorithms' in packet.ssh.field_names: + sshka = packet.ssh.server_host_key_algorithms + # Create hasshServer + hasshs_str = ';'.join([skex, seastc, smastc, scastc]) + hasshs = md5(hasshs_str.encode()).hexdigest() + record = { + "timestamp": packet.sniff_time.isoformat(), + "sourceIp": sourceIp, + "destinationIp": destinationIp, + "sourcePort": packet.tcp.srcport, + "destinationPort": packet.tcp.dstport, + "protocol": 'ssh', + "ssh": { + "server": protocol, + "hasshServer": hasshs, + "hasshServerAlgorithms": hasshs_str, + "hasshVersion": HASSH_VERSION, + "skex": skex, + "seastc": seastc, + "smastc": smastc, + "scastc": scastc, + "slcts": slcts, + "slstc": slstc, + "seacts": seacts, + "smacts": smacts, + "scacts": scacts, + "sshka": sshka + } + } + return record + + def client_ja3(self, packet): + # GREASE_TABLE Ref: https://tools.ietf.org/html/draft-davidben-tls-grease-00 + GREASE_TABLE = ['2570', '6682', '10794', '14906', '19018', '23130', + '27242', '31354', '35466', '39578', '43690', '47802', + '51914', '56026', '60138', '64250'] + # ja3 fields + tls_version = ciphers = extensions = elliptic_curve = ec_pointformat = "" + if 'handshake_version' in packet.tls.field_names: + tls_version = int(packet.tls.handshake_version, 16) + tls_version = str(tls_version) + if 'handshake_ciphersuite' in packet.tls.field_names: + cipher_list = [ + c.show for c in packet.tls.handshake_ciphersuite.fields + if c.show not in GREASE_TABLE] + ciphers = '-'.join(cipher_list) + if 'handshake_extension_type' in packet.tls.field_names: + extension_list = [ + e.show for e in packet.tls.handshake_extension_type.fields + if e.show not in GREASE_TABLE] + extensions = '-'.join(extension_list) + if 'handshake_extensions_supported_group' in packet.tls.field_names: + ec_list = [str(int(ec.show, 16)) for ec in + packet.tls.handshake_extensions_supported_group.fields + if str(int(ec.show, 16)) not in GREASE_TABLE] + elliptic_curve = '-'.join(ec_list) + if 'handshake_extensions_ec_point_format' in packet.tls.field_names: + ecpf_list = [ecpf.show for ecpf in + packet.tls.handshake_extensions_ec_point_format.fields + if ecpf.show not in GREASE_TABLE] + ec_pointformat = '-'.join(ecpf_list) + # TODO: log other non-ja3 fields + server_name = "" + if 'handshake_extensions_server_name' in packet.tls.field_names: + server_name = packet.tls.handshake_extensions_server_name + # Create ja3 + ja3_string = ','.join([ + tls_version, ciphers, extensions, elliptic_curve, ec_pointformat]) + ja3 = md5(ja3_string.encode()).hexdigest() + sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src + destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst + record = { + "timestamp": packet.sniff_time.isoformat(), + "sourceIp": sourceIp, + "destinationIp": destinationIp, + "sourcePort": packet.tcp.srcport, + "destinationPort": packet.tcp.dstport, + "protocol": "tls", + "tls": { + "serverName": server_name, + "ja3": ja3, + "ja3Algorithms": ja3_string, + "ja3Version": tls_version, + "ja3Ciphers": ciphers, + "ja3Extensions": extensions, + "ja3Ec": elliptic_curve, + "ja3EcFmt": ec_pointformat + } + } + return record + + def server_ja3(self, packet): + # GREASE_TABLE Ref: https://tools.ietf.org/html/draft-davidben-tls-grease-00 + GREASE_TABLE = ['2570', '6682', '10794', '14906', '19018', '23130', + '27242', '31354', '35466', '39578', '43690', '47802', + '51914', '56026', '60138', '64250'] + # ja3s fields + tls_version = ciphers = extensions = "" + if 'handshake_version' in packet.tls.field_names: + tls_version = int(packet.tls.handshake_version, 16) + tls_version = str(tls_version) + if 'handshake_ciphersuite' in packet.tls.field_names: + cipher_list = [ + c.show for c in packet.tls.handshake_ciphersuite.fields + if c.show not in GREASE_TABLE] + ciphers = '-'.join(cipher_list) + if 'handshake_extension_type' in packet.tls.field_names: + extension_list = [ + e.show for e in packet.tls.handshake_extension_type.fields + if e.show not in GREASE_TABLE] + extensions = '-'.join(extension_list) + # TODO: log other non-ja3s fields + # Create ja3s + ja3s_string = ','.join([ + tls_version, ciphers, extensions]) + ja3s = md5(ja3s_string.encode()).hexdigest() + sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src + destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst + record = { + "timestamp": packet.sniff_time.isoformat(), + "sourceIp": sourceIp, + "destinationIp": destinationIp, + "sourcePort": packet.tcp.srcport, + "destinationPort": packet.tcp.dstport, + "protocol": "tls", + "tls": { + "ja3s": ja3s, + "ja3sAlgorithms": ja3s_string, + "ja3sVersion": tls_version, + "ja3sCiphers": ciphers, + "ja3sExtensions": extensions + } + } + return record + + def client_rdfp(self, packet): + """returns ClientData message fields and RDFP (experimental fingerprint) + RDFP = md5(verMajor,verMinor,clusterFlags,encryptionMethods,extEncMethods,channelDef) + """ + # RDP fields + verMajor = verMinor = desktopWidth = desktopHeight = colorDepth = \ + sasSequence = keyboardLayout = clientBuild = clientName = \ + keyboardSubtype = keyboardType = keyboardFuncKey = postbeta2ColorDepth \ + = clientProductId = serialNumber = highColorDepth = \ + supportedColorDepths = earlyCapabilityFlags = clientDigProductId = \ + connectionType = pad1Octet = clusterFlags = encryptionMethods = \ + extEncMethods = channelCount = channelDef = cookie = req_protos = "" + + sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src + destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst + key = '{}:{}_{}:{}'.format( + sourceIp, + packet.tcp.srcport, + destinationIp, + packet.tcp.dstport) + if key in self.rdp_dict and "cookie" in self.rdp_dict[key]: + cookie = self.rdp_dict[key]["cookie"] + if key in self.rdp_dict and "req_protos" in self.rdp_dict[key]: + req_protos = self.rdp_dict[key]["req_protos"] + + # Client Core Data + # https://msdn.microsoft.com/en-us/library/cc240510.aspx + if 'version_major' in packet.rdp.field_names: + verMajor = packet.rdp.version_major + if 'version_minor' in packet.rdp.field_names: + verMinor = packet.rdp.version_minor + if 'desktop_width' in packet.rdp.field_names: + desktopWidth = packet.rdp.desktop_width + if 'desktop_height' in packet.rdp.field_names: + desktopHeight = packet.rdp.desktop_height + if 'colordepth' in packet.rdp.field_names: + colorDepth = packet.rdp.colordepth + if 'sassequence' in packet.rdp.field_names: + sasSequence = packet.rdp.sassequence + if 'keyboardlayout' in packet.rdp.field_names: + keyboardLayout = packet.rdp.keyboardlayout + if 'client_build' in packet.rdp.field_names: + clientBuild = packet.rdp.client_build + if 'client_name' in packet.rdp.field_names: + clientName = packet.rdp.client_name + if 'keyboard_subtype' in packet.rdp.field_names: + keyboardSubtype = packet.rdp.keyboard_subtype + if 'keyboard_type' in packet.rdp.field_names: + keyboardType = packet.rdp.keyboard_type + if 'keyboard_functionkey' in packet.rdp.field_names: + keyboardFuncKey = packet.rdp.keyboard_functionkey + if 'postbeta2colordepth' in packet.rdp.field_names: + postbeta2ColorDepth = packet.rdp.postbeta2colordepth + if 'client_productid' in packet.rdp.field_names: + clientProductId = packet.rdp.client_productid + if 'serialnumber' in packet.rdp.field_names: + serialNumber = packet.rdp.serialnumber + if 'highcolordepth' in packet.rdp.field_names: + highColorDepth = packet.rdp.highcolordepth + if 'supportedcolordepths' in packet.rdp.field_names: + supportedColorDepths = packet.rdp.supportedcolordepths + if 'earlycapabilityflags' in packet.rdp.field_names: + earlyCapabilityFlags = packet.rdp.earlycapabilityflags + if 'client_digproductid' in packet.rdp.field_names: + clientDigProductId = packet.rdp.client_digproductid + if 'connectiontype' in packet.rdp.field_names: + connectionType = packet.rdp.connectiontype + if 'pad1octet' in packet.rdp.field_names: + pad1Octet = packet.rdp.pad1octet.raw_value + + # Client Cluster Data + # https://msdn.microsoft.com/en-us/library/cc240514.aspx + if 'clusterflags' in packet.rdp.field_names: + clusterFlags_raw = packet.rdp.clusterflags.raw_value + # convert to little-endian + clusterFlags = struct.pack('