Super User Do

1. Multi-user privilege escalation tool

This is the literate code documentation of SUD, including its sourcecode.

For instructions on how to install, build and contribute see github.com/dyne/sud.

Stable releases can be downloaded from files.dyne.org/sud

software by Dyne.org



This software aims to be a UNIX tool for generic secure usage when in need of privilege escalation, operated from a terminal command-line interface. It is designed to run SUID root (root owner, mode 4755) with "super-user powers" to execute things as root on the system it is installed. SUD purpose is security through awareness: it leverages all possible measures to avoid vulnerabilities, primarily the reduction of complexity in its own source-code and its documentation.

2. Code structure overview

The main source file is sud.c.

The overall structure of sud.c is simple:

{sud.c 2}
{Header files of system dependencies, 3}
{Macros and exit codes, 4}
{Global variables, 5}
{Reusable functions, 5}
{The main program, 6}

What follows is a journey across these sections.

3. Headers

We want to have as less requirements as possible, so this list should be kept short and eventually include #ifdef directives for specific platform targets.

{Header files of system dependencies 3}
#define _DEFAULT_SOURCE 1
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

Used in section 2

Above is a pretty standard selection of headers to provide basic functionalities of i/o, terminal output and error reporting.

{Header files of system dependencies 3} +=
#include <sys/stat.h>
#include <unistd.h>

Used in section 2

Above are the necessary headers for lstat which is the function used to check that the command being executed does not depends from a writable binary file.

{Header files of system dependencies 3} +=
#include <pwd.h>

Used in section 2

This header is necessary to be able to impersonate another user using the -u option: it gives us access to getpwnam which we use to query the password database (i.e. the local /etc/passwd file) and retrieve the uid of the desired user.

{Header files of system dependencies 3} +=
#include <grp.h>

Used in section 2

This is needed for getgroups which gives us a list of groups to which the user belongs.

{Header files of system dependencies 3} +=
#ifdef PAM_AUTH
#include <security/pam_appl.h>
#endif

Used in section 2

Optional LibPAM support for user authentication through system configured password modules.

{Header files of system dependencies 3} +=
#include "src/parg.h"

Used in section 2

This loads the parg library for command-line argument parsing.

{Header files of system dependencies 3} +=
#ifdef RELEASE
#include "stamp.h"
#endif

Used in section 2

At last the stamp.h is also included in case the RELEASE directive is defined at build time, which indicates the creation of a time stamp and of the checksum hash used to verify the integrity of SUD releases.

4. Preprocessor directives

In this section there are values that are configured at build time, meaning they are "hard-coded" inside the binary of SUD when it is built.

{Macros and exit codes 4}
#define VERSION "1.1.0"
#define OK 0                  // status code for successful run
#define usage_error 1         // status code for command not found

#define ERR(fstr,...) { fprintf(stderr,fstr, ##__VA_ARGS__); fputc('\n',stderr); }
#define XXX(fstr,...) { fprintf(stderr,fstr, ##__VA_ARGS__); fputc('\n',stderr); }
#define ACT(fstr,...) { fprintf(stdout,fstr, ##__VA_ARGS__); fputc('\n',stdout); }

// maximum length of a command path
#define PATH_MAX 1024

Used in section 2

5. Global variables

Variables whose scope is global to the execution of this program and visible to all its functions. Their names must be unique and should not be clashing with variables local to functions.

{Global variables 5}
#ifdef PAM_AUTH
static char *password_input = 0x0;
#endif

Used in section 2

{Reusable functions 5}
static short compare(char *left, char *right, size_t len) {
	if(!left || !right) return 0;
	register unsigned int i;
	for (i=0; i<len; i++) {
		if(!left[i] || !right[i]) return 0; // no null
		if (left[i] ^ right[i]) return 0; // xor for equality
	}
	// check null termination
	if(left[i] || right[i]) return(0);
	return(1); // return success
}

