xorl %eax, %eax

CVE-2010-3848: Linux kernel econet_sendmsg() Stack Overflow

leave a comment »

A few days ago Nelson Elhage reported a number of Linux kernel vulnerabilities. Among them is the following one.

/*
 *      Send a packet.  We have to work out which device it's going out on
 *      and hence whether to use real Econet or the UDP emulation.
 */

static int econet_sendmsg(struct kiocb *iocb, struct socket *sock,
                          struct msghdr *msg, size_t len)
{
        struct sock *sk = sock->sk;
        struct sockaddr_ec *saddr=(struct sockaddr_ec *)msg->msg_name;
        struct net_device *dev;
        struct ec_addr addr;
        int err;
        unsigned char port, cb;
    ...
#ifdef CONFIG_ECONET_AUNUDP
        struct msghdr udpmsg;
        struct iovec iov[msg->msg_iovlen+1];
        struct aunhdr ah;
        struct sockaddr_in udpdest;
        __kernel_size_t size;
        int i;
        mm_segment_t oldfs;
#endif
    ...
        if (len + 15 > dev->mtu) {
                mutex_unlock(&econet_mutex);
                return -EMSGSIZE;
        }

        if (dev->type == ARPHRD_ECONET) {
    ...

Before moving on. Here you can see that if ‘ECONET_AUNUDP‘ option is enabled, it will declare a number of additional variables, including a variable sized array named ‘iov[]’. Its size depends on the message structure that will be processing. For completeness, here is that structure as defined at include/linux/socket.h:

struct msghdr {
        void    *       msg_name;       /* Socket name                  */
        int             msg_namelen;    /* Length of name               */
        struct iovec *  msg_iov;        /* Data blocks                  */
        __kernel_size_t msg_iovlen;     /* Number of blocks             */
        void    *       msg_control;    /* Per protocol magic (eg BSD file descriptor passing) */
        __kernel_size_t msg_controllen; /* Length of cmsg list */
        unsigned        msg_flags;
};

Next, we find a range check. It checks that the length ‘len’ plus the header size do not exceed the network interface’s MTU limit. Back to econet_sendmsg() we have a more interesting code.

        /* tack our header on the front of the iovec */
        size = sizeof(struct aunhdr);
        /*
         * XXX: that is b0rken.  We can't mix userland and kernel pointers
         * in iovec, since on a lot of platforms copy_from_user() will
         * *not* work with the kernel and userland ones at the same time,
         * regardless of what we do with set_fs().  And we are talking about
         * econet-over-ethernet here, so "it's only ARM anyway" doesn't
         * apply.  Any suggestions on fixing that code?         -- AV
         */
        iov[0].iov_base = (void *)&ah;
        iov[0].iov_len = size;
        for (i = 0; i < msg->msg_iovlen; i++) {
                void __user *base = msg->msg_iov[i].iov_base;
                size_t iov_len = msg->msg_iov[i].iov_len;
                /* Check it now since we switch to KERNEL_DS later. */
                if (!access_ok(VERIFY_READ, base, iov_len)) {
                        mutex_unlock(&econet_mutex);
                        return -EFAULT;
                }
                iov[i+1].iov_base = base;
                iov[i+1].iov_len = iov_len;
                size += iov_len;
         }

The comment informs us that this code is ‘b0rken’ and it’s quite simple to read that the loop will iterate as long as the ‘i’ is less than ‘ msg->msg_iovlen’, copying the data from the user controlled structure to the previously declared kernel one.
Nelson Elhage made the following observation:

CVE-2010-3848 is interesting because it's a bug class I haven't seen before,
although maybe people who have been around longer have. econet_sendmsg() can be
made to allocate > 8192 bytes on the kernel stack, overflowing the two pages
allocated for the stack, and allowing an attacker to clobber the 'struct
thread_info', which provides several easy exploit vectors.

Since the ‘iov[]’ array is created based on the user controlled message’s length, it could have size exceeding the kernel’s stack and thus corrupting kernel heap memory. To fix this, the patch was to create a statically allocated buffer:

 #ifdef CONFIG_ECONET_AUNUDP
        struct msghdr udpmsg;
-       struct iovec iov[msg->msg_iovlen+1];
+       struct iovec iov[2];
        struct aunhdr ah;
        struct sockaddr_in udpdest;
        __kernel_size_t size;
-       int i;
        mm_segment_t oldfs;
+       char *userbuf;
 #endif

Remove the length check and place it in the processing code for hardware ECONET…

        }
 
-       if (len + 15 > dev->mtu) {
-               mutex_unlock(&econet_mutex);
-               return -EMSGSIZE;
-       }
-
        if (dev->type == ARPHRD_ECONET) {
                /* Real hardware Econet.  We're not worthy etc. */
 #ifdef CONFIG_ECONET_NATIVE
                unsigned short proto = 0;
                int res;
 
+               if (len + 15 > dev->mtu) {
+                       mutex_unlock(&econet_mutex);
+                       return -EMSGSIZE;
+               }
+
                dev_hold(dev);

Before reaching the ‘XXX’ marked loop another length check is added

        }
 
+       if (len > 32768) {
+               err = -E2BIG;
+               goto error;
+       }
+
        /* Make up a UDP datagram and hand it off to some higher intellect. */
 
