escalatedquickly

Hacker || Malware Enthusiast || Nerd

Socket Programming - IPv6 Extension Headers

2018-01-11
2447 words

Recently in my research I had to deal with the IPv6 extension headers. In particular I had to deal with the Hop-By-Hop header. Unfortunately, the process of actually setting the option and getting it on the wire is not trivial, and the documentation is so-so. Because of this I thought I’d give explaining the process a go. But firstly, let’s discuss the header itself.

IPv6 Extension Headers

The two extensions headers that are covered by this method are the Hob-by-Hop options header and the destination options header. These headers look like the diagram below[1]:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Next Header  |  Hdr Ext Len  |                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               +
|                                                               |
.                                                               .
.                            Options                            .
.                                                               .
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

The first field is a 8-bit field that tells us what the next header after the current one is. Following that is another 8-bit field which tells us how long the current field is in units of 8 bytes. It should be noted, however, that this length does not include the first 8 bytes. In other words, if the header is 8 bytes long the length field will hold the value 0, while if the header is 16 bytes long it will hold the value 1.

After these two fields comes the actual options. They follow the below format, and must be a multiple of 8 bytes long. If they’re too short they’re padded to an acceptable length.

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - -
|  Option Type  |  Opt Data Len |  Option Data
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- - - - - - - - -

The first field tells us the type of the option. The most significant bits of this value determines how the packet is treated, according to the following table:

MSB Description
00 Skip this option and keep processing the header
01 Discard the packet
10 Drop the packet and return an IMCP parameter probelm packet, regardless of whether or not the destination address is multicast
11 Drop the packet and return an ICMP parameter problem packet, but only if the destination address is not multicast

The third most significant bit tells us if the option can change in en-route (1) or not (0). The option length field says how many bytes long the option is.

For the purposes of this guide, that is all you need to know. As you can see from the MSB meaning table, these options aren’t extremely useful if you follow the RFC, as they all either tells the receiver to ignore the option or drop the packet, but they might become useful in the future. Who knows.

Adding the extension headers to packets

And now we’re reached the reason you’re here; how do we actually make our packets that we put on the wire have these options? Unless you want to manually build the whole packet yourself, by hand (which is of course possible), the way to do it is to use struct cmsghdr. These structs, which hold message control data, are used by struct msghdr when using sendmsg(...)[2] to send packets. Since building packets by hand is a bit awkward and messy, this approach is what we’ll do. So let’s get to it!

The very first step is to create a socket, regardless of method, so let’s start with that. I am going to create a raw socket that can then be used to issue ICMP messages. To create this socket we do the following:

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

void fatal(char *msg)
{
   fprintf(stderr, "%s\n", msg);
   exit(EXIT_FAILURE);
}
...

int sockfd;
if((sockfd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) == -1)
   fatal(strerror(errno));

This creates a raw IPv6 socket that is meant to send ICMPv6 messages. If something goes wrong, and error message is printed to stderr and the program is shut down.

Now that we have our socket we’re ready to start looking at how to send this. As mentioned we’re going to use sendmsg(...) for this, so let’s start by examining the function prototype for this:

size_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

The function first takes a socket, of course, followed by a struct msghdr * and some flags. The socket is already created, and we’re not going to bother with flags right now, so the value of that is 0. The thing that’s interesting here is the struct msghdr, as this structure will hold all the information about the packet that the kernel needs to know, like destination. This is also how we’re going to pass the hop-by-hop header! It looks like this[3]:

struct msghdr
  {
    void *msg_name;
    socklen_t msg_namelen;

    struct iovec *msg_iov;
    size_t msg_iovlen;

    void *msg_control;
    size_t msg_controllen;
    int msg_flags;
  };

The first fields, msg_name and msg_namelen, are meant for the destination address and the length of it. The next two are the I/O-vectors, or buffers containing input. The msg_control and msg_controllen fields are the truly interesting here, since these are where we’ll put our struct cmsghdr and its length once we’ve built that. But let’s build what we can, while we’re already looking at this. Firstly, let’s set the I/O buffers and flags, since those are trivial.

#define BUFLEN 0xff

....

char sendbuf[BUFLEN];

struct iovec iov[2];
iov[0].iov_base = sendbuf;
iov[0].iov_len = BUFLEN;

struct msghdr msg = {0};
msg.msg_iov = iov;
msg.msg_iovlen = 2;
msg.msg_flags = 0;

This piece of code creates a 256 byte input buffer, stores it in an struct iov and stores that in the struct msghdr. The reason the variable iov is declared as an array with two elements is that we’re gonna need that for the options later. The next step is to set the msg_name and msg_namelen fields. To do this we create an struct sockaddr_in6 object and set the value of the IP address using the inet_pton(...) function. We then set the struct msghdr fields to point to this variable:

...

struct sockaddr_in6 *dst = malloc(sizeof(struct sockaddr_in6));
inet_pton(AF_INET6, <address>, (char *)&dst->sin6_addr);

...

msg.msg_name = dst;
msg.msg_namelen = sizeof(struct sockaddr_in6);

