How to Programmatically Discover MAC Addresses of Devices in a Network (Including BIOS-Only Machines)


2 views

When dealing with network devices that only have BIOS (no OS), traditional discovery methods like ARP or ping won't work. We need low-level network scanning techniques that can detect devices at the hardware level.

Here are several reliable approaches to discover MAC addresses in various network scenarios:

1. Using ARP Scanning (For OS-Enabled Devices)

For machines with an operating system, ARP scanning is the most straightforward method:


import scapy.all as scapy

def arp_scan(ip_range):
    answered = scapy.arping(ip_range, verbose=False)[0]
    devices = []
    for sent, received in answered:
        devices.append({'ip': received.psrc, 'mac': received.hwsrc})
    return devices

print(arp_scan("192.168.1.1/24"))

2. DHCP Server Logs Inspection

Checking DHCP server logs can reveal MAC addresses of all devices that requested IP addresses:


# For Linux DHCP servers:
cat /var/log/dhcpd.log | grep "DHCPDISCOVER"

3. Wake-on-LAN Packet Detection

For BIOS-only machines, sending WoL packets can help identify devices:


from wakeonlan import send_magic_packet

def discover_wol_devices(mac_prefix, ip_range):
    # Implement your discovery logic here
    pass

For devices without an OS, we need to use more sophisticated methods:

1. Network Switch MAC Address Tables

Most managed switches maintain MAC address tables:


# Cisco switches:
show mac address-table

2. Using nmap for Raw Packet Scanning

Nmap can perform various low-level scans:


nmap -sn -PR 192.168.1.0/24

3. Custom UDP Broadcast Probes

Creating custom broadcast probes can elicit responses from BIOS devices:


import socket

def send_bios_probe():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    s.sendto(b"BIOS_DISCOVERY_PROBE", ('255.255.255.255', 9))
    # Implement response handling

When performing network scans:

  • Always get proper authorization
  • Limit scan frequency to avoid network congestion
  • Be aware of legal implications in your jurisdiction

For BIOS machines intended for network booting, PXE servers maintain MAC records:


# Typical PXE config file location:
/etc/dhcp/dhcpd.conf

When dealing with machines in PXE boot state (only BIOS active), traditional network scanning methods often fail. These machines typically:

  • Don't respond to ICMP ping requests
  • Lack ARP cache entries until network boot initiates
  • Have no listening TCP/UDP services

Here are three reliable approaches to discover MAC addresses in such scenarios:

1. DHCP Server Log Inspection

Most enterprise DHCP servers log MAC addresses of PXE clients. For ISC DHCP servers:


# Sample ISC DHCP log entry
tail -f /var/log/dhcpd.log | grep "DHCPDISCOVER"
# Output format: DHCPDISCOVER from aa:bb:cc:dd:ee:ff via eth0

2. ARP Cache Monitoring During PXE Boot

Trigger ARP discovery during the brief window when PXE clients communicate:


# Linux script to capture ARP entries
#!/bin/bash
while true; do
  ip -s -s neigh flush all
  sleep 2
  arp -an | grep -v incomplete
done

3. Packet Sniffing with tcpdump

Capture DHCP/BOOTP packets which always contain MAC addresses:


# Capture PXE-related traffic
tcpdump -i eth0 -nn -v port bootpc or port bootps
# Filter for MAC addresses
tcpdump -i eth0 -nn -e -q | grep -E 'BOOTP|DHCP' | awk '{print $2,$8}'

Here's a complete Python solution using scapy:


from scapy.all import sniff, DHCP, BOOTP
import sys

def handle_dhcp(pkt):
    if pkt.haslayer(DHCP):
        mac = pkt[BOOTP].chaddr[:6].hex(':')
        print(f"Discovered MAC: {mac} via DHCP Option {pkt[DHCP].options[0][1]}")

print("Starting DHCP monitor...")
sniff(filter="udp and (port 67 or 68)", prn=handle_dhcp, store=0)

For large deployments, consider:

  • Wireshark's TShark for continuous monitoring
  • Custom ELK stack integration with DHCP logs
  • SNMP polling of network switches' bridge tables

Always cross-verify discovered MACs with:


# Compare with switch MAC tables
show mac address-table dynamic | include [VLAN_ID]