Monday, 26 November 2018

Pitfalls Using strcat

strcat is a C standard library call that concatenates strings. strncat is a similar call with a notion of bounds checking. Correct use of strcat and strncat can be problematic and it's easy for developers to use these APIs incorrectly. I'll outline some of the problems and show real code in Kali/Debian Linux that uses them incorrectly.

Introduction

Code review is necessary to aid secure development. Code review is also a primary tool used in vulnerability research. Although application development is popular today, systems languages like C are dominant in Operating Systems and in embedded devices.

The C standard library includes a number of string related APIs. Strings have long been a source of vulnerabilities in C and in this blog post, I'll highlight issues related to the strcat/strncat API.

The strcat/strncat API is defined in the Linux man pages as:
char *strcat(char *destconst char *src);
char *strncat(char *destconst char *srcsize_t n);  
The strcat() function appends the src string to the dest string, overwriting the terminating null byte ('\0') at the end of dest, and then adds a terminating null byte. The strings may not overlap, and the dest string must have enough space for the result. If dest is not large enough, program behavior is unpredictable; buffer overruns are a favorite avenue for attacking secure programs.  
The strncat() function is similar, except that it will use at most n bytes from src; and src does not need to be null-terminated if it contains n or more bytes.

Bug Pattern 1

The simplest bug pattern is not using the bounds checked version of strcat.

Here is a current example from a Kali/Debian package tbb-2017_U7/examples/parallel_for/tachyon/src/main.cpp

static char *window_title_string (int argc, const char **argv)
{
    int i;
    char *name;

    name = (char *) malloc (8192);
    char *title = getenv ("TITLE");
    if( title ) strcpy( name, title );
    else {
        if(strrchr(argv[0], '\\')) strcpy (name, strrchr(argv[0], '\\')+1);
        else if(strrchr(argv[0], '/')) strcpy (name, strrchr(argv[0], '/')+1);
        else strcpy (name, *argv[0]?argv[0]:"Tachyon");
    }
    for (i = 1; i < argc; i++) {
        strcat (name, " ");
        strcat (name, argv[i]);
    }
#ifdef _DEBUG
    strcat (name, " (DEBUG BUILD)");
#endif
    return name;
}

argv is passed via command line arguments and if the length of the command line arguments exceeds 8192, then a buffer overflow will occur.

Note there is also another simple buffer overflow in the above code.

    name = (char *) malloc (8192);
    char *title = getenv ("TITLE");
    if( title ) strcpy( name, title )

This is a classic heap overflow based on a large environment variable. To trigger these types of environment variable overflows, from the shell, we can use this command before running our vulnerable program

$ export TITLE=$(python -c  'print "A" * 10000')
$ ./vulnerable_program

Bug Pattern 2

The simplest fix to the previous bug pattern is to use the bounds checked version of strcat. However, the size field in strncat can be confusing to developers. The size indicates how many characters from source string to write, not the total size of the destination string. As such, a common bug pattern is to use the destination size for the size field ignoring the fact that if the destination string is of non zero length, the strncat call will cause a buffer overflow.

Here is a current example in openbios-1.1.git20171015/fs/hfs/hfs_fs.c

