
/*****************************************************************/
/* Generic wrapper to prevent exploitation of suid/sgid programs */
/* J. Zbiciak                                                    */
/*****************************************************************/

#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 2.0 1997/06/16 04:36:07 jzbiciak Exp $";

#include "wrapper.h"  /* User configurable area. */

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


/*------------------------------------------------------------------------*/
/* Global Variables                                                       */
/*------------------------------------------------------------------------*/
#if ENV_CHKUSER
static char *username=NULL;  /* Username of caller       */
#endif
static char *basename=NULL;  /* Basename of this program */
static char *fullpath=NULL;  /* Full path of wrapped prg */
static uid_t uid,euid;       /* UID/EUID of caller       */
static gid_t gid,egid;       /* GID/EGID of caller       */


/*------------------------------------------------------------------------*/
/* 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 char **argv0;
static inline
void log(char * s)
{
#if SYSLOG
    static char *logident=LOGIDENT;
    char buf[MAX_LOG];
    int l,m;
	
  
    /* Open up syslog; */
    openlog(LOGIDENT,LOG_PID,FACILITY);

    argv0=&logident; /*just in case some bozotic syslog() call groks argv[0]*/

    /* NOTE:  This assumes "s" is the malloc'd buffer we allocate in main() */
    if (s[MSG_LEN]!=0)
    {
	syslog(PRIORITY,"Uh oh -- syslog message buffer compromised!\n");
        syslog(PRIORITY,"uid=%.5d gid=%.5d euid=%.5d egid=%.5d",
                         (int)uid,(int)gid,(int)euid,(int)egid); 
        exit(1);
    }

    /* Whew -- if we get here, then things aren't so bad (we hope). */
    l=strlen(s);

    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 chars or remapped >MAX_REMAP
 *      -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_count=0;
    unsigned char * s=string;
    unsigned char remap;        

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

    *length=i;

    if (remap_count>MAX_REMAP)
    {
        /* too many remapped characters. */
        return -1; 
    }

    return remap_count;
}


/*------------------------------------------------------------------------*/
/*
 * Extract the base name of a program from a path specifier.  
 *
 * The path name is assumed to already be checked by "safe_str_scan".  It
 * is first scanned for the last '/' in the string (if any).  The
 * character following this slash (or the first character if none are
 * found) defines the start of the base name.  Next, the length of the
 * base name is checked against the defined maximum and zero.  Once
 * successfully passing these tests, it is crossreferenced against our
 * list of wrapped programs.
 *
 * Based on the results of the above procedures, extract_basename returns 
 * the following:
 *
 *       0 if the operation was successful
 *      -1 if the base name was empty
 *      -2 if the base name was too long/short
 *      -3 if the base name was not found or marked as invalid in our list
 *
 * The global variables "basename" and "fullpath" are set to reflect the
 * appropriate values upon success, or are reset to NULL on failure.  If
 * the basename was successfully extracted but not matched in the list, we
 * *will* set "basename", but will leave "fullpath" set to NULL, for error
 * reporting reasons.
 * 
 */
/*------------------------------------------------------------------------*/

static inline
int extract_basename(char *arg0, int max_base, int *length)
{
    char *bn,*s;
    int check;
    int i;

    /* In case we fail, make sure these are unset. */
    basename=fullpath=NULL;

    /* In case we find no '/', start our basename at the first char */
    bn=arg0;

    /* Now, trace all of arg0, looking for '/'s. */

    for (s=arg0; *s; s++)
        if (*s=='/') bn=s+1;

    if (*bn==0) /* null string! */
        return -1;

    /* We successfully extracted basename -- set it */
    basename=bn;   /* for error reporting purposes. */

    if ((check=safe_str_scan(bn,max_base,length))<0)
    {
        /* Note, safe_str_scan should only return -2 in this case, if
           safe_str_scan was called before on argv[0]. */
        return check;
    }

    /* Next, look up this basename in our database. */
    for (i=0;  wrap_profile[i].base_name; i++)
        if (!strncmp(bn,wrap_profile[i].base_name,max_base))
            break;

    /* When we get to here, we've either matched something, or hit
       the wildcard at the end of the list. */

    if (wrap_profile[i].full_path==NULL)
    {
        return -3;   /* Invalid program name */
    }

    /* If we make it here, we've matched something valid. */

    basename=bn;
    fullpath=wrap_profile[i].full_path;

    return 0;
}



