Generic wrapper

Joe Zbiciak (jzbiciak@DALDD.SC.TI.COM)
Mon, 26 May 1997 22:03:27 -0500

All,

Since there are a plethora of buffer overflows waiting to happen, and
since the AUSCERT wrapper isn't sufficient for many people, I'm making
my more generic wrapper available to all.

The following URL includes the source and a short description of the
wrapper:

http://cegt201.bradley.edu/~im14u2c/wrapper/

A quick description of the wrapper's functionality:

-- Filters argv/envp, looking for excessive length arguments and
environment variable. Allowed length is tunable on a per-variable
basis for environement variables. There's a single upper limit
for argument lengths.

-- Remaps/disallows "dangerous" characters in argv/envp. You can
configure a set of remappings, and a set of disallowed characters.
Additionally, you can set a threshold of remapped characters, in
case you're only moderately paranoid. :-)

-- Validates USER/LOGNAME environement variables against the username
returned by getpwuid().

-- Passes only prechosen environment variables.

-- Presets other environment variables to specific values.

-- Logs exploit attempts/suspicious behavior to syslog().

In contrast, the AUSCERT wrapper only checks argument lengths and
logs the error to syslog().

Incidentally, the subroutines in this code are small enough that
compiling with "gcc -O6 -fomit-frame-pointer -finline-functions" will
generally flatten the call graph completely, making it pretty resistant
to any overflows itself (not that any should be possible within the
wrapper, but I don't blindly trust my libc!). All of the functions
are declared "static inline".

Most of the functionality of the wrapper is configurable, so you can
disable those portions which do not interest/pertain to you.

One other note: Thanks to "*Hobbit*" (hobbit@avian.org) for the idea
which led to the "safe_str_scan()" functionality. He sent me a
preliminary version of his own wrapper, which I borrowed that idea from.

For those of you without WWW access, I'm including a copy of the source
here:

---wrapper.c---

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

#include <stdio.h>
#include <syslog.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>
#include <pwd.h>
#include <stdlib.h>
#include <ctype.h>

char rcsid[]="$Id: wrapper.c,v 1.5 1997/05/22 04:40:47 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 */
/* Paths */
#define WRAPPED "./test_wrap" /* Set to full path of wrapped executable */
#define REALBIN WRAPPED"_" /* Usually can be left untouched. */

/* Wrapper behavior */
#define SYSLOG 1 /* Enable/disable SYSLOGging */
#define LOG_UIDS 1 /* Enable/disable recording uid w/syslog */
#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() */
#define PARANOID_USER 1 /* Verify USER/LOGNAME against uid */

/* Wrapped program characteristics */
#define MAX_ARG (32) /* Maximum argv parameter length. */
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 */
int has_username; /* Check this variable against username */
} 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 */
{
{ "ROWS=", 5, 4, 0 },
{ "COLUMNS=", 8, 4, 0 },
{ "LC_CTYPE=", 9, 64, 0 },
{ "LC_MESSAGES=", 11, 64, 0 },
{ "LC_TIME=", 8, 64, 0 },
{ "LOGNAME=", 8, 16, 1 },
{ "TERM=", 5, 16, 0 },
{ "USER=", 5, 16, 1 },
};
#define NUM_ALLOWED_ENV (sizeof(allowed_env)/sizeof(TEnvInfo))

char * preset_env [] = /* Environ. vars we force the program to see */
{
"PATH=/bin:/usr/bin:/usr/ucb:/usr/local/bin",
};
#define NUM_PRESET_ENV (sizeof(preset_env)/sizeof(char *))