Exchange the <address> above with a string containing the destination address of your packets, and you’re good to go. Up to this point we’ve covered most of the preparations that are necessary to make this work. The final step, before actually sending the packet, is to set the extension headers. The process is a bit messy, to say the least, but I think that we’ll be able to pull through.

Setting the extension header uses struct cmsghdr. The struct is declared as follows[3]:

struct cmsghdr
{
  size_t cmsg_len;
  int cmsg_level;
  int cmsg_type;
}

The cmsg_len field contains the length of the data plus the length of the header. cmsg_level holds the originating protocol, e.g. IPPROTO_IPV6 in our case. cmsg_type holds the message type. Since we’re adding a hop-by-hop header the value here will be IPV6_HOPOPTS. Now, it may seem like setting these values is all we need to do to get this up and running, but it’s quite a bit trickier than that. This is because the struct stores the data after the struct itself, so we need to initialize it carefully. For what we want to do we need four functions and three macro functions, all defined in RFC3542[4]. Let’s start with the macros:

The first macro, CMSG_LEN(len) returns the length of the header and data, with any necessary alignment taken into account. This is the value of the cmsg_len field. The second macro is CMSG_DATA(cmsg), which returns a pointer to the actual ancillary data. Finally, the CMSG_SPACE(len) macro simply returns the size in bytes of the ancillary data and a payload of the specified length. We will use this to allocate space for the header.

The four functions needed are listed below, and since they’re a bit complicated, we’ll go through them one by one:

inet6_opt_init(void *extbuf, socklen_t extlen);
inet6_opt_append(void *extbuf, socklen_t extlen, int offset,
                 uint8_t type, socklen_t len, uint8_t align, void **databufp);
inet6_opt_set_val(void *databuf, int offset, void *val, socklen_t vallen);
inet6_opt_finish(void *extbuf, socklen_t extlen, offset);

inet6_opt_init(...) is quite simple. It returns the size of an empty header. If extbuf points to valid memory it also initializes the header’s length field. This function has to be called before any of the other functions can be called.

inet6_opt_append(...) has two jobs. If extbuf is NULL the function returns how long the header would’ve been if an option was actually appended. If extbuf is not NULL, on the other hand, it will append the option, return the length as previously described and return a pointer to the data through the databufp argument. len and align describe the length of the option, as well as the required alignment. len is specified as the number of octets of data. align cannot be exceed len and must have the value 1, 2, 4 or 8, which represent 0, 16, 32 or 64 bits of alignment respectively. offset should be the returned value of either inet6_opt_init or a previous call to inet6_opt_append.

inet6_opt_set_val(...) takes a pointer to the intended data, i.e. databuf from the last function, and simply writes whatever is stored in val to that position. As with the last function, offset should be the returned value of the last inet6_opt_append call.

inet6_opt_finish(...) calculates any final padding needed to make the header a multiple of eight octets long. offset is, as usual, the result of running inet6_opt_init or inet6_opt_append, whichever comes last. When extbuf is not NULL the padding isn’t just calculated, but also added to the header.

So now that we know what all these things do, it’s just a matter of applying this. The inet6_opt_... functions are all specific to GNU, so to ensure that they’re actually available we need to add define _GNU_SOURCE before including any libraries.

What should also be noted, is that in order for us to know how large our struct cmsghdr has to be to accommodate the header we must first do a “dryrun” of the setting up. In other words, run all the functions above once with extbuf set to NULL, then allocate memory for the struct and then run all functions again with real values. In the end this is what it turns into:

#define _GNU_SOURCE

...

struct cmsghdr *cmsg;
void *hopbuf;
socklen_t hoplen;
void *databuf;
int curlen;
uint8_t val = 0xCC;

// Dryrun to find out size of headers
if((curlen = inet6_opt_init(NULL, 0)) == -1)
   fatal(strerror(errno));
if((curlen = inet6_opt_append(NULL, 0, curlen, 0x80, 1, 1, NULL)) == -1)
   fatal(strerror(errno));
if((curlen = inet6_opt_finish(NULL, 0, curlen)) == -1)
   fatal(strerror(errno));
hoplen = curlen;

// Allocate memory and set known options
cmsg = (struct cmsghdr*)malloc(CMSG_SPACE(hoplen));
cmsg->cmsg_len = CMSG_LEN(hoplen);
cmsg->cmsg_level = IPPROTO_IPV6;
cmsg->cmsg_type = IPV6_HOPOPTS;
hopbuf = CMSG_DATA(cmsg);

// Build actual header
if((curlen = inet6_opt_init(hopbuf, hoplen)) == -1)
   fatal(strerror(errno));
if((curlen = inet6_opt_append(hopbuf, hoplen, curlen, 0x80, 1, 1, &databuf)) == -1)
   fatal(strerror(errno));
if(inet6_opt_set_val(databuf, 0, &val, sizeof(val)) == -1)
   fatal(strerror(errno));
if((curlen = inet6_opt_finish(hopbuf, hoplen, curlen)) == -1)
   fatal(strerror(errno));