        memset(&udpdest, 0, sizeof(udpdest));

The buggy code is changed as you can see here:

        /* tack our header on the front of the iovec */
        size = sizeof(struct aunhdr);
-       /*
-        * XXX: that is b0rken.  We can't mix userland and kernel pointers
-        * in iovec, since on a lot of platforms copy_from_user() will
-        * *not* work with the kernel and userland ones at the same time,
-        * regardless of what we do with set_fs().  And we are talking about
-        * econet-over-ethernet here, so "it's only ARM anyway" doesn't
-        * apply.  Any suggestions on fixing that code?         -- AV
-        */
        iov[0].iov_base = (void *)&ah;
        iov[0].iov_len = size;
-       for (i = 0; i < msg->msg_iovlen; i++) {
-               void __user *base = msg->msg_iov[i].iov_base;
-               size_t iov_len = msg->msg_iov[i].iov_len;
-               /* Check it now since we switch to KERNEL_DS later. */
-               if (!access_ok(VERIFY_READ, base, iov_len)) {
-                       mutex_unlock(&econet_mutex);
-                       return -EFAULT;
-               }
-               iov[i+1].iov_base = base;
-               iov[i+1].iov_len = iov_len;
-               size += iov_len;
+
+       userbuf = vmalloc(len);
+       if (userbuf == NULL) {
+               err = -ENOMEM;
+               goto error;
        }
 
+       iov[1].iov_base = userbuf;
+       iov[1].iov_len = len;
+       err = memcpy_fromiovec(userbuf, msg->msg_iov, len);
+       if (err)
+               goto error_free_buf;
+
        /* Get a skbuff (no data, just holds our cb information) */
        if ((skb = sock_alloc_send_skb(sk, 0,
                                       msg->msg_flags & MSG_DONTWAIT,
-                                      &err)) == NULL) {
-               mutex_unlock(&econet_mutex);
-               return err;
-       }
+                                      &err)) == NULL)
+               goto error_free_buf;
 
        eb = (struct ec_cb *)&skb->cb;

So, instead of using that straightforward copying loop, it allocates the appropriate space using vmalloc() and then uses memcpy_fromiovec() to copy the user derived data to the newly allocated kernel buffer. Also, a new label ‘error_free_buf’ was added to free the allocated space and avoid memory leaks.

        set_fs(oldfs);
+
+error_free_buf:
+       vfree(userbuf);
 #else
        err = -EPROTOTYPE;
 #endif
+       error:
        mutex_unlock(&econet_mutex);

And of course, the ‘udpmsg.msg_iovlen’ was changed to avoid using the user controlled length directly.

        udpmsg.msg_iov = &iov[0];
-       udpmsg.msg_iovlen = msg->msg_iovlen + 1;
+       udpmsg.msg_iovlen = 2;
        udpmsg.msg_control = NULL;

Regarding its exploitation, Jon Oberheide wrote a blog post that you can find here. Although it is very well written and useful, the technique is not entirely new. Almost any kernel level exploit depends on that same principle. Find the closest function pointer that you can have a controlled write operation, map a privilege escalation code that address and finally make a call to it.
Undoubtedly, Jon Oberheide used restart_block() callback function which I’m not aware of any public exploits being using it but either way it’s still not a new exploitation technique.

About these ads

Written by xorl

December 1, 2010 at 07:21

Posted in bugs, linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 63 other followers