#ifdef PAM_AUTH
static int
pam_converse (int num_msg, const struct pam_message **msgs,
              struct pam_response **resp, void *data) {
    int i;
    struct pam_response* responses;
    (void) data;

    // safety
    if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
	    ERR("Internal PAM error: invalid num_msgs == %u",num_msg);
	    return PAM_CONV_ERR;
    }

    // allocate responses
    responses = calloc(num_msg, sizeof(struct pam_response)); // FIXME
    if (!responses) {
	    ERR("PAM error: %s","out of memory");
	    return PAM_BUF_ERR;
    }

    for (i = 0; i < num_msg; i++) {
	    const struct pam_message *msg = msgs[i];
	    struct pam_response* response = &(responses[i]);
	    char* style = NULL;
	    switch (msg->msg_style) {
	    case PAM_PROMPT_ECHO_OFF: style = "PAM_PROMPT_ECHO_OFF"; break;
	    case PAM_PROMPT_ECHO_ON: style = "PAM_PROMPT_ECHO_ON"; break;
	    case PAM_ERROR_MSG: style = "PAM_ERROR_MSG"; break;
	    case PAM_TEXT_INFO: style = "PAM_TEXT_INFO"; break;
	    default: ERR("Internal error: invalid msg_style: %d", msg->msg_style); break;
	    }
	    // XXX("conversation(): msg[%d], style %s, msg = \"%s\"", i, style, msg->msg);

	    switch (msg->msg_style) {
		case PAM_TEXT_INFO:
			ACT("[sud] %s",msg->msg);
			break;

	    case PAM_PROMPT_ECHO_OFF:
		    // reply with password
		    response->resp = password_input;
		    if (!response->resp)
			    return PAM_CONV_ERR;
		    break;

	    default:
		    ERR("Internal error: unknown message style: '%s'", style);
		    return PAM_CONV_ERR;
	    }
	    response->resp_retcode = 0;
    }

    *resp = responses;

    return PAM_SUCCESS;
}
#endif

Used in section 2

6. Main

Now we come to the general layout of the main() function, which is run every time SUD is executed with arguments. It is composed of 5 sections:

{The main program 6}
int main(int argc, char **argv)
{
	{Declare variables, 6}
    {Parse command-line options, 7}
    {Verify privileged access, 8}
    {Execute the command, 9}
    {Print any errors, 9}
}

Used in section 2

Below the first section declares variables that are used inside main:

{Declare variables 6}
     // full path to command
    char fullcmd[PATH_MAX] = {0x0};

     // verify target command executable
    struct stat st;

     // target privilege
    int target_uid=0;

     // cycling through groups to verify authorization
    int ngroups = 0;
    struct group* gr;

     // authorization flag
    short authorized = 0;

7. Parse command-line options

The next section parses commandline options using the included parg library.

{Parse command-line options 7}
     int c, optind;
     struct parg_state ps;
     parg_init(&ps);
     while ((c = parg_getopt(&ps, argc, argv, "hvu:")) != -1) {

Used in section 6

This following check is necessary to avoid parsing options if a command was already selected: we make it mandatory to place options before the command and its arguments. Once a command is recognized, then all following arguments belong to it.

{Parse command-line options 7} +=
	     if(fullcmd[0]) break;

Used in section 6

The case 1 switch is the one that parses the command to execute and its arguments.

A check is made to see that the command is included in the PATH environmental variable, unless it is specified as an absolute path to file.

This check is a bit sloppy and could be improved.

{Parse command-line options 7} +=
	     switch (c) {

	     case 1: // stop to parse options, save the command and parse its arguments
	     if(!fullcmd[0]) {
		     struct stat tst;
		     char file[PATH_MAX];
		     char *p, *path = getenv ("PATH");
		     if (path) // Check if command is found in $PATH
			     for (p = path; *p; p++) {
				     if (*p==':' && (p>path&&*(p-1)!='\\')) {
					     *p = 0;
					     snprintf (file, sizeof (file)-1, "%s/%s", path, ps.optarg);
					     if (!lstat (file, &tst)) {
						     // command found
						     snprintf(fullcmd,PATH_MAX,"%s",file);
						     optind = ps.optind-1;
					         break;
					     }
					     *p = ':';
					     path = p+1;
				     }
			     }
	     }
	     break;

Used in section 6

A few traditional options

{Parse command-line options 7} +=
	     case 'h':
	     help:
		     ACT("Usage: %s [-h] [-v] [-u USER] COMMAND",argv[0]);
		     return OK;
		     break;
	     case 'v':
		     ACT("Sud version %s", VERSION);
#ifdef RELEASE
		     ACT("%s %s",SHA512_SUD_C, VERSION);
		     ACT("built on %s",BUILD_TIME);
#endif
		     return OK;
		     break;
	     case '?':
		     ERR("unknown option -%c", ps.optopt);
		     return usage_error;
		     break;

Used in section 6

The option -u is followed by an argument and indicates a different user than root.

{Parse command-line options 7} +=
	     case 'u':
		     {
			     struct passwd *puid;
			     errno = 0;
			     puid = getpwnam(ps.optarg);
			     if(!puid && errno) ERR("error in %s: getpwnam",__func__);
			     if(puid) target_uid = puid->pw_uid;
		     }
			     break;
	     default:
		     ERR("error: unhandled option -%c", c);
		     return usage_error;
		     break;
	     }
     }
     if(argc==1) goto help;