Note the value of the type field in the call to inet6_opt_append(...) above: 0x80. This value sets the type field of the options header, and since 0x80 is 1000 0000 in binary, this tells the IPv6 stack to return an ICMP Parameter Problem message. For the purpose of this post, there isn’t really a specific reason for choosing this - any value with at least one bit set in the upper nibble works.

The final step before we’re ready to send the message is to add the option definitions we just set up to the message itself.

iov[1].iov_base = hopbuf;
iov[1].iov_len = sizeof(hopbuf);
msg.msg_control = cmsg;
msg.msg_controllen = CMSG_SPACE(hoplen);

And that’s it, we can now send the message using sendmsg(...). Putting it all together, we get the following:

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define BUFLEN 0xff

void fatal(char *msg)
{
   fprintf(stderr, "%s\n", msg);
   exit(EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
    // Initialize the socket
    int sockfd;
    if((sockfd = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) == -1)
       fatal(strerror(errno));

    // Get destination address
    struct sockaddr_in6 *dst = malloc(sizeof(struct sockaddr_in6));
    inet_pton(AF_INET6, <address>, (char *)&dst->sin6_addr);

    char sendbuf[BUFLEN];

    struct iovec iov[2];
    iov[0].iov_base = sendbuf;
    iov[0].iov_len = BUFLEN;

    struct msghdr msg = {0};
    msg.msg_iov = iov;
    msg.msg_iovlen = 2;
    msg.msg_flags = 0;
    msg.msg_name = dst;
    msg.msg_namelen = sizeof(struct sockaddr_in6);

    struct cmsghdr *cmsg;
    void *hopbuf;
    socklen_t hoplen;
    void *databuf;
    int curlen;
    uint8_t val = 0xCC;

    // Dryrun to find out size of headers
    if((curlen = inet6_opt_init(NULL, 0)) == -1)
       fatal(strerror(errno));
    if((curlen = inet6_opt_append(NULL, 0, curlen, 0x80, 1, 1, NULL)) == -1)
       fatal(strerror(errno));
    if((curlen = inet6_opt_finish(NULL, 0, curlen)) == -1)
       fatal(strerror(errno));
    hoplen = curlen;

    // Allocate memory and set known options
    cmsg = (struct cmsghdr*)malloc(CMSG_SPACE(hoplen));
    cmsg->cmsg_len = CMSG_LEN(hoplen);
    cmsg->cmsg_level = IPPROTO_IPV6;
    cmsg->cmsg_type = IPV6_HOPOPTS;
    hopbuf = CMSG_DATA(cmsg);

    // Build actual header
    if((curlen = inet6_opt_init(hopbuf, hoplen)) == -1)
       fatal(strerror(errno));
    if((curlen = inet6_opt_append(hopbuf, hoplen, curlen, 0x80, 1, 1, &databuf)) == -1)
       fatal(strerror(errno));
    if(inet6_opt_set_val(databuf, 0, &val, sizeof(val)) == -1)
       fatal(strerror(errno));
    if((curlen = inet6_opt_finish(hopbuf, hoplen, curlen)) == -1)
       fatal(strerror(errno));


    iov[1].iov_base = hopbuf;
    iov[1].iov_len = sizeof(hopbuf);
    msg.msg_control = cmsg;
    msg.msg_controllen = CMSG_SPACE(hoplen);

    if(sendmsg(sockfd, &msg, 0) == -1)
             fatal(strerror(errno));

    return 0;
}

When compiled and executed, this will send a single ICMPv6 message with the Hop-by-Hop option header set. The figure below shows a screenshot of Wireshark examining such a message, headed for localhost (::1).

ICMPv6 message with Hop-by-Hop header

Conclusion

So, there we go, that’s all there is to it. Setting up IPv6 extension headers can be a bit long-winded, and like I said in the beginning it’s not exactly intuitive. But once you wrap your head around the control structures involved, the “redundant” procedure, and the IPv6 protocol itself, it’s not too bad.

Even though this post has talked exclusively about the Hop-By-Hop header, the code above should be fairly easy to adapt to other extension headers, or modify to be more generic. If I did a lot of IPv6 network programming and I often needed to set various extension headers, I’d put a somewhat modified version of the code above in a local networking programming library that I’d link into my projects. That way, I’d only have to deal with it once.

Anyway, I hope that you found this somewhat useful, and that it saves you some headache trying to figure this out from lacklustre documentation.

Happy hacking!

References

[1] D. S. E. Deering and B. Hinden, “Internet Protocol, Version 6 (IPv6) Specification.” in Request for comments. RFC 8200; RFC Editor, Jul. 2017. doi: 10.17487/RFC8200.

[2] SEND(2) linux programmer’s manual, 5.10 ed. 2020.

[3] W. R. Stevens, UNIX network programming vol. 1 networking apis : Sockets and xti. Upper Saddle River, N.J.: Prentice Hall PTR, 1998.

[4] T. Jinmei, W. R. Stevens, and E. Nordmark, “Advanced Sockets Application Program Interface (API) for IPv6.” in Request for comments. RFC 3542; RFC Editor, Jun. 2003. doi: 10.17487/RFC3542.