Files
bigbrother/tests/test_presence_parsers.py
T
Cobra 06c84e1800 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.
2026-04-10 15:49:03 -04:00

246 lines
7.2 KiB
Python

#!/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!")