Used in section 6

8. Authenticate

Here we are done parsing the command and selections and we start checking if the user is allowed to escalate privileges. There are various important checks done here, which is the section where code is most sensitive. From here onwards the golden rule should be: better safe than sorry!

Checks implemented are:

  1. the user is part of at least one group among: admin, wheel, sudo or sud.
  2. the command is an existing file that is executable and not writable to anyone
  3. optionally PAM authentication is used and users are prompted for their password
{Verify privileged access 8}

    // get number of groups first
    ngroups = getgroups(0, NULL);
    gid_t *groups = calloc(ngroups, sizeof(gid_t));
    // get the list of groups
    getgroups(ngroups, groups);
    for (int i = 0; i < ngroups; i++){
	    gr = getgrgid(groups[i]);
	    if(!gr) {
		    ERR("getgrgid error: %s",strerror(errno));
		    return usage_error;
	    }
	    if(compare(gr->gr_name,"admin",5)) authorized = 1; // OSX
	    if(compare(gr->gr_name,"wheel",5)) authorized = 1;
	    if(compare(gr->gr_name,"sudo",4)) authorized = 1;
	    if(compare(gr->gr_name,"sud",3)) authorized = 1;
    }
	free(groups);
    if(!authorized) {
	    ERR("[sud] user not authorized: %s (%s)",getlogin(),getenv("USER"));
	    return usage_error;
    }

    // command must exist as binary on the filesystem
       if (lstat (fullcmd, &st) == -1) {
	    ERR("cannot stat command: %s", fullcmd);
	    return usage_error;
    }
    if (st.st_mode & 0022) {
	    // command must have correct permissions and not be writable to anyone
	    ERR("cannot run a binary others can write: %s", fullcmd);
	    return usage_error;
    }

#ifdef PAM_AUTH
               // PAM
               struct pam_conv pam_conversation = { pam_converse, NULL };
               pam_handle_t* pamh;
               int res;
               res = pam_start("sud", getlogin(), &pam_conversation, &pamh);
               if(res != PAM_SUCCESS) {
	               ERR("PAM start failure: %s",pam_strerror(pamh, res));
	               return usage_error;
               }
               password_input = getpass("[sud] Password: ");
               res = pam_authenticate(pamh, 0);
               if(res != PAM_SUCCESS) {
	               ERR("PAM auth failure: %s",pam_strerror(pamh, res));
	               return usage_error;
               }
               res = pam_acct_mgmt(pamh, 0);
               if(res != PAM_SUCCESS) {
	               ERR("PAM account management failure: %s",pam_strerror(pamh, res));
	               return usage_error;
               }
               res = pam_setcred(pamh, PAM_ESTABLISH_CRED);
               if(res != PAM_SUCCESS) {
	               ERR("PAM credentials failure: %s",pam_strerror(pamh, res));
	               return usage_error;
               }
               res = pam_open_session(pamh, PAM_SILENT);
               if(res != PAM_SUCCESS) {
	               ERR("PAM session open failure: %s",pam_strerror(pamh, res));
	               return usage_error;
               }
               res = pam_close_session(pamh, PAM_SILENT);
               if(res != PAM_SUCCESS) {
	               ERR("PAM session close failure: %s",pam_strerror(pamh, res));
	               return usage_error;
               }
               res = pam_end(pamh, res);
               if(res != PAM_SUCCESS) {
	               ERR("PAM end failure: %s",pam_strerror(pamh, res));
	               return usage_error;
               }
               // ACT("PAM authentication: %s", "successful");
#endif

Used in section 6

9. Command execution

If the main function has kept executing until here, then it means the user is authorized. Then it proceeds calling setuid and seteuid to elevate the privileges of the running process. At last it uses execve to execute the command followed by its arguments.

{Execute the command 9}

       // privilege escalation
       if (setuid (target_uid) <0) {
	       ERR("setuid: %s",strerror(errno));
	       return usage_error;
       }
       if (seteuid (target_uid) <0) {
	       ERR("seteuid: %s",strerror(errno));
	       return usage_error;
       }

    // turn current process into the execution of command
    execve(fullcmd, &argv[optind], NULL); //&argv[ps.optind], NULL);

Used in section 6

Any error returned by the previous execve call is interpreted and printed to screen.

{Print any errors 9}
    // execv returns only on errors
    ERR("execv: %s", strerror(errno));

Used in section 6

That's all folks!