#if ENV_CHKUSER

/* 
   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 ENV_CHKUSER
   function.  :-)

   Note:  According to *Hobbit*, some getpw*() functions might grok
   the environment to determine where /etc/passwd is, etc.  This is
   inherently broken if they do.  How do /bin/passwd and /bin/login
   work securely on such systems?!  RTFM if you're concerned if your
   system behaves in this manner.
 */

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,match;
    int check;
    int length;
    char *buf;
    char *var;

    /* The following statements exist solely to suppress 'gcc -Wall'      */
    /* warning messages that don't mean anything useful.                  */
    buf=rcsid;
    buf=hdr_rcsid;
    #ifdef STATIC_CFGTBL
    buf=cfgtbl[0];
    #endif

    /* Allocate a fairly sizable buffer for messages off the global heap. */
    /* Such buffers are difficult to exploit even if there is a buffer    */
    /* overflow problem.  I've taken great pains to ensure there aren't   */
    /* any in this wrapper, however.                                      */

    buf=(char*) malloc (MSG_LEN*2);  /* 2x allows some extra padding */

    /* To double check the absense of an overrun in our output buffer, we */
    /* place a 0 at position buf[MSG_LEN].  If this is overwritten, we    */
    /* will not print the contents of buf[], since there may be an exploit*/
    /* somehow.  Instead, we just print a canned error message and hope   */
    /* for the best.                                                      */
    buf[MSG_LEN]=0;


    /* The log() routine replaces argv[0] with our fixed version defined  */
    /* the wrapper configuration header.  It does so through this var.    */
    argv0=&(argv[0]); 

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

    /* Check/filter argv */
    for (i=0; i<argc; i++)
    {
        if ((check=safe_str_scan(argv[i], 
                                    i?ARG_MAXLEN:ARG0_MAXLEN, 
                                    &length))<0)
        {
            printf("Error: Aborting!\n%s: %-.64s\n", 
                check==-2?"Excessive argument length":
                check==-1?"Invalid characters present in argument":
                          "Internal error processing argument",
                argv[i]);

            sprintf(buf,   /* safe */
                    "Possible overrun attempt (arg[%.2d] len=%.5d err=%.2d) ",
                    i, length, check);

            log(buf);
            exit(1);  /* safety net */
        }
    }

    /* Check basename */
    if ((check=extract_basename(argv[0],ARG0_BASELEN,&length))<0)
    {
        printf("Error: Aborting!\n%s: %-.64s\n",
                check==-3?"Invalid program name":
                check==-2?"Excessive program name length":
		check==-1?"Null program name":
                          "Internal error processing program name",
                basename?basename:"(null)");
    
        sprintf(buf,    /* safe */
                "Possible exploit attempt "
		"(base len=%.5d err=%.2d) basename=%.32s ",
                length, check, basename?basename:"(null)");
        
        log(buf);   
        exit(1);    /* safety net */
    }

    /* Check/filter environment */

    for (i=j=0;envp[i];i++)
    {
#if ENV_PASSTHRU
        var=envp[i];
#else
        var=NULL;
#endif

        /* Look for envp[i] in env_profile[] */
        for (match=k=0;env_profile[k].env!=NULL;k++)
	{
            if (0!=(match=!strncmp(envp[i],
				   env_profile[k].env,
				   env_profile[k].name_len)))
		break;
	}

        if (match) 
        {
	    env_profile[k].matched=1; /* notate that we've matched this one */
            if (env_profile[k].allow)
            {
                if ((check=safe_str_scan(envp[i]+env_profile[k].name_len,
                                         env_profile[k].max_len,
                                         &length))<0)
                {
                    printf("Error: Aborting!\n%s: %-.64s\n", 
                        check==-2?"Excessive environment var length":
                        check==-1?"Invalid characters present in environment":
                                  "Internal error processing environment",
                        envp[i]);

                    sprintf(buf,   /* safe -- we control env_profile[] */
                        "Possible overrun attempt "
                        "(envp[%.*s] len=%.5d err=%.2d) ",
			env_profile[k].name_len-1,
                        env_profile[k].env, length, check);

                    log(buf);
                    exit(1); /* safety net */
                }   

                var=envp[i];
            } else
	    {
		match=0; /* flag this so that the PASSTHRU check will grab it */
	    }

            if (env_profile[k].deny)
                var=NULL;

            if (env_profile[k].preset && !var)
		/* mark as not matched, we'll set it later */
		env_profile[k].matched=0;  
        }
#if ENV_PASSTHRU && ENV_CHKPASS
        if (!match && var && (check=safe_str_scan(var,ENV_MAXPASS,&length))<0)
	{
	
	    printf("Error: Aborting!\n%s: %-.64s\n", 
		check==-2?"Excessive environment var length":
		check==-1?"Invalid characters present in environment":
			  "Internal error processing environment",
		envp[i]);

	    sprintf(buf,   /* safe -- we control env_profile[] */
		"Possible overrun attempt "
		"(envp[passthru] len=%.5d err=%.2d) ",
		length, check);

	    log(buf);
	    exit(1); /* safety net */
	}
#endif

        
        if (var)
            envp[j++]=var;
    }
    envp[j]=0;
            