static int
_search( hfsvol *vol, const char *path, const char *sname, hfsfile **ret_fd )
{
        hfsdir *dir;
        hfsdirent ent;
        int topdir=0, status = 1;
        char *p, buf[256];

        strncpy( buf, path, sizeof(buf) );
        if( buf[strlen(buf)-1] != ':' )
                strncat( buf, ":", sizeof(buf) );
        buf[sizeof(buf)-1] = 0;

The above code is also interesting that strncpy() has a bug in it too. From the strncpy man page:
The strncpy() function is similar, except that at most n bytes of src are copied. Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.

The above code does not guarantee NUL termination before the strncat and it tries to explicitly NUL terminate too late. This might result in the appearance of a large string depending on the state of the uninitialised stack and therefore might lead to another buffer overflow.

Bug Pattern 3

A simple fix for bug pattern 2 is to take into account the length of the destination string. However, it's quite easy to introduce an off-by-1 and write past the defined buffer by 1 NUL byte.

Here is a current example in wmi-1.3.16/Samba/source/client/tree.c

  /* 
   * Got a list of comps now, should check that we did not hit a workgroup
   * when we got other things as well ... Later
   *
   * Now, build the path
   */

  snprintf(path_string, sizeof(path_string), "smb:/");

  for (j = i - 1; j >= 0; j--) {

    strncat(path_string, "/", sizeof(path_string) - strlen(path_string));
    strncat(path_string, comps[j], sizeof(path_string) - strlen(path_string));

  }
  
  fprintf(stdout, "Path string = %s\n", path_string);

  return path_string;

Correct Code

Let's look at some correct code in android-tools_5.1.1_r38/system/core/adb/commandline.c:


    snprintf(buf, sizeof(buf), "backup");
    for (argc--, argv++; argc; argc--, argv++) {
        strncat(buf, ":", sizeof(buf) - strlen(buf) - 1);
        strncat(buf, argv[0], sizeof(buf) - strlen(buf) - 1);
    }

Correct Code?

We'll keep on looking at the previous code. Are they being consistent in other uses of strncat?

static int logcat(transport_type transport, char* serial, int argc, char **argv)
{
    char buf[4096];

    char *log_tags;
    char *quoted;

    log_tags = getenv("ANDROID_LOG_TAGS");
    quoted = escape_arg(log_tags == NULL ? "" : log_tags);
    snprintf(buf, sizeof(buf),
            "shell:export ANDROID_LOG_TAGS=\"%s\"; exec logcat", quoted);
    free(quoted);

    if (!strcmp(argv[0], "longcat")) {
        strncat(buf, " -v long", sizeof(buf) - 1);
    }

    argc -= 1;
    argv += 1;
    while(argc-- > 0) {
        quoted = escape_arg(*argv++);
        strncat(buf, " ", sizeof(buf) - 1);
        strncat(buf, quoted, sizeof(buf) - 1);
        free(quoted);
    }

    send_shellcommand(transport, serial, buf);
    return 0;
}

It's back to simple strncat misuse leading to a buffer overflow. It shows you that even within a single source file, consistency and security are not always evident. It probably means that multiple developers worked on this code.

Conclusion

strcat and strncat are simple APIs that are often misused. It is prudent to be aware of the misuse patterns to identify vulnerabilities.

Sunday, 25 November 2018

Memory Bugs in Multiple Linux Kernel Drivers using DebugFS

Multiple drivers in the Linux Kernel using the DebugFS follow a bug pattern enabling memory disclosure and corruption of heap allocated memory. Generally, kernel memory is copied back into user space using an incorrect length field leading to memory disclosure.

There's no need for concern. These bugs are almost completely mitigated by the kernel configuration CONFIG_HARDENED_USERCOPY, which is set as a default on many Linux distributions. In the code review below, only 1 bug out of 4 is not mitigated by this configuration. Additionally, these bugs have low impact because DebugFS is normally only enabled by kernel developers. It is unlikely for production kernels to enable this feature.

Introduction

The Linux kernel is the heart of the operating system and controls things from device interfacing to process scheduling. DebugFS is a filesystem used internally by kernel developers to provide additional debugging information undesirable in production.

From Wikipedia,


debugfs is a special file system available in the Linux kernel since version 2.6.10-rc3.[1] It was written by Greg Kroah-Hartman.[2]debugfs is a simple-to-use RAM-based file system specially designed for debugging purposes. It exists as a simple way for kernel developers to make information available to user space.[3] Unlike /proc, which is only meant for information about a process, or sysfs, which has strict one-value-per-file rules, debugfs has no rules at all. Developers can put any information they want there.[4]

In the Linux kernel, debugfs is enabled using the DEBUGFS configuration variable.

Formatted String APIs in the Kernel

Before we look at the bugs, let's look at some kernel APIs which are used in triggering the bugs.
Most people are probably familiar with snprintf(). snprintf will write a formatted string and place it into a buffer. It performs bounds checking so will prevent simple buffer overflows.
snprintf — Format a string and place it in a buffer

int snprintf (char * buf,
size_t size,
const char * fmt,
...);

The return value is the number of characters which would be generated for the given input, excluding the trailing null, as per ISO C99. If the return is greater than or equal to size, the resulting string is truncated.
The return value is the number of bytes it would have written if bounds checking was ignored. The return value allows you to determine the number of characters needed in the buffer, so the correct size buffer may be allocated before trying to fully construct the output.
In the Linux kernel, an additional API is provided which slightly modifies the semantics of snprintf. In this case, the return value is altered.

scnprintf — Format a string and place it in a buffer

int scnprintf (char * buf,
size_t size,
const char * fmt,
...);

The return value is the number of characters written into buf not including the trailing '\0'. If size is == 0 the function returns 0. 

This is interesting. And if people aren't aware of the differences between scnprintf and snprintf, there is an opportunity for vulnerabilities.

Userspace Copies

Memory gets copied back and forth between userspace and kernelspace. This is a mechanism that could allow either memory corruption if too much memory was copied into kernelspace, or memory disclosure if too much memory was copied into userspace.

One API of interest is simple_copy_from_buffer.
ssize_t simple_read_from_buffer(void __user * to, size_t count, loff_t * ppos, const void * from, size_t available) 
copy data from the buffer to user space

The simple_read_from_buffer() function reads up to count bytes from the buffer from at offset ppos into the user space address starting at to.
On success, the number of bytes read is returned and the offset ppos is advanced by this number, or negative value is returned on error.

Hardened Usercopies

simple_read_from_buffer uses the copy_to_user API internally. Furthmore, copy_to_user is normally protected by the HARDENED_USERCOPY mitigation.

From https://lwn.net/Articles/727322/
Much of the heap memory used within the kernel is obtained from the slab allocator. The hardened usercopy patch set, merged for the 4.8 kernel, attempts to limit the impact of erroneous copy operations by ensuring that no single operation can cross the boundary between one slab-allocated object and the next. But the kernel gets a lot of large memory objects from the slab allocator, and it is often not necessary to copy the entire object between the kernel and user space. In cases where only part of an object needs to be copied, it would be useful to prevent a rogue copy operation from copying to or from parts of the structure that do not need to be exposed in this way. 
Ultimately, this means that if simple_copy_to_user copies heap allocated memory, then this mitigation will prevent a memory disclosure.

DebugFS Drivers Done the Right Way

Here is a kernel driver writing data to a buffer that will be copied back into userspace. Note that it uses scnprintf.
        buf = kmalloc(buf_size, GFP_KERNEL);

        if (!buf)
                return -ENOMEM;

        off = 0;
        off += scnprintf(buf + off, buf_size - off,
                        "Mp2 Device Information:\n");

        off += scnprintf(buf + off, buf_size - off,
                        "========================\n");
        off += scnprintf(buf + off, buf_size - off,
                        "\tMP2 C2P Message Register Dump:\n\n");
        u.v32 = readl(privdata->mmio + AMD_C2P_MSG0);
        off += scnprintf(buf + off, buf_size - off,
                        "AMD_C2P_MSG0 -\t\t\t%#06x\n", u.v32);

...

        u.v32 = readl(privdata->mmio + AMD_P2C_MSG_INTSTS);
        off += scnprintf(buf + off, buf_size - off,
                        "AMD_P2C_MSG_INTSTS -\t\t%#06x\n", u.v32);

        ret = simple_read_from_buffer(ubuf, count, offp, buf, off);
        kfree(buf);
        return ret;
}

There are a couple of interesting points here. Let's outline the algorithm.
  1. Allocate a kernel buffer
  2. Write formatted content to the buffer.
    1. Ensure the buffer does not overflow by using the appropriate size value in scnprintf
    2. Track how much data is actually written by summing the return values of scnprintf
  3. Copy buffer back to userspace
The code above requires scnprintf to work.

DebugFS Almost Done the Right Way

Let's look at a partially correct driver using snprintf.
        struct eg20t_port *priv = file->private_data;
        char *buf;
        u32 len = 0;
        ssize_t ret;
        unsigned char lcr;

        buf = kzalloc(PCH_REGS_BUFSIZE, GFP_KERNEL);
        if (!buf)
                return 0;

        len += snprintf(buf + len, PCH_REGS_BUFSIZE - len,
                        "PCH EG20T port[%d] regs:\n", priv->port.line);

        len += snprintf(buf + len, PCH_REGS_BUFSIZE - len,
                        "=================================\n");
        len += snprintf(buf + len, PCH_REGS_BUFSIZE - len,
                        "IER: \t0x%02x\n", ioread8(priv->membase + UART_IER));

