OSX and ARP spoofing

Here's a simple program that spoofs an ARP reply for a given IP address. If I spin up two terminal sessions on the same machine.

Run this code in one window

% ./spoof en0 192.168.1.7 Listening on en0 for ARP requests to 192.168.1.7 Spoofing MAC: 00:0c:87:47:50:27

And in the second window cause the OS to issue an ARP_REQ % ping 192.168.1.7

You will see the program respond to the ARP request. (Wireshark will see the ARP_REQ and ARP_REPLY packets) however my arp table isn't updated with the MAC for the IP address. There is no firewall active.

% arp -a|grep 192.168.1.7 (192.168.1.7) at (incomplete) on en0 ifscope [ethernet]

This is running on a MacBook pro M3 (OSX 15.4).

HOWEVER, on a MacBook pro M4 (OSX 15.2) is does Work !!!!!

Can anyone explain why its not working?

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include   // ADDED: Needed for ARP constants like ARPOP_REQUEST, ARPOP_REPLY, ARPHRD_ETHER

// Minimum Ethernet frame size (60 bytes)
#define ETH_MIN_LEN 60

// Ethernet header structure
struct eth_hdr {
    u_char dest[6];
    u_char src[6];
    u_short ethertype;
} __attribute__((packed));

// ARP header structure
struct arp_hdr {
    u_short htype;
    u_short ptype;
    u_char hlen;
    u_char plen;
    u_short opcode;
    u_char sender_mac[6];
    u_int sender_ip;
    u_char target_mac[6];
    u_int target_ip;
} __attribute__((packed));

// Combined ARP packet (Ethernet + ARP)
struct arp_packet {
    struct eth_hdr eth;
    struct arp_hdr arp;
} __attribute__((packed));

// Global variables (customize as needed)
pcap_t *handle;
u_int spoof_ip;               // The IP address for which we want to answer ARP (in network byte order)
u_char spoof_mac[6] = {0x00, 0x0c, 0x87, 0x47, 0x50, 0x27}; // Hard-coded MAC for spoofing

// Packet handler function
void packet_handler(u_char *args, const struct pcap_pkthdr *header, const u_char *packet) {
    (void)args; // unused parameter

    // Check that this is an ARP packet
    struct eth_hdr *eth = (struct eth_hdr *)packet;
    if (ntohs(eth->ethertype) != ETHERTYPE_ARP)
        return;

    // Get the ARP header
    struct arp_hdr *arp = (struct arp_hdr *)(packet + sizeof(struct eth_hdr));

    // Only process ARP requests for our spoof_ip
    if (ntohs(arp->opcode) != ARPOP_REQUEST)
        return;
    if (arp->target_ip != spoof_ip)
        return;

    // Build the ARP reply
    struct arp_packet reply;
    // Ethernet header: reply's destination is the original sender, source is our spoof_mac.
    memcpy(reply.eth.dest, eth->src, 6);
    memcpy(reply.eth.src, spoof_mac, 6);
    reply.eth.ethertype = htons(ETHERTYPE_ARP);

    // ARP header: reply opcode, hardware and protocol types/lengths.
    reply.arp.htype = htons(ARPHRD_ETHER);
    reply.arp.ptype = htons(ETHERTYPE_IP);
    reply.arp.hlen = 6;
    reply.arp.plen = 4;
    reply.arp.opcode = htons(ARPOP_REPLY);

    // In the reply, our spoof MAC and IP become the sender info.
    memcpy(reply.arp.sender_mac, spoof_mac, 6);
    reply.arp.sender_ip = spoof_ip;

    // The target is the original sender.
    memcpy(reply.arp.target_mac, arp->sender_mac, 6);
    reply.arp.target_ip = arp->sender_ip;

    // Determine reply length and pad if necessary.
    int reply_len = sizeof(reply);
    u_char buffer[ETH_MIN_LEN];
    memset(buffer, 0, ETH_MIN_LEN);
    memcpy(buffer, &reply, reply_len);
    
    if (pcap_sendpacket(handle, buffer, ETH_MIN_LEN) == -1) {
        fprintf(stderr, "Error sending packet: %s\n", pcap_geterr(handle));
    } else {
        struct in_addr addr;
        addr.s_addr = spoof_ip;
        printf("Sent ARP reply for %s\n", inet_ntoa(addr));
    }
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s  \n", argv[0]);
        exit(EXIT_FAILURE);
    }
    char *dev = argv[1];
    char errbuf[PCAP_ERRBUF_SIZE];
    spoof_ip = inet_addr(argv[2]);

    // Open the device for capturing (promiscuous mode enabled)
    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) {
        fprintf(stderr, "Could not open device %s: %s\n", dev, errbuf);
        exit(EXIT_FAILURE);
    }

    printf("Listening on %s for ARP requests to %s\n", dev, argv[2]);
    printf("Spoofing MAC: %02x:%02x:%02x:%02x:%02x:%02x\n",
           spoof_mac[0], spoof_mac[1], spoof_mac[2],
           spoof_mac[3], spoof_mac[4], spoof_mac[5]);

    // Start capture loop
    pcap_loop(handle, -1, packet_handler, NULL);
    pcap_close(handle);
    return 0;
}
Written by RichColey in 775485021
Can anyone explain why its not working?

I don’t have a definitive answer for you here, but there are two common causes of problems like this:

  • There’s ongoing tension as to whether Ethernet-like drivers should allow you to change the local MAC address.

  • Wi-Fi STAs [1] typically supports a single MAC address, meaning that higher-level tools, like the Virtualization framework, have to implement the ‘fun’ that is MAC-NAT [2].

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Using the terms from Wi-Fi Fundamentals.

[2] Which is not well covered by our documentation (r. 127734903).

Hi,

Thanks for the response.

The code isn't trying to actually change any MAC address, its issuing a valid ARP response..

It doesn't really explain why it worked on the M4 either of course. Undeterministic behaviour feels like a bug to me...

When you talk to virtualisation frameworks are you eluding to tools like Fusion or VirtualBox that essentially provide network bridging? I'll go read the referenced 127734903.

OSX and ARP spoofing
 
 
Q