[3241] in bugtraq

home help back first fref pref prev next nref lref last post

Re: libresolv+ bug

daemon@ATHENA.MIT.EDU (Zygo Blaxell)
Wed Aug 21 17:35:47 1996

Date: 	Wed, 21 Aug 1996 16:43:21 -0400
Reply-To: Bugtraq List <BUGTRAQ@netspace.org>
From: Zygo Blaxell <zblaxell@MYRUS.COM>
To: Multiple recipients of list BUGTRAQ <BUGTRAQ@netspace.org>

In article <199608210055.TAA18125@enteract.com>,
Thomas Ptacek  <tqbf@enteract.com> wrote:
>If you'd like to avoid the "conventional" stack overflow tactics, you can
>do so without hardware modification (<stifling giggles>) simply by keeping
>as many "buffers" as you can in dynamically allocated memory (malloc char
>stars instead of declaring character arrays, etc, etc). Beyond subversions
>that are made possible by manipulating the program's data (what filename
>is that SUID program writing to? What UID did getuid() return? etc...),
>nobody's talking about a way to exploit dynamically allocated memory to
>cause inappropriate transfers of control.

Dynamically allocated memory can still be exploited; it's just harder.

Hardware-protected memory on both ends of every buffer and between
the return address on a stack frame and the current function's automatic
variables would be good for security purposes.  That way most simple
buffer overruns would get caught by the CPU and translated into a
segmentation fault.

        int foo(char *not_too_long_please)
        {
                int a,b,c;
                char buf[100];
                char buf2[1000];
                buf[100]=0;                               /* causes SIGSEGV */
                buf2[1000];                          /* also causes SIGSEGV */
                sprintf(buf,"foo%s\n",not_too_long_please);    /* safer now */
        }

A simple and reasonably efficient implementation would simply arrange for
arrays to be allocated at the end of a hardware memory page.  So, assuming
a CPU with a stack that grows downward, 4-byte ints and 4K pages, we have
memory that might look like this:

        0x00122000: 4096 unallocated, read/write prohibited bytes
        0x00123000: 3996 unallocated bytes
                    100 bytes for 'buf'
        0x00124000: 4096 unallocated, read/write prohibited bytes
        0x00125000: 3096 unallocated bytes
                    1000 bytes for 'buf2'
        0x00126000: 4096 unallocated, read/write prohibited bytes
        0x00127000: unknown number of unallocated bytes
                    12 bytes for a, b, and c
                    stack frame for 'foo'
                    foo's caller's automatic variables and frame
                    foo's caller's caller's automatic variables and frame

It's mostly safe to allow the stack frame for a function that is called
by 'foo' to appear immediately before 'buf', but not safe to allow the
non-array variables (a, b, c) to be there.  Any buffer overrun at the high
end of the buffer would cause a segmentation fault, but a buffer underrun
at the low end of the buffer would silently clobber anything else in
the same page.  It would be caught if the overrun hit the low end of the
page.

Note that we only use this if we have an array in automatic, global,
or static memory, or we take the address of a simple type; a function
like this one would have no special variable allocation:

        char *bar(char *parameter)
        {
                char a;                         /* not an array */
                int *b=malloc(4*sizeof(int));   /* a pointer, not an array */
                /* malloc, of course, would give you your very own page... */
        }

What to do with 'structs' and 'unions' that contain arrays I'll
leave as an exercise for the reader.

I'm sure a similar motivation prompted OS people to unmap the page
containing address 0, to catch NULL pointer dereferences.

It does create some problems, however; most compilers prefer automatic
variables to have some constant offset from the current value of the stack
pointer; this scheme violates that because the positions of some automatic
variables vary depending on the amount of space used in the last page
of the stack before a function is called.  This can be fixed, but at the
expense of some exotic optimizations.

Alternatively, the protected variables could be allocated via malloc()
which would do the memory protection and select unmapped pages for you;
part of the function return sequence would call free() on the array.
This makes automatic arrays not really automatic after all.

Changing the CPU's memory maps is not something you want to do very often
either.  The OS or runtime environment could help by allocating alternate
pages in the stack segment beforehand; the function call sequence would
then have to be careful to avoid hitting one of these unallocated areas
of stack space.  This is a little easier for malloc() and a lot easier
for global and static variables, which only have to be allocated once at
program startup.

