1

I was looking for a way to play around with my MAC address and modify it and I bumped on the following snippet of ANSI-C code that works but I am not sure how. Inside the snippet, I have added points for the questions that I would like to ask.

/*
 * Spoof a MAC address with LD_PRELOAD
 *
 * If environment variable $MAC_ADDRESS is set in the form "01:02:03:04:05:06"
 * then use that value, otherwise use the hardcoded 'mac_address' in this file.
 *
 * Bugs: This currently spoofs MAC addresses for *all* interfaces.
 * It would be better to watch calls to socket() for the interfaces
 * you want and then only spoof ioctl calls to that file descriptor.
 */
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>

#define SIOCGIFHWADDR 0x8927

static unsigned char mac_address[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};

int
ioctl(int d, int request, unsigned char *argp)
{
    static void *handle = NULL;
    static int (*orig_ioctl)(int, int, char*);
    int ret;
    char *p;

    // If this var isn't set, use the hardcoded address above
    p=getenv("MAC_ADDRESS");

    if (handle == NULL) {
        char *error;
        handle = dlopen("/lib/libc.so.6", RTLD_LAZY);
        if (!handle) {
            fputs(dlerror(), stderr);
            exit(EXIT_FAILURE);
        }
        orig_ioctl = dlsym(handle, "ioctl");
        if ((error = dlerror()) != NULL) {
            fprintf(stderr, "%s\n", error);
            exit(EXIT_FAILURE);
        }
    }
    
    ret = orig_ioctl(d, request, argp);
    
    if (request == SIOCGIFHWADDR) {
        unsigned char *ptr = argp + 18; // [1] what is the +18 purpose?

        int i;
        for (i=0; i < 6; i++) {
            if (!p) {
                ptr[i] = mac_address[i];
                continue;
            }
            int val = 0;
            if (p[0]>='0' && p[0]<='9') val |= (p[0]-'0')<<4;
            else if (p[0]>='a' && p[0]<='f') val |= (p[0]-'a'+10)<<4;
            else if (p[0]>='A' && p[0]<='F') val |= (p[0]-'A'+10)<<4;
            else break;
            if (p[1]>='0' && p[1]<='9') val |= p[1]-'0';
            else if (p[1]>='a' && p[1]<='f') val |= p[1]-'a'+10;
            else if (p[1]>='A' && p[1]<='F') val |= p[1]-'A'+10;
            else break;
            if (p[2]!=':' && p[2]!='\0') break;
            ptr[i] = val;
            p+=3;
        }
    }
    return ret; 
}

So, my first question [1]: In the statement unsigned char *ptr = argp + 18; what is the purpose of the 'addition'? How can we derive this number? After looking at strace ifconfig for example, i can see that the ioctl is called with the following arguments:

ioctl(5, SIOCGIFHWADDR, {ifr_name="eth0", ifr_hwaddr={sa_family=ARPHRD_ETHER, sa_data=00:15:5d:2f:7f:86}}) = 0

Where 5 is the file descriptor, SIOCGIFHWADDR is the flag that corresponds to get/set HW address and the rest is the argp. But, how exactly can we iterate over this struct of data ?

The functionality of the code can be tested after getting the .so file and doing something like e.g,:

export LD_PRELOAD="./hostid-spoof.so" # the compiled lib
export MAC_ADDRESS="11:22:33:44:55:66"
ifconfig

Then the mac addresses of the net interfaces are indeed changed. But how exactly does this take effect?

ex1led
  • 427
  • 5
  • 21

1 Answers1

2

The MAC address isn't actually changed; rather, this returns a fake address to local processes such as ifconfig that try to retrieve the address. It's not clear why that would be valuable. What goes in or out on the wire won't be affected.

The LD_PRELOAD serves to "hook" the ioctl function from the standard C library. See What is the LD_PRELOAD trick?. The call is first is chained along to the real ioctl function. If the command was SIOCGIFHWADDR, then we mess with the returned data. The argp parameter points to a struct ifreq; see the netdevice(7) man page (for Linux, or the equivalent on your own OS). The author of the code presumably looked at the binary layout of struct ifreq on this OS to determine that the MAC address is located starting at byte 18 of the struct, and then pokes the desired fake address into the 6 bytes starting at that location, converting hex to binary in a somewhat clumsy fashion.


To see where the 18 comes from, here's what I did on my system:

  • As explained by the man page above, struct ifreq is defined in <net/if.h> as (irrelevant members omitted):
struct ifreq {
    char ifr_name[IFNAMSIZ]; /* Interface name */
    union {
        // ...
        struct sockaddr ifr_hwaddr;
        // ...
    };
};

<net/if.h> defines IFNAMSIZ as IF_NAMESIZE which is defined as 16. So there's 16 bytes right there. Next, struct sockaddr is defined in <sys/socket.h>, or rather in <bits/socket.h> which the latter includes, as:

struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
    char sa_data[14];           /* Address data.  */
  };

The __SOCKADDR_COMMON macro is defined in <bits/sockaddr.h> as:

#define __SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

so it expands to sa_family_t sa_family;. And just above in <bits/sockaddr.h> we have

typedef unsigned short int sa_family_t;

Now unsigned short int is a 16-bit type on my system, so that's 2 bytes. The sa_data member will start immediately after that, because a char array doesn't require any padding. Thus 16 + 2 = 18.

Another way would have been to write a simple program that prints out the value of offsetof(struct ifreq, ifr_hwaddr) + offsetof(struct sockaddr, sa_data). That also prints 18 on my system.


If you want to actually change the hardware MAC address of your interface, that's more a question of system administration than programming; ask on http://superuser.com or an appropriate site for your operating system.

Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • It is clear to me that the actual HW address does not change, rather it gets ''spoofed'' this is true. Having said that, I presume that the `ioctl` call then treats the `argp` argument as a reference and modifies it, hence the data. But, I am rather interested at this `+18`. How can for example iterate over the contents of the `argp` after the `ioctl` call and determine what is the exact offseet of the `ifr_hwaddr` for instance? – ex1led Oct 02 '21 at 10:17
  • 1
    @ex1led: See edits. – Nate Eldredge Oct 02 '21 at 15:06
  • Thanks! But, after viewing the struct `ifreq` inside the `` header file I wonder: shouldn't the rest of the fields included in the struct play a role and appear inside `argp` ? Even though they are not initialized, shouldn't they lets say occupy their respective space within the struct ? (and thus, affect the offset)? – ex1led Oct 03 '21 at 01:03
  • 1
    @ex1led: The offset calculation is correct. The `struct ifreq` really only has two members: `ifr_name` at offset 0, and an anonymous union at offset 16. All members of the union are allocated in the same memory, and so they all have the same offset, namely 16. Of course we can only use one of them at a time, and the documentation explains which one is used for each call. (You are familiar with how a union works in general, I assume?) – Nate Eldredge Oct 03 '21 at 01:07
  • 1
    @ex1led: Let me say that this code is unnecessarily complicated in several ways. First, I don't see any reason why they should have to use the offsets by hand. They could have just included `` themselves, declared the `argp` argument as `struct ifreq *argp` and then written to `argp->ifr_hwaddr.sa_data[i]` directly. And the handwritten hex to binary conversion is silly when `strtoul` or `sscanf` would work perfectly well. – Nate Eldredge Oct 03 '21 at 01:17
  • I missed that we are talking about a `union` here. I thought that it is another `struct` member. Sorry about that. I agree about the unnecessary obfuscation of the code. – ex1led Oct 03 '21 at 20:27