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
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.
The main source file is sud.c
.
The overall structure of sud.c
is simple:
ndex.html/g {Header files of system dependencies, 3} ndex.html/g {Macros and exit codes, 4} ndex.html/g {Reusable functions, 5} ndex.html/g {The main program, 6}
What follows is a journey across these sections.
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.
#define _DEFAULT_SOURCE 1 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h>ndex.html/g
Used in section 2
Above is a pretty standard selection of headers to provide basic functionalities of i/o, terminal output and error reporting.
#include <sys/stat.h> #include <unistd.h>ndex.html/g
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.
#include <pwd.h>ndex.html/g
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.
#include <grp.h>ndex.html/g
Used in section 2
This is needed for getgroups which gives us a list of groups to which the user belongs.
#include "src/parg.h"ndex.html/g
Used in section 2
This loads the parg library for command-line argument parsing.
#ifdef RELEASE #include "stamp.h" #endifndex.html/g
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.
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.
#define VERSION "1.2.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 1024ndex.html/g
Used in section 2
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.
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 }ndex.html/g
Used in section 2
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:
int main(int argc, char **argv) { ndex.html/g {Declare variables, 6} ndex.html/g {Parse command-line options, 7} ndex.html/g {Verify privileged access, 8} ndex.html/g {Execute the command, 9} ndex.html/g {Print any errors, 9} }ndex.html/g
Used in section 2
Below the first section declares variables that are used inside main:
// 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;
The next section parses commandline options using the included parg
library.
int c, optind; struct parg_state ps; parg_init(&ps); while ((c = parg_getopt(&ps, argc, argv, "hvu:")) != -1) {ndex.html/g
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.
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.
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;ndex.html/g
Used in section 6
A few traditional options
-h
prints out help
-v
prints out the version and binary hash checksum
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;ndex.html/g
Used in section 6
The option -u
is followed by an argument and indicates a different user than root.
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;ndex.html/g
Used in section 6
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:
admin
, wheel
, sudo
or sud
.
// get number of groups first ngroups = getgroups(0, NULL); gid_t *groups = calloc(ngroups, sizeof(gid_t)); // get the list of groups, here we intend to ignore the result #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-result" getgroups(ngroups, groups); #pragma GCC diagnostic pop 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; }ndex.html/g
Used in section 6
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.
// 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);ndex.html/g
Used in section 6
Any error returned by the previous execve
call is interpreted and printed to screen.
// execv returns only on errors ERR("execv: %s", strerror(errno));ndex.html/g
Used in section 6
That's all folks!