#if ENV_CHKUSER
    /* Check USER/LOGNAME-type variables. */

    /* 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);
        log("Invalid user (no password entry)");
        exit(1);  /* Safety net */
    }

    for (i=0;envp[i];i++)
    {
        /* Look for envp[i] in env_profile[] */
        for (match=k=0;env_profile[k].env!=NULL;k++)
	{
            if (0!=(match=!strncmp(envp[i],
				   env_profile[k].env,
				   env_profile[k].name_len)))
		break;
	}

        if (!match || !env_profile[k].has_username) 
            continue;

        /* Check variable value against username we looked up */
        if (uid!=0 && 
            strcmp(username,envp[i]+env_profile[k].name_len)!=0)
        {
            printf("Error: Aborting!\n"
                    "Environment variable %-.64s doesn't match username\n",
                    envp[i]);
        
            sprintf(buf, /* Safe -- we limit string length */
                    "Variable didn't match username (env '%.32s'): ",
                    envp[i]);

            log(buf);
            exit(1);  /* safety net */
        }
    }
#endif

    /* Finally, append any "preset" variables that weren't matched */
    for (k=0;env_profile[k].env!=NULL;k++)
    {
	if (env_profile[k].preset && !env_profile[k].matched)
	    envp[j++]=env_profile[k].val;
	
    }
    envp[j]=0;

    if (!basename || !fullpath)
    {
        printf("Internal wrapper error.  Aborting.\n");
        log("Internal wrapper error -- basename or fullpath unset.\n");
        exit(1); /* safety net */
    }

    argv[0]=basename;
    execve(fullpath,argv,envp);

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

    exit(1); /* safety net */

}

/* 
 * $Log: wrapper.c,v $
 * Revision 2.0  1997/06/16 04:36:07  jzbiciak
 * Major update to wrapper.  Moved config information to wrapper.h,
 * restructured environment variable analysis, added wrapped-program
 * configuration table, and did some general bugfixing/cleaning up.
 *
 * 
 * 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 8-bit 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.
 *
 */