...

        if (len > PCH_REGS_BUFSIZE)
                len = PCH_REGS_BUFSIZE;

        ret =  simple_read_from_buffer(user_buf, count, ppos, buf, len);
        kfree(buf);
        return ret;
}

We can see that an extra length check must be made if snprintf is used. If this check was not in place, then it would be possible to leak memory back into user space (providing debugfs was enabled and hardened usercopy was not).

First Bug

Lets look at a code snippet from the snprintf code I showed earlier.

        len += snprintf(buf + len, PCH_REGS_BUFSIZE - len,
                        "=================================\n");

What happens when len is greater than PCH_REGS_BUFSIZE? This could occur if we ended up writing a larger than expected size to the buffer. This is seemingly possible, otherwise, why do we need all the extra code for bounds checking? The code tries to mitigate against larger than expected results by using bounds checking in snprintf.

Lets assume len can be greater than PCH_REGS_BUFSIZE.

snprintf will protect the driver right?

When len exceeds the expected bounds, PCH_REGS_BUFSIZE - len becomes negative. And this value is passed to snprintf. That's not good.

int vsnprintf(char *buf, size_t size, const char *fmt, va_list args)
{
        unsigned long long num;
        char *str, *end;
        struct printf_spec spec = {0};

        /* Reject out-of-range values early.  Large positive sizes are
           used for unknown buffer sizes. */
        if (WARN_ON_ONCE(size > INT_MAX))
                return 0;

So size is passed as a negative as I discussed earlier. This  makes size > INT_MAX. We don't get memory corruption, but there will be a warning.

So this almost correct driver will warn, but there won't be any information disclosure or memory corruption.

Memory Corruption (1)

Here is a driver in drivers/misc/lkdtm_core.c

static ssize_t lkdtm_debugfs_read(struct file *f, char __user *user_buf, size_t count, loff_t *off) { char *buf; int i, n, out; buf = (char *)__get_free_page(GFP_KERNEL); if (buf == NULL) return -ENOMEM; n = snprintf(buf, PAGE_SIZE, "Available crash types:\n"); for (i = 0; i < ARRAY_SIZE(crashtypes); i++) { n += snprintf(buf + n, PAGE_SIZE - n, "%s\n", crashtypes[i].name); } buf[n] = '\0'; out = simple_read_from_buffer(user_buf, count, off, buf, n); free_page((unsigned long) buf); return out; }

We know that snprintf does bounds checking and if the bounds are exceeded (n is greater PAGE_SIZE), we'll get a negative size for snprintf. The kernel will issue a warning because it is greater than INT_MAX.

The allocated buffer is manually NUL terminated using buf[n]. If n is greater than PAGE_SIZE, which is a possibility, then we'll have a NUL byte written out of bounds. We achieved memory corruption that will work even without HARDENED_USERCOPY.

Memory Disclosure (2)

Here is a vulnerable driver in drivers/spi/spi-dw.c

#ifdef CONFIG_DEBUG_FS
#define SPI_REGS_BUFSIZE        1024
static ssize_t dw_spi_show_regs(struct file *file, char __user *user_buf,
                size_t count, loff_t *ppos)
{
        struct dw_spi *dws = file->private_data;
        char *buf;
        u32 len = 0;
        ssize_t ret;

        buf = kzalloc(SPI_REGS_BUFSIZE, GFP_KERNEL);
        if (!buf)
                return 0;

        len += snprintf(buf + len, SPI_REGS_BUFSIZE - len,
                        "%s registers:\n", dev_name(&dws->master->dev));
        len += snprintf(buf + len, SPI_REGS_BUFSIZE - len,
                        "=================================\n");
        len += snprintf(buf + len, SPI_REGS_BUFSIZE - len,
...
        len += snprintf(buf + len, SPI_REGS_BUFSIZE - len,
                        "DMARDLR: \t0x%08x\n", dw_readl(dws, DW_SPI_DMARDLR));
        len += snprintf(buf + len, SPI_REGS_BUFSIZE - len,
                        "=================================\n");

        ret = simple_read_from_buffer(user_buf, count, ppos, buf, len);
        kfree(buf);
        return ret;

In this case, len might be large and lead to a userspace copy larger than the kernel buffer.

Memory Disclosure (3)


Let's look at drivers/char/virtio_console.c

static ssize_t debugfs_read(struct file *filp, char __user *ubuf,
                            size_t count, loff_t *offp)
{
        struct port *port;
        char *buf;
        ssize_t ret, out_offset, out_count;

        out_count = 1024;
        buf = kmalloc(out_count, GFP_KERNEL);
        if (!buf)
                return -ENOMEM;

        port = filp->private_data;
        out_offset = 0;
        out_offset += snprintf(buf + out_offset, out_count,
                               "name: %s\n", port->name ? port->name : "");
        out_offset += snprintf(buf + out_offset, out_count - out_offset,
                               "guest_connected: %d\n", port->guest_connected);
...
        out_offset += snprintf(buf + out_offset, out_count - out_offset,
                               "is_console: %s\n",
                               is_console_port(port) ? "yes" : "no");
        out_offset += snprintf(buf + out_offset, out_count - out_offset,
                               "console_vtermno: %u\n", port->cons.vtermno);

        ret = simple_read_from_buffer(ubuf, count, offp, buf, out_offset);
        kfree(buf);
        return ret;
}

Another memory disclosure following the same bug pattern.

Potential Memory Disclosures (4)


Let's look at drivers/net/wireless/marvell/libertas/debugfs.c

static ssize_t lbs_dev_info(struct file *file, char __user *userbuf,
                                  size_t count, loff_t *ppos)
{
        struct lbs_private *priv = file->private_data;
        size_t pos = 0;
        unsigned long addr = get_zeroed_page(GFP_KERNEL);
        char *buf = (char *)addr;
        ssize_t res;
        if (!buf)
                return -ENOMEM;

        pos += snprintf(buf+pos, len-pos, "state = %s\n",
                                szStates[priv->connect_status]);
        pos += snprintf(buf+pos, len-pos, "region_code = %02x\n",
                                (u32) priv->regioncode);

        res = simple_read_from_buffer(userbuf, count, ppos, buf, pos);

        free_page(addr);
        return res;
}

This might not be possible to trigger if the string length is short, but it still presents a buggy pattern . This driver has a number of cases using this pattern however. For example:

        for (i = 0; i < num_of_items; i++) {
                if (d[i].size == 1)
                        val = *((u8 *) d[i].addr);
                else if (d[i].size == 2)
                        val = *((u16 *) d[i].addr);
                else if (d[i].size == 4)
                        val = *((u32 *) d[i].addr);
                else if (d[i].size == 8)
                        val = *((u64 *) d[i].addr);

                pos += sprintf(p + pos, "%s=%d\n", d[i].name, val);
        }

        res = simple_read_from_buffer(userbuf, count, ppos, p, pos);

        free_page(addr);
        return res;
}

Other Bugs

There a few other places following similar patterns, but they are unlikely to be triggered.

Recommendations


The solution is quite easy. Replace snprintf with scnprintf.

Conclusion


This is an interesting look into bug patterns. There is almost certainly going to be a history of cut and pasting insecure code between drivers that leads to problems. It also highlights the problems of duplicate code leading to bugs and vulnerabilities - when code is manually duplicated instead of being centralised, bugs appear.

Overall, these bugs are extremely low impact. Highlighting them is more an exercise into the code review process and the nature of bugs. Still, they are security bugs and should be fixed.

Thursday, 6 September 2018

Linux Kernel Infoleaks

Here are 6 Linux kernel local infoleaks.

InfoSect is available for engagements in code review. Please look at http://infosectcbr.com.au/consulting or check out some of our public code review videos on http://youtube.com/c/InfoSect. Or, check out our code review training at http://infosectcbr.com.au/training.

1 ==========================


/usr/src/linux-source-4.14/drivers/net/appletalk/ipddp.h

struct ipddp_route
{
        struct net_device *dev;             /* Carrier device */
        __be32 ip;                       /* IP address */
        struct atalk_addr at;              /* Gateway appletalk address */
        int flags;
        struct ipddp_route *next;
};


/usr/src/linux-source-4.14/drivers/net/appletalk/ipddp.c

static struct ipddp_route* __ipddp_find_route(struct ipddp_route *rt)
{
        struct ipddp_route *f;

        for(f = ipddp_route_list; f != NULL; f = f->next)
        {
                if(f->ip == rt->ip &&
                   f->at.s_net == rt->at.s_net &&
                   f->at.s_node == rt->at.s_node)
                        return f;
        }

        return NULL;
}
...

                case SIOCFINDIPDDPRT:
                        spin_lock_bh(&ipddp_route_lock);
                        rp = __ipddp_find_route(&rcp);
                        if (rp)
                                memcpy(&rcp2, rp, sizeof(rcp2));
                        spin_unlock_bh(&ipddp_route_lock);

                        if (rp) {
                                if (copy_to_user(rt, &rcp2,
                                                 sizeof(struct ipddp_route)))
                                        return -EFAULT;
                                return 0;
                        } else
                                return -ENOENT;

++ in the ipddp_route struct, there are pointers next and dev which get
++ leaked here.

2 ===================================================

struct snd_ctl_elem_list {
        unsigned int offset;            /* W: first element ID to get */
        unsigned int space;             /* W: count of element IDs to get */
        unsigned int used;              /* R: count of element IDs set */
        unsigned int count;             /* R: count of all elements */
        struct snd_ctl_elem_id __user *pids; /* R: IDs */
        unsigned char reserved[50];
};
+++ note reserved, pids

/usr/src/linux-source-4.14/sound/core/control.c

static int snd_ctl_elem_list(struct snd_card *card,
                             struct snd_ctl_elem_list __user *_list)
{
        struct snd_ctl_elem_list list;
        struct snd_kcontrol *kctl;
        struct snd_ctl_elem_id id;
        unsigned int offset, space, jidx;
        int err = 0;
...

               list_for_each_entry(kctl, &card->controls, list) {
                        if (offset >= kctl->count) {
                                offset -= kctl->count;
                                continue;
                        }
                        for (jidx = offset; jidx < kctl->count; jidx++) {
                                snd_ctl_build_ioff(&id, kctl, jidx);
                                if (copy_to_user(list.pids + list.used, &id,
                                                 sizeof(id))) {
                                        err = -EFAULT;
                                        goto out;
                                }
                                list.used++;
                                if (!--space)
                                        goto out;
                        }
                        offset = 0;
                }
        }
 out:
        up_read(&card->controls_rwsem);
        if (!err && copy_to_user(_list, &list, sizeof(list)))
                err = -EFAULT;
+++ copying list back to userspace leaks a pointer at least


static long snd_ctl_ioctl(struct file *file, unsigned int cmd, unsigned long arg
)
{
        struct snd_ctl_file *ctl;
        struct snd_card *card;
        struct snd_kctl_ioctl *p;
        void __user *argp = (void __user *)arg;
        int __user *ip = argp;
        int err;

        ctl = file->private_data;
        card = ctl->card;
        if (snd_BUG_ON(!card))
                return -ENXIO;
        switch (cmd) {
        case SNDRV_CTL_IOCTL_PVERSION:
                return put_user(SNDRV_CTL_VERSION, ip) ? -EFAULT : 0;
        case SNDRV_CTL_IOCTL_CARD_INFO:
                return snd_ctl_card_info(card, ctl, cmd, argp);
        case SNDRV_CTL_IOCTL_ELEM_LIST:
                return snd_ctl_elem_list(card, argp);

3 ======================================

struct snd_emu10k1_fx8010_info {
        unsigned int internal_tram_size;        /* in samples */
        unsigned int external_tram_size;        /* in samples */
        char fxbus_names[16][32];               /* names of FXBUSes */
        char extin_names[16][32];               /* names of external inputs */
        char extout_names[32][32];              /* names of external outputs */
        unsigned int gpr_controls;              /* count of GPR controls */
};
+++ note the strings/names are fixed lengths of 32

/usr/src/linux-source-4.14/sound/pci/emu10k1/emufx.c

static int snd_emu10k1_fx8010_ioctl(struct snd_hwdep * hw, struct file *file, unsigned int cmd, unsigned long arg)
{
...
        switch (cmd) {
        case SNDRV_EMU10K1_IOCTL_PVERSION:
                emu->support_tlv = 1;
                return put_user(SNDRV_EMU10K1_VERSION, (int __user *)argp);
        case SNDRV_EMU10K1_IOCTL_INFO:
                info = kmalloc(sizeof(*info), GFP_KERNEL);
+++ note kmalloc
                if (!info)
                        return -ENOMEM;
                snd_emu10k1_fx8010_info(emu, info);
                if (copy_to_user(argp, info, sizeof(*info))) {
                        kfree(info);
                        return -EFAULT;
                }
                kfree(info);
                return 0;

...

static void snd_emu10k1_fx8010_info(struct snd_emu10k1 *emu,
                                   struct snd_emu10k1_fx8010_info *info)
{
        char **fxbus, **extin, **extout;
        unsigned short fxbus_mask, extin_mask, extout_mask;
        int res;

        info->internal_tram_size = emu->fx8010.itram_size;
        info->external_tram_size = emu->fx8010.etram_pages.bytes / 2;
        fxbus = fxbuses;
        extin = emu->audigy ? audigy_ins : creative_ins;
        extout = emu->audigy ? audigy_outs : creative_outs;
        fxbus_mask = emu->fx8010.fxbus_mask;
        extin_mask = emu->fx8010.extin_mask;
        extout_mask = emu->fx8010.extout_mask;
        for (res = 0; res < 16; res++, fxbus++, extin++, extout++) {
                copy_string(info->fxbus_names[res], fxbus_mask & (1 << res) ? *fxbus : NULL, "FXBUS", res);
                copy_string(info->extin_names[res], extin_mask & (1 << res) ? *extin : NULL, "Unused", res);
                copy_string(info->extout_names[res], extout_mask & (1 << res) ? *extout : NULL, "Unused", res);
        }
+++ info leaks. the copy_strings partially fill the structure/strings which
+++ are 32 bytes. the remainder of the string is uninitialized (from kmalloc)

...
static void copy_string(char *dst, char *src, char *null, int idx)
{
        if (src == NULL)
                sprintf(dst, "%s %02X", null, idx);
        else
                strcpy(dst, src);
}




4 ====================================

struct inquiry_data {
        bdaddr_t        bdaddr;
        __u8            pscan_rep_mode;
        __u8            pscan_period_mode;
        __u8            pscan_mode;
        __u8            dev_class[3];
        __le16          clock_offset;
        __s8            rssi;
        __u8            ssp_mode;
};
a

/usr/src/linux-source-4.14/net/bluetooth/hci_core.c

static int inquiry_cache_dump(struct hci_dev *hdev, int num, __u8 *buf)
{
        struct discovery_state *cache = &hdev->discovery;
        struct inquiry_info *info = (struct inquiry_info *) buf;
        struct inquiry_entry *e;
        int copied = 0;

        list_for_each_entry(e, &cache->all, all) {
                struct inquiry_data *data = &e->data;

                if (copied >= num)
                        break;

                bacpy(&info->bdaddr, &data->bdaddr);
                info->pscan_rep_mode    = data->pscan_rep_mode;
                info->pscan_period_mode = data->pscan_period_mode;
                info->pscan_mode        = data->pscan_mode;
                memcpy(info->dev_class, data->dev_class, 3);
                info->clock_offset      = data->clock_offset;
+++ note rssi and ssp_mode fields are not filled
+++ because this is from a kmalloc, it is left uninitialized. hence an infoleak

                info++;
                copied++;
        }

...
       /* cache_dump can't sleep. Therefore we allocate temp buffer and then
         * copy it to the user space.
         */
        buf = kmalloc(sizeof(struct inquiry_info) * max_rsp, GFP_KERNEL);
+++ note kmalloc
        if (!buf) {
                err = -ENOMEM;
                goto done;
        }

        hci_dev_lock(hdev);
        ir.num_rsp = inquiry_cache_dump(hdev, max_rsp, buf);
        hci_dev_unlock(hdev);

        BT_DBG("num_rsp %d", ir.num_rsp);

        if (!copy_to_user(ptr, &ir, sizeof(ir))) {
                ptr += sizeof(ir);
                if (copy_to_user(ptr, buf, sizeof(struct inquiry_info) *
                                 ir.num_rsp))
                        err = -EFAULT;
        } else
                err = -EFAULT;

        kfree(buf);

5 ========================================================

/usr/src/linux-source-4.14/drivers/video/fbdev/omap2/omapfb/omapfb-ioctl.c

        buf = vmalloc(mr->buffer_size);
        if (!buf) {
                DBG("vmalloc failed\n");
                return -ENOMEM;
        }

        r = display->driver->memory_read(display, buf, mr->buffer_size,
                        mr->x, mr->y, mr->w, mr->h);
+++ r returns the amount of memory read. can be less than buffer_size

        if (r > 0) {
                if (copy_to_user(mr->buffer, buf, mr->buffer_size))
+++ copies the entire buffer
+++ hence, an infoleak from unitialised memory from vmalloc
                       r = -EFAULT;
        }

        vfree(buf);

6 ==============================================

/usr/src/linux-source-4.14/arch/ia64/sn/kernel/sn2/sn_hwperf.c
a
static long sn_hwperf_ioctl(struct file *fp, u32 op, unsigned long arg)
{
        struct sn_hwperf_ioctl_args a;
...
        r = copy_from_user(&a, (const void __user *)arg,
                sizeof(struct sn_hwperf_ioctl_args));
        if (r != 0) {
                r = -EFAULT;
                goto error;
        }
...
        if (a.ptr) {
                p = vmalloc(a.sz);
                if (!p) {
                        r = -ENOMEM;
                        goto error;
                }
        }

        if (op & SN_HWPERF_OP_MEM_COPYIN) {
                r = copy_from_user(p, (const void __user *)a.ptr, a.sz);
                if (r != 0) {
                        r = -EFAULT;
                        goto error;
                }
        }

...

        switch (op) {
        case SN_HWPERF_GET_CPU_INFO:
...
        case SN_HWPERF_GET_MMRS:
        case SN_HWPERF_SET_MMRS:
        case SN_HWPERF_OBJECT_DISTANCE:
                op_info.p = p;
                op_info.a = &a;
                op_info.v0 = &v0;
                op_info.op = op;
                r = sn_hwperf_op_cpu(&op_info);
                if (r) {
                        r = sn_hwperf_map_err(r);
                        a.v0 = v0;
                        goto error;
                }
                break;

        default:
                /* all other ops are a direct SAL call */
                r = ia64_sn_hwperf_op(sn_hwperf_master_nasid, op,
                              a.arg, a.sz, (u64) p, 0, 0, &v0);
                if (r) {
                        r = sn_hwperf_map_err(r);
                        goto error;
                }
                a.v0 = v0;
                break;
        }
+++ if any of the above cases don't fully overwrite the memory buffer from
+++ vmalloc, then we might have uninitialized memory and hence an infoleak

        if (op & SN_HWPERF_OP_MEM_COPYOUT) {
                r = copy_to_user((void __user *)a.ptr, p, a.sz);
                if (r != 0) {
                        r = -EFAULT;
                        goto error;
                }
        }

=======================================================

Tuesday, 3 July 2018

ASUS DSL-AC3100 Router Firmware BCM Kernel Driver Bug

Time for some kernel bugs.

//********************************************************************************************
// misc. ioctl calls come to here. (flash, led, reset, kernel memory access, etc.)
//********************************************************************************************
static int board_ioctl( struct inode *inode, struct file *flip,
                       unsigned int command, unsigned long arg )
{
    int ret = 0;
    BOARD_IOCTL_PARMS ctrlParms;
    unsigned char ucaMacAddr[NVRAM_MAC_ADDRESS_LEN];
// ARCADYAN
    unsigned char ucaDectRfpi[NVRAM_ARCADYAN_DECT_RFPI_LEN];
    unsigned char ucaDectRxtun[NVRAM_ARCADYAN_DECT_RXTUN_LEN];
/// ARCADYAN

    switch (command) {
#if defined(BRCM_XDSL_DISTPOINT)
    case BOARD_IOCTL_FTTDP_DSP_BOOTER:  
        download_dsp_booter();
        break;
#endif
    case BOARD_IOCTL_FLASH_WRITE:
        if (copy_from_user((void*)&ctrlParms, (void*)arg, sizeof(ctrlParms)) == 0) {

            switch (ctrlParms.action) {
            case SCRATCH_PAD:
                if (ctrlParms.offset == -1)
                    ret =  kerSysScratchPadClearAll();
                else
                    ret = kerSysScratchPadSet(ctrlParms.string, ctrlParms.buf, ctrlParms.offset);
                break;
...
            case NVRAM:
            {
                NVRAM_DATA * pNvramData;

                /*
                 * Note: even though NVRAM access is protected by
                 * flashImageMutex at the kernel level, this protection will
                 * not work if two userspaces processes use ioctls to get
                 * NVRAM data, modify it, and then use this ioctl to write
                 * NVRAM data.  This seems like an unlikely scenario.
                 */
                mutex_lock(&flashImageMutex);
                if (NULL == (pNvramData = readNvramData()))
                {
                    mutex_unlock(&flashImageMutex);
                    return -ENOMEM;
                }
                if ( !strncmp(ctrlParms.string, "WLANFEATURE", 11 ) ) { //Wlan Data data
                    pNvramData->wlanParams[NVRAM_WLAN_PARAMS_LEN-1]= *(unsigned char *)(ctrlParms.string+11);
                    writeNvramDataCrcLocked(pNvramData);
                }
                else if ( !strncmp(ctrlParms.string, "WLANDATA", 8 ) ) { //Wlan Data data
                        int t_strlen=ctrlParms.strLen-8;
                        int nm=_get_wl_nandmanufacture();
                        if(nm<WLAN_MFG_PARTITION_HASSIZE) {
                                if(t_strlen>NVRAM_WLAN_PARAMS_LEN-1)
                                        t_strlen=NVRAM_WLAN_PARAMS_LEN-1;
                                memset((char *)pNvramData + ((size_t) &((NVRAM_DATA *)0)->wlanParams),
                                                0, sizeof(pNvramData->wlanParams)-1 );
                                memcpy( (char *)pNvramData + ((size_t) &((NVRAM_DATA *)0)->wlanParams),
                                                ctrlParms.string+8,
                                                t_strlen);

ok... where to begin?

lets keep the following in mind

typedef struct boardIoctParms
{
    char *string;
    char *buf;
    int strLen;
    int offset;
    BOARD_IOCTL_ACTION action;
    int result;
} BOARD_IOCTL_PARMS;

Lets go back over the code now:

    case BOARD_IOCTL_FLASH_WRITE:
        if (copy_from_user((void*)&ctrlParms, (void*)arg, sizeof(ctrlParms)) == 0) {

This is pretty straight forward. Copy the ioctl user args into kernelspace.

                else if ( !strncmp(ctrlParms.string, "WLANDATA", 8 ) ) { //Wlan Data data
                        int t_strlen=ctrlParms.strLen-8;

Huh? strncmp without copying the string from user to kernelspace? That doesn't seem good. Lets assume we can make this work.

                       int t_strlen=ctrlParms.strLen-8;
                        int nm=_get_wl_nandmanufacture();
                        if(nm<WLAN_MFG_PARTITION_HASSIZE) {
                                if(t_strlen>NVRAM_WLAN_PARAMS_LEN-1)
                                        t_strlen=NVRAM_WLAN_PARAMS_LEN-1;

t_strlen is a signed int. And ctrlParms.strLen is untrusted. Can we make t_strlen negative? We just need to pass the t_strlen > NVRAM_WLAN_PARAMS_LEN-1 check.

#define NVRAM_WLAN_PARAMS_LEN      256

Fortunately it's a signed integer, which means we can simply make ctrlParms.strLen less than 8. And we'll get a negative value into t_strlen.

                                 memset((char *)pNvramData + ((size_t) &((NVRAM_DATA *)0)->wlanParams),
                                                0, sizeof(pNvramData->wlanParams)-1 );
                                memcpy( (char *)pNvramData + ((size_t) &((NVRAM_DATA *)0)->wlanParams),
                                                ctrlParms.string+8,
                                                t_strlen);

And there we go. a memcpy with a negative size. This will cause memory corruption.


ASUS DSL-AC3100 Router Firmware radvd Bugs

Lets look at packet processing in the router code

                case ND_OPT_RDNSS_INFORMATION:
                        rdnssinfo = (struct nd_opt_rdnss_info_local *) opt_str;
                        count = rdnssinfo->nd_opt_rdnssi_len;

                        /* Check the RNDSS addresses received */
                        switch (count) {
                                case 7:

Now opt_str is the options part of the packet. Does the code check that the packet is big enough to account for these options? No. There are a bunch of cases like this. All lead to out of bounds memory access.

Lets look at the current radvd source from a recent Linux distro

                case ND_OPT_RDNSS_INFORMATION: {
                        char rdnss_str[INET6_ADDRSTRLEN];
                        struct AdvRDNSS *rdnss = 0;
                        struct nd_opt_rdnss_info_local *rdnssinfo = (struct nd_opt_rdnss_info_local *)opt_str;
                        if (len < sizeof(*rdnssinfo))
                                return;
                        int count = rdnssinfo->nd_opt_rdnssi_len;

                        /* Check the RNDSS addresses received */
                        switch (count) {
                        case 7:

It's clearly been fixed. But the patches haven't been backported to the router firmware.

ASUS DSL-AC3100 Router Firmware sendpackets Bug

This is a tiny bug, but it's still a bug nevertheless. strncpy is not guaranteed to NUL terminate if the max buf size is reached. The code below doesn't explicity NUL terminate the strncpy to iface. It's probably not been triggered because the stack is likely to be clean when the program reaches the strncpy. However, it's not guaranteed.

void
main (int argc, char **argv)
{
  pcap_t *fp;
  char errbuf[PCAP_ERRBUF_SIZE];
  int i;
  int j;
  int nstreams;

  int cnt;
  int tdelay;
  char iface[32];
  int patternlen;
  int opt;
  struct timeval tstart;
  struct timeval t;
  struct timeval tint;
  int pdone;
  int pbusy;

  iface[0] = 0;
  patternlen = 0;
  nstreams = 0;
  tdelay = 0;


  while ((opt = getopt (argc, argv, "i:t:c:p:")) != -1)
    {
      switch (opt)
        {
        case 'i':
          strncpy (iface, optarg, 32);
          break;

ASUS DSL-AC3100 Router Firmware PPP bug

Lets look at the following code in the current version of ppp on Linux.

static void
cbcp_recvreq(us, pckt, pcktlen)
    cbcp_state *us;
    u_char *pckt;
    int pcktlen;
{
    u_char type, opt_len, delay, addr_type;
    char address[256];
    int len = pcktlen;

    address[0] = 0;

    while (len >= 2) {
        dbglog("length: %d", len);

        GETCHAR(type, pckt);
        GETCHAR(opt_len, pckt);
        if (opt_len < 2 || opt_len > len)
            break;

        if (opt_len > 2)
            GETCHAR(delay, pckt);

        us->us_allowed |= (1 << type);

        switch(type) {
        case CB_CONF_NO:
            dbglog("no callback allowed");
            break;

        case CB_CONF_USER:
            dbglog("user callback allowed");
            if (opt_len > 4) {
                GETCHAR(addr_type, pckt);
                memcpy(address, pckt, opt_len - 4);
                address[opt_len - 4] = 0;
                if (address[0])
                    dbglog("address: %s", address);
            }
            break;

        case CB_CONF_ADMIN:
            dbglog("user admin defined allowed");
            break;

        case CB_CONF_LIST:
            break;
        }
        len -= opt_len;
    }
    if (len != 0) {
        if (debug)
            dbglog("cbcp_recvreq: malformed packet (%d bytes left)", len);
        return;
    }

    cbcp_resp(us);
}

Now I draw your attention to the loop condition. while (len >= 2). Lets look at the router firmware code:

    while (len) {
        dbglog("length: %d", len);

        GETCHAR(type, pckt);
        GETCHAR(opt_len, pckt);

        if (opt_len > 2)
            GETCHAR(delay, pckt);

        us->us_allowed |= (1 << type);

        switch(type) {
        case CB_CONF_NO:
            dbglog("no callback allowed");
            break;

        case CB_CONF_USER:
            dbglog("user callback allowed");
            if (opt_len > 4) {
                GETCHAR(addr_type, pckt);
                memcpy(address, pckt, opt_len - 4);
                address[opt_len - 4] = 0;
                if (address[0])
                    dbglog("address: %s", address);
            }
            break;

        case CB_CONF_ADMIN:
            dbglog("user admin defined allowed");
            break;

        case CB_CONF_LIST:
            break;
        }
        len -= opt_len;
    }

What's going to happen if the opt_len's don't add up to the packet size? Lets think of when len is 1 and opt_len is 2. It'll wrap around to a non zero negative. And the loop will keep iterating.

Clearly buggy.

ASUS DSL-AC3100 Router Firmware DHCP Bug

It's great that ASUS makes the GPL firmware source for their routers easy to download. I wish more vendors would do this.

Unfortunately, it didn't take more than a few minutes of auditing to come across the DHCPd code. Lets look at the original non ASUS code in wide-dhcp-server.

int
dhcp6_get_options(p, ep, optinfo)
        struct dhcp6opt *p, *ep;
        struct dhcp6_optinfo *optinfo;
{
        struct dhcp6opt *np, opth;
        int i, opt, optlen, reqopts, num;
        u_int16_t num16;
        char *bp, *cp, *val;
        u_int16_t val16;
        u_int32_t val32;
        struct dhcp6opt_ia optia;
        struct dhcp6_ia ia;
        struct dhcp6_list sublist;
        int authinfolen;

        bp = (char *)p;
        for (; p + 1 <= ep; p = np) {
                struct duid duid0;

                /*
                 * get the option header.  XXX: since there is no guarantee
                 * about the header alignment, we need to make a local copy.
                 */
                memcpy(&opth, p, sizeof(opth));
                optlen = ntohs(opth.dh6opt_len);

...
                case DH6OPT_STATUS_CODE:
                        if (optlen < sizeof(u_int16_t))
                                goto malformed;
                        memcpy(&val16, cp, sizeof(val16));
                        num16 = ntohs(val16);
                        debug_printf(LOG_DEBUG, "", "  status code: %s",
                            dhcp6_stcodestr(num16));

It's pretty clear that the options parsing code has to verify optlen. We also note that optlen is a signed 32-bit integer and that optlen casts ntohs() which returns a 16-bit unsigned int by default.

Lets look at the router firmware code which has added its own extensions:

      // brcm: get ACS URL from dhcp server option 17
      case DH6OPT_VENDOR_OPTS:
      {
          char *option_string;
          int option_len;
          u_int32_t enterprise_id;
          u_int16_t sub_option_num=1;
          int sub_option_offset=0;
          int sub_option_len=0;

          /* No guarentee on alignment, so copy to word variable */
          memcpy(&enterprise_id, cp, sizeof(enterprise_id));
          enterprise_id = ntohl(enterprise_id);

          // the first word in the data is the enterprise number.
          // I cannot find an Enterprise number for Broadband Forum in the
          // IANA database, so don't check for now.  See page 85 of RFC 3315.
          dprintf(LOG_DEBUG, FNAME, "    enterprise-number: %d (0x%x)\n",
                  enterprise_id, enterprise_id);

          // advance to point to the real data
          option_string = cp + 4;
          option_len = optlen - 4;

          // look for sub option 1: ManagementServer.URL
          if (findEncapVendorSpecificOption(option_string, option_len,
                 sub_option_num, &sub_option_offset, &sub_option_len))
          {
             int copyLen = sizeof(optinfo->acsURL) - 1;
             if (copyLen > sub_option_len) copyLen=sub_option_len;

             memcpy(optinfo->acsURL, &option_string[sub_option_offset], copyLen);
             optinfo->acsURL[copyLen] = '\0';
             // fprintf(stderr, "Found acsURL %s!!\n", optinfo->acsURL);
          }

Now we note that option_len is optlen - 4. There is no input validation on optlen. Because option_len is a signed int, if we make optlen < 4, we can get a negative value into option_len. Now _if_ we were able to enter the code that does:

             int copyLen = sizeof(optinfo->acsURL) - 1;
             if (copyLen > sub_option_len) copyLen=sub_option_len;

And sub_option_len was also negative, then we could get a buffer overflow, since copyLen and sub_option_len are both signed. Lets look at the function that triggers all of this: 

int findEncapVendorSpecificOption(const char *option, int len,
                                  u_int16_t sub_option_num,
                                  int *sub_option_offset, int *sub_option_len)
{
   struct dhcp6opt hdr;
   int i=0;
   u_int16_t curr_sub_option_num;
   int curr_sub_option_len;

   while (i < len)
   {
      /* no guarantee on alignment, so copy header */
      memcpy(&hdr, &option[i], sizeof(hdr));
      curr_sub_option_num = ntohs(hdr.dh6opt_type);
      curr_sub_option_len = ntohs(hdr.dh6opt_len);

      /* sanity check */
      if (i + 4 + curr_sub_option_len > len)
      {
         printf("sub-option exceeds len, %d %d %d",
                 i, curr_sub_option_len, len);
         return 0;
      }

      if (sub_option_num == curr_sub_option_num)
      {
         *sub_option_offset = i+4;
         *sub_option_len = curr_sub_option_len;
         return 1;
      }

      i += 4 + curr_sub_option_len;  /* advance i to the next sub-option */
   }

   return 0;
}

Hmm.. we get blocked. When len is negative, we can't enter the loop. Sure, we can still get out of bounds reads for option_len is positive since len is not validated. But it's unlikely that we can use this bug for anything interesting in terms of memory corruption.

So... tl;dr No input validation on length in vendor specific dhcp code. Out of bounds memory access. No memory corruption.

What other goodies exist in vendor supplied GPL source code?

Sunday, 1 July 2018

Month of Kali Off-By-Ones #1 #2 # 3 #4

The following 4 code snippets have classic off-by-one bugs. They don't explicitly nul terminate strings after a strncpy. If strncpy reaches its buffer max, it won't nul terminate. In fact, strncpy's behaviour is quite problematic and prone to this type of bug so OpenBSD introduced strlcpy many years ago and other OSs have done the same.

source package: libaria #1

char myWaitingForDir[2048];



AREXPORT void ArClientFileLister::changeToAbsDir(const char *dir)
{
  myDataMutex.lock();
  strncpy(myWaitingForDir, dir, sizeof(myWaitingForDir));
  myLastRequested.setToNow();
  //printf("Getting %s\n", myWaitingForDir);
  std::string waitingFor = myWaitingForDir;
  myDataMutex.unlock();
  //myClient->requestOnceWithString("getDirListing", waitingFor.c_str());
  getDirListing(waitingFor.c_str());
}

source package: xenomai #2

static inline void xntimer_set_name(xntimer_t *timer, const char *name)
{
#ifdef CONFIG_XENO_OPT_STATS
        strncpy(timer->name, name, sizeof(timer->name));
#endif /* CONFIG_XENO_OPT_STATS */
}

source package: service-wrapper #3

    char localAddr[128];
...
        addr.S_un.S_addr = (u_long)tcpTable->table[i].dwLocalAddr;
        strncpy(localAddr, inet_ntoa(addr), sizeof(localAddr));
        localPort = ntohs((u_short)tcpTable->table[i].dwLocalPort);
        
#ifdef GETPORTSTATUS_DEBUG
        addr.S_un.S_addr = (u_long)tcpTable->table[i].dwRemoteAddr;
        strncpy(remoteAddr, inet_ntoa(addr), sizeof(remoteAddr));

is this a bug? inet_ntoa will be quite small and nul terminate.. I class it as a bad development practice.

source package: nfs-utils #4

        static char buf[PATH_MAX];
        struct stat st;
        char *path;

        /* First: test length of name and whether it exists */
        if (lstat(parentdir, &st) == -1) {
                (void)fprintf(stderr, "%s: Failed to stat %s: %s",
                                progname, parentdir, strerror(errno));
                return false;
        }

        /* Ensure we have a clean directory pathname */
        strncpy(buf, parentdir, sizeof(buf));
        path = dirname(buf);
        if (*path == '.') {
                (void)fprintf(stderr, "%s: Unusable directory %s",
                                progname, parentdir);
                return false;
        }

Exploiting the Lorex 2K Indoor Wifi at Pwn2Own Ireland

Introduction In October InfoSect participated in Pwn2Own Ireland 2024 and successfully exploited the Sonos Era 300 smart speaker and Lor...