/* Set an entry to what a char maps to, if allowed, or 0 if it's not. */
/* This table pretty well assumes an ASCII representation is being used. */
unsigned char allowed_char [] =
{
/* Default table disallows all but a handful of certain "well known" */
/* ASCII control characters. Otherwise, only alpha, numeric, and */
/* punctuation characters are allowed through. By default, disallowed */
/* characters are mapped to the character defined by "NA". If a */
/* character is marked with a zero, it causes the wrapper to abort. */
/* If a character is marked with NA, it's merely remapped. To disable */
/* remapping entirely (causing any disallowed char to abort), define NA */
/* to be 0. The variable MAX_REMAP is used by the main body of the */
/* program to control how many remapped characters you'll permit per */
/* each string. (environment variable, argument, etc.) */

#define NA '_'
#define MAX_REMAP (32)

/* Control characters: */
/* Permits BEL, BS, TAB, NL, VF, CR, ESC, and remaps the rest. */
/* ^@ -- ^G ( 0 -- 7) */ 0, NA, NA, NA, NA, NA, NA,'\a',
/* ^H -- ^O ( 8 -- 15) */ '\b','\t','\n', NA,'\v','\r', NA, NA,
/* ^P -- ^W ( 16 -- 23) */ NA, NA, NA, NA, NA, NA, NA, NA,
/* ^X -- ^_ ( 24 -- 31) */ NA, NA, NA, 27, NA, NA, NA, NA,

/* Alphabetics, numerics, and punctuation: */
/* Permits all of them by default, except for the DEL character (127) */
/* ( 32 -- 39) */ ' ', '!', '"', '#', '$', '%', '&','\'',
/* ( 40 -- 47) */ '(', ')', '*', '+', ',', '-', '.', '/',
/* ( 48 -- 55) */ '0', '1', '2', '3', '4', '5', '6', '7',
/* ( 56 -- 63) */ '8', '9', ':', ';', '<', '=', '>', '?',
/* ( 64 -- 71) */ '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
/* ( 72 -- 79) */ 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
/* ( 80 -- 87) */ 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
/* ( 88 -- 95) */ 'X', 'Y', 'Z', '[','\\', ']', '^', '_',
/* ( 96 -- 103) */ '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
/* (104 -- 111) */ 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
/* (112 -- 119) */ 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
/* (120 -- 127) */ 'x', 'y', 'z', '{', '|', '}', '~', NA,

/* Extended ASCII: Flatly disallowed. */
/* (128 -- 143) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* (144 -- 159) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* (160 -- 175) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* (176 -- 191) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* (192 -- 207) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* (208 -- 223) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* (224 -- 239) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* (240 -- 255) */ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};

/* Internal wrapper use only -- shouldn't need to adjust, generally */
#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

#if SYSLOG && !defined(LOG_UIDS)
#error Define "LOG_UIDS" to be either 1 or 0 explicitly
#endif

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

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

static uid_t uid,euid;
static gid_t gid,egid;

/* Find the last whitespace character before 'len' -- for word-wrapping */
static inline
int word_wrap(char * s, int len)
{
int i,j;

for (i=j=0; *s && i<len; s++,i++)
if (isspace(*s)) j=i;

if (j==0 || !*s) j=i-1;

return j;
}

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

l=strlen(s);

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

do {
m=word_wrap(s,MAX_LOG-2)+1;
strncpy(buf,s,m);
buf[m]=0;
#if SYSLOG==2
/* debug syslog output, rather than actually syslogging */
printf("syslog: %s\n",buf);
#else
syslog (PRIORITY, buf);
#endif
l-=m;
if (l>0) s+=m;
} while (l>0);

#if LOG_UIDS
/*
As a matter of paranoia, also log uid,gid,euid,egid.
We log the euid/egid as well, in case user somehow
managed to modify one or both aside from what our perms
are set to.
*/
#if SYSLOG==2
printf ("syslog: uid=%.5d gid=%.5d euid=%.5d egid=%.5d\n",
(int)uid,(int)gid,(int)euid,(int)egid);
#else
syslog (PRIORITY,"uid=%.5d gid=%.5d euid=%.5d egid=%.5d",
(int)uid,(int)gid,(int)euid,(int)egid);
#endif
#endif

closelog();
#endif

exit(1);
}

/*
Scan a string, ensuring its contents are completely safe. Remaps
characters according to the "allowed_char[]" array.

Places the length of the string in *length. The function returns
the following:

0 if the string was not modified
-1 if the string contained illegal characters
-2 if the string was too long

otherwise it returns the number of remapped characters (in case you
want to apply a "threshold" on the number of remapped characters you
allow).
*/
static inline
int safe_str_scan(unsigned char * string, int max_length, int *length)
{
int i, remap_flag=0;
unsigned char * s=string;
unsigned char remap;

for (i=0;*s && i<max_length;i++,s++)
{
remap=allowed_char[(*s)&0xFF];
if (!remap)
{
*s=0; /* truncate bogus string right here */
*length=i+strlen(s);
return -1;
}
if (remap!=*s)
{
remap_flag=1;
*s=remap;
}
}
/* determine if string was too long */
if (*s)
{
*length=i+strlen(s);
return -2;
}

*length=i;
return remap_flag;
}

#if PARANOID_USER
static char *username=NULL;

/*
Try to look up this user's login name by his uid.

This routine is broken for sites with multiple users sharing the
same uid. Fix your site if you want to use the PARANOID_USER
function. :-)
*/
static inline
char * determine_username(void)
{
static struct passwd *pw=NULL;

if (!pw)
pw=getpwuid(uid);

if (!pw)
username=NULL; /* uhoh -- not found! */
else
username=pw->pw_name;

return username;
}
#endif

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