We could reserve this kind of variable allocation to special programs by
defining an 'detect-buffer-overruns' flag for the compiler.  That way
only setuid and privileged programs would suffer; most of these are
short-lived or long-lived-but-simple programs that would not suffer
very much from having memory-gobbling recursion or lots of mprotect()
system calls around their automatic array variables.

Of course, now you have to remember to compile setuid programs with the
'detect-buffer-overruns' flag...

>But, as we all know, that's not a particularly effective solution to the
>problem. The real problems, as I'm sure you'll agree, der Mouse, are that
>SUID programs aren't being written carefully enough, library routines that
>are potentially depended on by SUID programs aren't written with those
>security issues in mind, and, in general, most Unix OS's give
>"priveledged" programs that need to do one or two specific things far, far
>too much power.

The thing is, it's ridiculously hard to train programmers to write safe
code in a language like C.  In C you're forced to know about too many
details of memory allocation; you shouldn't need to care about allocating
space for strings when all you want to do is perform one trivial system
call and parse a configuration file.

>Or, more succinctly, all the hardware opcode-munging and internal bounds
>checking and runtime sanity checks in the world aren't going to save an
>SUID program from the next 2049 race conditions we'll find in it after
>fixing buffer overflows.

>Perhaps a better "fix" to discuss would be to find out what problematic
>SUID programs need their SUID permissions for, and discuss a way around
>that. I, for one, would be really happy to see the next discussion about
>the next SUID hole be accompanied with a discussion of "how can we rewrite
>this program so it never needs to run as root?".

A program like 'mount' could be trivially replaced by a combination of:

1.  A general-purpose setuid program that acts as a switching point for
    privileged programs.  This program destroys unfriendly environment
    variables, and could even do most of the things that setuid programs
    need to be setuid for; see below.

2.  An interface script written in a language that handles its own memory
    allocation, like perl, or even GNU flavors of sed and awk; tcl would
    work as well.  All of these programs are vulnerable to environment
    variable attacks; some are vulnerable to even nastier ones (like
    the Tk 'send' command).  This would check configuration files,
    command-line arguments, etc against local policies.

3.  a non-setuid 'mount' program.

If new 'features' show up in libraries or environment variables, step
1 gets rid of them.  If step 1 needs to be changed, it only needs to be
changed once to bring the entire system up to date.

At step 2 we strictly limit the command line presented by the user,
if we allow the user to change the command line at all.  Instead of
arbitrary parameters to the 'mount' command, for example, step 2 can
allow only a choice between '-r' and '-w', and '/dev/fd0' or '/dev/cdrom',
and all other inputs are invalid.  This also allows easier site-specific
customizations.

At step 3, we provide the actual functionality using a possibly
security-naive program.  This program must not do anything really stupid,
such as run an editor with its effective uid.

The actions performed in step 2 are written in a simple, safe, interpreted
language, designed to provide just enough functionality to check access
requests against access policies.  This should get rid of buffer-overrun
attacks as well.

The step 1 program could also obtain many of the privileges that setuid
programs need, drop any other privileges they have, and then skip directly
to step 3.  Some examples:

        - binding to privileged ports (call bind(), drop privileges, exec
                                       child; inndstart does this with innd)
        - getting I/O privileges (Linux SVGAlib was rumored to do this; a
                                  SVGAlib starter program would obtain I/O
                                  privileges for the video card, drop root,
                                  then exec the real SVGAlib program)
        - opening files (a mail delivery agent could be passed a file
                         descriptor opened O_RDWR for a mailbox; an 'at'
                         or 'cron' replacement could use this mechanism to
                         access only the invoking user's crontab file; step
                         2 would replace the 'at.allow' and 'at.deny' files)

--
Zygo Blaxell. Unix/soft/hardware guru, was for U of Waterloo CS Club, now for
(name withheld by request). 10th place, ACM Intl Collegiate Programming Contest
Finals, 1994.  Admin Linux/TCP/IP for food, clothing, anime.  Pager: 1 (613)
760 8572.  "I gave up $1000 to avoid working on windoze... *sigh*" - Amy Fong

home help back first fref pref prev next nref lref last post