316 lines
12 KiB
Python
316 lines
12 KiB
Python
"""
|
|
Ethernet Data decoders for Chapter 10 data types
|
|
Supports Ethernet Data Formats 0-1 (0x68-0x69)
|
|
"""
|
|
|
|
import struct
|
|
from typing import Dict, Any, Optional
|
|
from .base import DataTypeDecoder, DecodedPayload
|
|
|
|
|
|
class EthernetDecoder(DataTypeDecoder):
|
|
"""Decoder for Ethernet Data types (0x68-0x69)"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.data_type_base = 0x68
|
|
self.data_type_name = "Ethernet Data"
|
|
self.supported_formats = [0x68, 0x69]
|
|
|
|
def can_decode(self, data_type: int) -> bool:
|
|
return data_type in [0x68, 0x69]
|
|
|
|
def get_data_type_name(self, data_type: int) -> str:
|
|
format_names = {
|
|
0x68: "Ethernet Data Format 0",
|
|
0x69: "Ethernet UDP Payload"
|
|
}
|
|
return format_names.get(data_type, f"Ethernet Format {data_type & 0x0F}")
|
|
|
|
def decode(self, payload: bytes, ch10_header: Dict[str, Any]) -> Optional[DecodedPayload]:
|
|
"""Decode Ethernet payload"""
|
|
data_type = ch10_header.get('data_type', 0)
|
|
|
|
if not self.can_decode(data_type):
|
|
return None
|
|
|
|
if data_type == 0x68:
|
|
return self._decode_ethernet_format0(payload, ch10_header)
|
|
elif data_type == 0x69:
|
|
return self._decode_ethernet_udp_payload(payload, ch10_header)
|
|
|
|
return None
|
|
|
|
def _decode_ethernet_format0(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
|
|
"""Decode Ethernet Format 0 (Full Ethernet Frame)"""
|
|
decoded_data = {}
|
|
errors = []
|
|
|
|
# Parse IPH
|
|
iph = self._parse_intra_packet_header(payload)
|
|
if iph:
|
|
decoded_data.update(iph)
|
|
data_start = iph['data_start']
|
|
else:
|
|
data_start = 0
|
|
errors.append("Failed to parse intra-packet header")
|
|
|
|
# Parse Ethernet Format 0 header
|
|
if data_start + 12 <= len(payload):
|
|
eth_header = self._safe_unpack('<III', payload, data_start)
|
|
if eth_header:
|
|
frame_status = eth_header[2]
|
|
|
|
decoded_data.update({
|
|
'ethernet_timestamp': eth_header[1],
|
|
'frame_status_word': frame_status,
|
|
'frame_length': frame_status & 0x3FFF,
|
|
'length_error': bool(frame_status & 0x8000),
|
|
'crc_error': bool(frame_status & 0x10000),
|
|
'content_type': (frame_status >> 28) & 0x3
|
|
})
|
|
|
|
# Decode content type
|
|
content_types = {
|
|
0: "Full MAC frame",
|
|
1: "Payload only",
|
|
2: "Reserved",
|
|
3: "Reserved"
|
|
}
|
|
decoded_data['content_type_description'] = content_types.get(
|
|
decoded_data['content_type'], "Unknown"
|
|
)
|
|
|
|
# Extract Ethernet frame data
|
|
frame_data_start = data_start + 12
|
|
frame_length = decoded_data['frame_length']
|
|
|
|
if frame_data_start + frame_length <= len(payload):
|
|
frame_data = payload[frame_data_start:frame_data_start + frame_length]
|
|
|
|
# Parse Ethernet header if full MAC frame
|
|
if decoded_data['content_type'] == 0 and len(frame_data) >= 14:
|
|
eth_parsed = self._parse_ethernet_header(frame_data)
|
|
decoded_data.update(eth_parsed)
|
|
else:
|
|
decoded_data['raw_frame_data'] = frame_data[:64].hex() # First 64 bytes
|
|
|
|
decoded_data['actual_frame_length'] = len(frame_data)
|
|
else:
|
|
errors.append("Frame data extends beyond payload")
|
|
else:
|
|
errors.append("Failed to parse Ethernet Format 0 header")
|
|
else:
|
|
errors.append("Insufficient data for Ethernet header")
|
|
|
|
return DecodedPayload(
|
|
data_type=0x68,
|
|
data_type_name="Ethernet Data Format 0",
|
|
format_version=0,
|
|
decoded_data=decoded_data,
|
|
raw_payload=payload,
|
|
errors=errors,
|
|
metadata={'decoder': 'EthernetDecoder'}
|
|
)
|
|
|
|
def _decode_ethernet_udp_payload(self, payload: bytes, ch10_header: Dict[str, Any]) -> DecodedPayload:
|
|
"""Decode Ethernet UDP Payload (Format 1)"""
|
|
decoded_data = {}
|
|
errors = []
|
|
|
|
# Parse IPH
|
|
iph = self._parse_intra_packet_header(payload)
|
|
if iph:
|
|
decoded_data.update(iph)
|
|
data_start = iph['data_start']
|
|
else:
|
|
data_start = 0
|
|
errors.append("Failed to parse intra-packet header")
|
|
|
|
# Parse UDP payload header
|
|
if data_start + 16 <= len(payload):
|
|
udp_header = self._safe_unpack('<IIHHHH', payload, data_start)
|
|
if udp_header:
|
|
decoded_data.update({
|
|
'udp_timestamp': udp_header[1],
|
|
'src_ip': self._ip_to_string(udp_header[2]),
|
|
'dst_ip': self._ip_to_string(udp_header[3]),
|
|
'src_port': udp_header[4],
|
|
'dst_port': udp_header[5]
|
|
})
|
|
|
|
# Extract UDP payload
|
|
udp_payload_start = data_start + 16
|
|
if udp_payload_start < len(payload):
|
|
udp_payload = payload[udp_payload_start:]
|
|
decoded_data['udp_payload_length'] = len(udp_payload)
|
|
decoded_data['udp_payload_preview'] = udp_payload[:64].hex()
|
|
|
|
# Try to identify payload content
|
|
payload_info = self._analyze_udp_payload(udp_payload)
|
|
decoded_data.update(payload_info)
|
|
else:
|
|
errors.append("Failed to parse UDP header")
|
|
else:
|
|
errors.append("Insufficient data for UDP header")
|
|
|
|
return DecodedPayload(
|
|
data_type=0x69,
|
|
data_type_name="Ethernet UDP Payload",
|
|
format_version=1,
|
|
decoded_data=decoded_data,
|
|
raw_payload=payload,
|
|
errors=errors,
|
|
metadata={'decoder': 'EthernetDecoder'}
|
|
)
|
|
|
|
def _parse_ethernet_header(self, frame_data: bytes) -> Dict[str, Any]:
|
|
"""Parse Ethernet MAC header"""
|
|
if len(frame_data) < 14:
|
|
return {'eth_parse_error': 'Insufficient data for Ethernet header'}
|
|
|
|
# Parse MAC addresses and EtherType
|
|
dst_mac = frame_data[0:6]
|
|
src_mac = frame_data[6:12]
|
|
ethertype = struct.unpack('>H', frame_data[12:14])[0]
|
|
|
|
eth_data = {
|
|
'dst_mac': ':'.join(f'{b:02x}' for b in dst_mac),
|
|
'src_mac': ':'.join(f'{b:02x}' for b in src_mac),
|
|
'ethertype': f'0x{ethertype:04x}',
|
|
'ethertype_description': self._decode_ethertype(ethertype)
|
|
}
|
|
|
|
# Parse payload based on EtherType
|
|
if ethertype == 0x0800 and len(frame_data) >= 34: # IPv4
|
|
ip_data = self._parse_ip_header(frame_data[14:])
|
|
eth_data.update(ip_data)
|
|
elif ethertype == 0x0806 and len(frame_data) >= 42: # ARP
|
|
arp_data = self._parse_arp_header(frame_data[14:])
|
|
eth_data.update(arp_data)
|
|
|
|
return eth_data
|
|
|
|
def _parse_ip_header(self, ip_data: bytes) -> Dict[str, Any]:
|
|
"""Parse IPv4 header"""
|
|
if len(ip_data) < 20:
|
|
return {'ip_parse_error': 'Insufficient data for IP header'}
|
|
|
|
version_ihl = ip_data[0]
|
|
version = (version_ihl >> 4) & 0x0F
|
|
ihl = version_ihl & 0x0F
|
|
|
|
if version != 4:
|
|
return {'ip_parse_error': f'Unsupported IP version: {version}'}
|
|
|
|
tos, total_length, identification, flags_fragment = struct.unpack('>BHHH', ip_data[1:9])
|
|
ttl, protocol, checksum = struct.unpack('>BBH', ip_data[8:12])
|
|
src_ip = struct.unpack('>I', ip_data[12:16])[0]
|
|
dst_ip = struct.unpack('>I', ip_data[16:20])[0]
|
|
|
|
return {
|
|
'ip_version': version,
|
|
'ip_header_length': ihl * 4,
|
|
'ip_tos': tos,
|
|
'ip_total_length': total_length,
|
|
'ip_id': identification,
|
|
'ip_ttl': ttl,
|
|
'ip_protocol': protocol,
|
|
'ip_src': self._ip_to_string(src_ip),
|
|
'ip_dst': self._ip_to_string(dst_ip),
|
|
'ip_protocol_name': self._decode_ip_protocol(protocol)
|
|
}
|
|
|
|
def _parse_arp_header(self, arp_data: bytes) -> Dict[str, Any]:
|
|
"""Parse ARP header"""
|
|
if len(arp_data) < 28:
|
|
return {'arp_parse_error': 'Insufficient data for ARP header'}
|
|
|
|
hw_type, proto_type, hw_len, proto_len, opcode = struct.unpack('>HHBBH', arp_data[0:8])
|
|
sender_hw = arp_data[8:14]
|
|
sender_proto = struct.unpack('>I', arp_data[14:18])[0]
|
|
target_hw = arp_data[18:24]
|
|
target_proto = struct.unpack('>I', arp_data[24:28])[0]
|
|
|
|
return {
|
|
'arp_hw_type': hw_type,
|
|
'arp_proto_type': f'0x{proto_type:04x}',
|
|
'arp_opcode': opcode,
|
|
'arp_opcode_description': 'Request' if opcode == 1 else 'Reply' if opcode == 2 else f'Unknown ({opcode})',
|
|
'arp_sender_hw': ':'.join(f'{b:02x}' for b in sender_hw),
|
|
'arp_sender_ip': self._ip_to_string(sender_proto),
|
|
'arp_target_hw': ':'.join(f'{b:02x}' for b in target_hw),
|
|
'arp_target_ip': self._ip_to_string(target_proto)
|
|
}
|
|
|
|
def _analyze_udp_payload(self, payload: bytes) -> Dict[str, Any]:
|
|
"""Analyze UDP payload content"""
|
|
analysis = {}
|
|
|
|
if len(payload) == 0:
|
|
return {'payload_analysis': 'Empty payload'}
|
|
|
|
# Check for common protocols
|
|
if len(payload) >= 4:
|
|
# Check for DNS (port 53 patterns)
|
|
if payload[0:2] in [b'\x00\x01', b'\x81\x80', b'\x01\x00']:
|
|
analysis['possible_protocol'] = 'DNS'
|
|
# Check for DHCP magic cookie
|
|
elif payload[:4] == b'\x63\x82\x53\x63':
|
|
analysis['possible_protocol'] = 'DHCP'
|
|
# Check for RTP (version 2)
|
|
elif (payload[0] & 0xC0) == 0x80:
|
|
analysis['possible_protocol'] = 'RTP'
|
|
else:
|
|
analysis['possible_protocol'] = 'Unknown'
|
|
|
|
# Basic statistics
|
|
analysis['payload_entropy'] = self._calculate_entropy(payload[:256]) # First 256 bytes
|
|
analysis['null_bytes'] = payload.count(0)
|
|
analysis['printable_chars'] = sum(1 for b in payload[:256] if 32 <= b <= 126)
|
|
|
|
return analysis
|
|
|
|
def _calculate_entropy(self, data: bytes) -> float:
|
|
"""Calculate Shannon entropy of data"""
|
|
if not data:
|
|
return 0.0
|
|
|
|
counts = [0] * 256
|
|
for byte in data:
|
|
counts[byte] += 1
|
|
|
|
entropy = 0.0
|
|
length = len(data)
|
|
for count in counts:
|
|
if count > 0:
|
|
p = count / length
|
|
entropy -= p * (p.bit_length() - 1) # log2(p)
|
|
|
|
return entropy
|
|
|
|
def _ip_to_string(self, ip_int: int) -> str:
|
|
"""Convert 32-bit integer to IP address string"""
|
|
return f"{(ip_int >> 24) & 0xFF}.{(ip_int >> 16) & 0xFF}.{(ip_int >> 8) & 0xFF}.{ip_int & 0xFF}"
|
|
|
|
def _decode_ethertype(self, ethertype: int) -> str:
|
|
"""Decode EtherType field"""
|
|
types = {
|
|
0x0800: "IPv4",
|
|
0x0806: "ARP",
|
|
0x86DD: "IPv6",
|
|
0x8100: "VLAN",
|
|
0x88F7: "PTP"
|
|
}
|
|
return types.get(ethertype, f"Unknown (0x{ethertype:04x})")
|
|
|
|
def _decode_ip_protocol(self, protocol: int) -> str:
|
|
"""Decode IP protocol field"""
|
|
protocols = {
|
|
1: "ICMP",
|
|
6: "TCP",
|
|
17: "UDP",
|
|
89: "OSPF",
|
|
132: "SCTP"
|
|
}
|
|
return protocols.get(protocol, f"Unknown ({protocol})") |