/* Set our uid/gid variables */
uid = getuid ();
gid = getgid ();
euid= geteuid();
egid= getegid();

#if PARANOID_USER
/* Look up username, so we can compare against USER=/LOGNAME= */
if (!determine_username())
{
printf("Who are you?\nError: Aborting!\n"
"No entry in password file for uid=%d\n",(int)uid);
/* Safe since uid/gid etc. are max 5 chars apiece */
sprintf(buf,
"Invalid user (no password entry)");
log(buf);
exit(1); /* Safety net */
}
#endif

/* Check all of argv. Log and exit if any args have length > MAX_ARG */
for (i=1;i<argc && argv[i]!=0;i++)
{
check=safe_str_scan(argv[i],MAX_ARG,&length);
if (check<0 || check>MAX_REMAP)
{
printf("Error: Aborting!\n%s: '%s'\n",
check==-2?"Excessive argument length":
check==-1||
check>0?"Invalid characters present in argument":
"Internal error processing argument",
argv[i]);

/* Safe since uid/gid etc. are max 5 chars apiece */
sprintf(buf,
"Possible overrun attempt (argv len=%.5d err=%.2d) ",
length, check);

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)
{
check=safe_str_scan(envp[i],
allowed_env[k].max_len+
allowed_env[k].name_len,&length);
if (check<0 || check>MAX_REMAP)
{
printf("Error: Aborting!\n%s: '%s'\n",
check==-2?"Excessive environment variable length":
check==-1||
check>0?"Invalid characters present in environment":
"Internal error processing environment",
envp[i]);

/* Safe because we have control over allowed_env[] */
sprintf(buf,
"Possible overrun attempt (env '%.32s' len=%.5d err=%.2d):",
envp[i], length, check);

log(buf);
exit(1); /* safety net */
}
#if PARANOID_USER
/* Check variable value against username we looked up */
if (uid!=0 && allowed_env[k].has_username &&
strcmp(username,envp[i]+allowed_env[k].name_len)!=0)
{
printf("Error: Aborting!\n"
"Environment variable '%s' doesn't match username\n",
envp[i]);
/* Safe because we control allowed_env[] */
sprintf(buf,
"Variable didn't match username (env '%.32s'): ",
envp[i]);
log(buf);
exit(1); /* safety net */
}
#endif
envp[j++]=envp[i];
}
if (j>NUM_ALLOWED_ENV)
{
/* This should only happen if we somehow had duplicate copies of */
/* the same allowed environment variable passed to us, which */
/* shouldn't happen. */
log("Internal error to wrapper: too many allowed env vars found");
exit(1); /* safety net */
}
}
for (i=0;i<NUM_PRESET_ENV;i++,j++)
{
envp[j]=preset_env[i];
}
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("REALBIN") failed! errno=%.5d\n",errno);
perror("execve("REALBIN") failed");
log(buf);

exit(1); /* safety net */

}

/*
$Log: wrapper.c,v $
* Revision 1.5 1997/05/22 04:40:47 jzbiciak
* Skip username test if uid==0 (root) since root goes by many names
* on many systems.
*
* Revision 1.4 1997/05/22 04:30:45 jzbiciak
* Added "default environment variables"
* Added checking of USER, LOGNAME, and potentially other env.
* variables against user-id, if PARANOID_USER==1
* Moved code for printing uid/gid/euid/egid into log() function
* so it's more uniform. Can be configured in/out with LOG_UIDS
*
* Revision 1.3 1997/05/21 23:16:54 jzbiciak
* make message more similar between environment and argument errors.
*
* Revision 1.2 1997/05/21 23:05:18 jzbiciak
* Added scanning/remapping invalid characters, including a thresholding
* feature (just in case someone had a stray escape character laying around,
* we won't throw up the red flag straight away.)
*
* Also, added some comments to further clarify the code.
*
* Added wordwrap and debugging feature to syslogging code.
*
*/

---end of file---

And here's a simple test program which dumps the argv/envp passed to it:

---test_wrap.c---

main(int argc, char * argv[], char *envp[])
{
int i;

for (i=0; argv[i]; i++)
printf(" argv[%d] = '%s'\n",i,argv[i]);
for (i=0; envp[i]; i++)
printf(" envp[%d] = '%s'\n",i,envp[i]);

return 0;
}
---end of file---

Enjoy!

--Joe

--
 +--------------Joseph Zbiciak--------------+
 |- - - - jzbiciak@daldd.sc.ti.com - - - - -|
 | - - http://ee1.bradley.edu/~im14u2c/ - - |      Not your average "Joe."
 |- - - - Texas Instruments,  Dallas - - - -|
 +-------#include <std_disclaimer.h>--------+