Deploy presence daemon to OPi Zero 3 test implant
sensor.service running at 10.0.0.0. ARP + DHCP sensors active. Probe sniffer degraded (no wlan1). BLE degraded (BlueZ too old for or_patterns). Untracked audit files committed for reference.
This commit is contained in:
@@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test suite for presence_daemon parsers — netlink and DHCP bounds checking.
|
||||||
|
Tests minimal reproductions of bounds checking issues.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# We'll test by importing just the functions directly
|
||||||
|
# by reading the source and executing key portions
|
||||||
|
|
||||||
|
|
||||||
|
def parse_netlink_current(data: bytes) -> None:
|
||||||
|
"""Current implementation from presence_daemon.py."""
|
||||||
|
try:
|
||||||
|
offset = 0
|
||||||
|
while offset < len(data):
|
||||||
|
if offset + 16 > len(data):
|
||||||
|
break
|
||||||
|
|
||||||
|
nlmsg_len, nlmsg_type, _, _, _ = struct.unpack_from('=IHHII', data, offset)
|
||||||
|
if nlmsg_len < 16:
|
||||||
|
break
|
||||||
|
|
||||||
|
payload = data[offset + 16:offset + nlmsg_len]
|
||||||
|
# This is the issue: if nlmsg_len > len(data), we silently get a short payload
|
||||||
|
# No guard that offset + nlmsg_len <= len(data)
|
||||||
|
|
||||||
|
offset += (nlmsg_len + 3) & ~3
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ndmsg_current(data: bytes, msg_type: int) -> None:
|
||||||
|
"""Current implementation from presence_daemon.py."""
|
||||||
|
try:
|
||||||
|
if len(data) < 12:
|
||||||
|
return
|
||||||
|
|
||||||
|
state = struct.unpack_from('=H', data, 8)[0]
|
||||||
|
|
||||||
|
mac = None
|
||||||
|
ip = None
|
||||||
|
offset = 12
|
||||||
|
|
||||||
|
while offset + 4 <= len(data):
|
||||||
|
rta_len, rta_type = struct.unpack_from('=HH', data, offset)
|
||||||
|
if rta_len < 4:
|
||||||
|
break
|
||||||
|
|
||||||
|
val = data[offset + 4:offset + rta_len]
|
||||||
|
|
||||||
|
if rta_type == 1 and len(val) == 6: # NDA_LLADDR
|
||||||
|
mac = ':'.join(f'{b:02x}' for b in val)
|
||||||
|
elif rta_type == 2: # NDA_DST
|
||||||
|
if len(val) == 4:
|
||||||
|
pass # would parse IP
|
||||||
|
|
||||||
|
offset += (rta_len + 3) & ~3
|
||||||
|
|
||||||
|
# If we had extracted a MAC, we'd proceed
|
||||||
|
# The issue: rta_len might claim more bytes than available
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dhcp_current(data: bytes) -> None:
|
||||||
|
"""Current implementation from presence_daemon.py."""
|
||||||
|
try:
|
||||||
|
if len(data) < 14:
|
||||||
|
return
|
||||||
|
|
||||||
|
eth_type = struct.unpack('!H', data[12:14])[0]
|
||||||
|
if eth_type != 0x0800:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(data) < 23:
|
||||||
|
return
|
||||||
|
proto = data[23]
|
||||||
|
if proto != 17:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(data) < 38:
|
||||||
|
return
|
||||||
|
src_port = struct.unpack('!H', data[34:36])[0]
|
||||||
|
dst_port = struct.unpack('!H', data[36:38])[0]
|
||||||
|
if not (src_port in (67, 68) and dst_port in (67, 68)):
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(data) < 42:
|
||||||
|
return
|
||||||
|
dhcp = data[42:]
|
||||||
|
if len(dhcp) < 236:
|
||||||
|
return
|
||||||
|
|
||||||
|
# MAC extraction — offset 28-34
|
||||||
|
mac_bytes = dhcp[28:34]
|
||||||
|
mac = ':'.join(f'{b:02x}' for b in mac_bytes)
|
||||||
|
if mac in ('00:00:00:00:00:00', 'ff:ff:ff:ff:ff:ff'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(dhcp) < 240:
|
||||||
|
return
|
||||||
|
|
||||||
|
msg_type = None
|
||||||
|
i = 240
|
||||||
|
while i < len(dhcp):
|
||||||
|
opt = dhcp[i]
|
||||||
|
if opt == 255:
|
||||||
|
break
|
||||||
|
if opt == 0:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
if i + 1 >= len(dhcp):
|
||||||
|
break
|
||||||
|
|
||||||
|
length = dhcp[i + 1]
|
||||||
|
if i + 2 + length > len(dhcp):
|
||||||
|
break
|
||||||
|
|
||||||
|
val = dhcp[i + 2:i + 2 + length]
|
||||||
|
|
||||||
|
if opt == 53 and length == 1:
|
||||||
|
msg_type = val[0]
|
||||||
|
|
||||||
|
i += 2 + length
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Test cases
|
||||||
|
|
||||||
|
def test_parse_netlink_truncated_header():
|
||||||
|
"""Netlink parser should handle buffer shorter than header gracefully."""
|
||||||
|
truncated = b'\x01\x02\x03'
|
||||||
|
# Should not raise
|
||||||
|
parse_netlink_current(truncated)
|
||||||
|
print("✓ test_parse_netlink_truncated_header passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_netlink_header_claims_more_than_available():
|
||||||
|
"""Netlink parser should handle nlmsg_len > buffer length."""
|
||||||
|
# Build a netlink header that claims 100 bytes but only provide 16
|
||||||
|
nlmsg_len = 100
|
||||||
|
nlmsg_type = 20
|
||||||
|
flags = 0
|
||||||
|
seq = 0
|
||||||
|
pid = 0
|
||||||
|
data = struct.pack('=IHHII', nlmsg_len, nlmsg_type, flags, seq, pid)
|
||||||
|
assert len(data) == 16
|
||||||
|
# Should not raise
|
||||||
|
parse_netlink_current(data)
|
||||||
|
print("✓ test_parse_netlink_header_claims_more_than_available passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_netlink_offset_overflow():
|
||||||
|
"""Netlink parser should not overflow offset calculation."""
|
||||||
|
# Two messages: first claims huge length, second is valid
|
||||||
|
msg1 = struct.pack('=IHHII', 0xffffffff, 20, 0, 0, 0) # Claims ~4GB
|
||||||
|
msg2 = struct.pack('=IHHII', 16, 20, 0, 1, 0) # Valid header
|
||||||
|
data = msg1 + msg2
|
||||||
|
# Should not raise (might infinite loop with current code)
|
||||||
|
parse_netlink_current(data)
|
||||||
|
print("✓ test_parse_netlink_offset_overflow passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_ndmsg_rta_claims_more_data():
|
||||||
|
"""ndmsg parser should handle RTA that claims more bytes than available."""
|
||||||
|
# ndmsg header: family, pad, type, flags, state, index (6 bytes with BBBBHH)
|
||||||
|
ndmsg_header = struct.pack('=BBBBHH', 0, 0, 0, 0, 64, 0)
|
||||||
|
|
||||||
|
# RTA header claiming 20 bytes total length
|
||||||
|
rta_len = 20
|
||||||
|
rta_type = 1 # NDA_LLADDR
|
||||||
|
rta_header = struct.pack('=HH', rta_len, rta_type)
|
||||||
|
|
||||||
|
# But only provide 2 bytes of actual RTA data (not 16 bytes as claimed)
|
||||||
|
data = ndmsg_header + rta_header + b'\x01\x02'
|
||||||
|
|
||||||
|
# Should not raise
|
||||||
|
parse_ndmsg_current(data, 20) # RTM_NEWNEIGH
|
||||||
|
print("✓ test_parse_ndmsg_rta_claims_more_data passed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_dhcp_minimal_valid():
|
||||||
|
"""DHCP parser should accept valid minimal DHCP frame."""
|
||||||
|
# Build minimal valid DHCP frame (Eth + IP + UDP + DHCP)
|
||||||
|
eth_dst = b'\xff\xff\xff\xff\xff\xff'
|
||||||
|
eth_src = b'\x00\x11\x22\x33\x44\x55'
|
||||||
|
eth_type = struct.pack('!H', 0x0800)
|
||||||
|
|
||||||
|
# IP header (20 bytes)
|
||||||
|
ip_version_ihl = 0x45
|
||||||
|
ip_dscp_ecn = 0
|
||||||
|
ip_length = struct.pack('!H', 20 + 8 + 260)
|
||||||
|
ip_id = struct.pack('!H', 0)
|
||||||
|
ip_flags_frag = struct.pack('!H', 0x4000)
|
||||||
|
ip_ttl = 64
|
||||||
|
ip_proto = 17 # UDP
|
||||||
|
ip_checksum = struct.pack('!H', 0)
|
||||||
|
ip_src = b'\x00\x00\x00\x00'
|
||||||
|
ip_dst = b'\xff\xff\xff\xff'
|
||||||
|
|
||||||
|
ip_header = (
|
||||||
|
bytes([ip_version_ihl, ip_dscp_ecn]) +
|
||||||
|
ip_length + ip_id + ip_flags_frag +
|
||||||
|
bytes([ip_ttl, ip_proto]) + ip_checksum +
|
||||||
|
ip_src + ip_dst
|
||||||
|
)
|
||||||
|
|
||||||
|
# UDP header (8 bytes)
|
||||||
|
udp_src = struct.pack('!H', 68)
|
||||||
|
udp_dst = struct.pack('!H', 67)
|
||||||
|
udp_length = struct.pack('!H', 8 + 260)
|
||||||
|
udp_checksum = struct.pack('!H', 0)
|
||||||
|
|
||||||
|
udp_header = udp_src + udp_dst + udp_length + udp_checksum
|
||||||
|
|
||||||
|
# DHCP payload (260 bytes)
|
||||||
|
dhcp_body = bytearray(260)
|
||||||
|
dhcp_body[0] = 1 # BOOTREQUEST
|
||||||
|
dhcp_body[4:10] = b'\x12\x34\x56\x78\x9a\xbc'
|
||||||
|
dhcp_body[28:34] = b'\xaa\xbb\xcc\xdd\xee\xff' # chaddr
|
||||||
|
|
||||||
|
# DHCP magic + option 53
|
||||||
|
dhcp_body[240:244] = b'\x63\x82\x53\x63'
|
||||||
|
dhcp_body[244:247] = b'\x35\x01\x01' # option 53, length 1, value 1 (DISCOVER)
|
||||||
|
dhcp_body[247] = 255 # End
|
||||||
|
|
||||||
|
frame = eth_dst + eth_src + eth_type + ip_header + udp_header + bytes(dhcp_body)
|
||||||
|
parse_dhcp_current(frame)
|
||||||
|
print("✓ test_parse_dhcp_minimal_valid passed")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_parse_netlink_truncated_header()
|
||||||
|
test_parse_netlink_header_claims_more_than_available()
|
||||||
|
test_parse_netlink_offset_overflow()
|
||||||
|
test_parse_ndmsg_rta_claims_more_data()
|
||||||
|
test_parse_dhcp_minimal_valid()
|
||||||
|
print("\nAll tests passed!")
|
||||||
Reference in New Issue
Block a user