sup.c

sup 1.2

(c) 2016 Dyne.org Foundation, Amsterdam

Written by:

License (LGPLv3)

This source code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.

This source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Please refer to the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with this source code; if not, write to: Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

Headers

#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <libgen.h>
#include <pwd.h>

struct rule_t {
    int uid;
    int gid;
    const char *cmd;
    const char *path;
    const char *hash;
};

#include "config.h"

#ifdef HASH
#include "sha256.h"
#endif

Help

static const char *HEADER = "sup " VERSION " - small and beautiful superuser tool\n";

static const char *COPYLEFT =
    "copyright (C) 2016 dyne.org foundation, license GNU GPL v3+\n"
    "this is free software: you are free to change and redistribute it\n"
    "for the latest sourcecode go to <https://git.devuan.org/jaromil/sup>\n";

static const char *LICENSE =
    "this source code is distributed in the hope that it will be useful,\n"
    "but without any warranty; without even the implied warranty of\n"
    "merchantability or fitness for a particular purpose.\n"
    "when in need please refer to <http://dyne.org/support>.\n";


static const char *HELP =
    "Syntax: sup [options] command [arguments...]\n"
    "\n"
    "Options:\n"
    " -l     list compiled-in authorizations and flags\n"
    " -u     set uid to this user name\n"
    " -g     set gid to this group name\n"
    " -d     fork command as background process (daemon)\n"
    " -p     saves pid of background process to file (daemon)\n"
    "\n"
    "Please report bugs to <https://git.devuan.org/jaromil/sup/issues>\n";

maximum length of a command string

#define MAXCMD 512

maximum length of a command full path string

#define MAXFILEPATH 4096

Always return 1 on error, conforming to standard shell checks. Reason of error is described by stderr text before colon, extended reason can be provided or falls back to errno.

static int error(const char *code, const char *reason) {
    fprintf (stderr, "%s: %s\n",
             code? code : "",
             reason? reason : strerror (errno));
    exit(1);
}

Check if binary is found in $PATH

static char *getpath(const char *str) {
    struct stat st;
    static char file[MAXFILEPATH];
    char *p, *path = getenv ("PATH");
    if (path)
        for (p = path; *p; p++) {
            if (*p==':' && (p>path&&*(p-1)!='\\')) {
                *p = 0;
                snprintf (file, sizeof (file)-1, "%s/%s", path, str);
                if (!lstat (file, &st))
                    return file;
                *p = ':';
                path = p+1;
            }
        }
    return NULL;
}

Compute SHA256 hash of binary

#ifdef HASH

#define CHUNK 1048576 // 1MiB
static uint32 getsha(const char *path, unsigned char *dest) {
    static sha256_context sha;

    unsigned char buf[CHUNK]; // 1 MiB
    uint32 len, tot;
    FILE *fd;

    fd = fopen(path,"r");
    if(!fd) error("fopen", "cannot read binary file");

    sha256_starts(&sha);
    clearerr(fd);
    len=0;
    tot=0;

    do {

read chunk of data in binary file

        len = fread(buf, 1, CHUNK, fd);

stop if NULL read

        if(!len) break;
        tot+=len;

stop if EOF reached

        if(len<CHUNK) break;

compute sha256 of chunk

        sha256_update(&sha, buf, len);

    } while(len>0);

check file descriptor for errors

    if(ferror(fd)) {
        fclose(fd);
        error("fread", "error reading binary file");
    }
    fclose(fd);

compute last chunk

    if(len>0) sha256_update(&sha, buf, len);

finish and save result in *dest

    sha256_finish(&sha, dest);

    return(tot);
}

#endif

Main()

int main(int argc, char **argv) {

    static char fullcmd[MAXCMD];
    static char *cmd;

    struct passwd *pw;
    struct stat st;

    static int i, uid, gid;
    static int target_uid=0;
    static int target_gid=0;

#ifdef HASH
    unsigned char digest[32];
    char output[65];
#endif

#ifdef DAEMON
    int fork_daemon = 0;
    char pidfile[MAXFILEPATH] = "";
#endif

parse commandline options

    int opt;
    while((opt = getopt(argc, argv, "+hvdlu:g:p:")) != -1) {

        switch(opt) {

#ifdef DAEMON
        case 'p':
            snprintf(pidfile,MAXFILEPATH,"%s",optarg);
            break;
#endif

        case 'u':
        {
            struct passwd *puid;
            errno=0;
            puid=getpwnam(optarg);
            if(!puid && errno) error("uid_getpwnam",NULL);
            if(puid) target_uid=puid->pw_uid;
        }
        break;

        case 'g':
        {
            struct passwd *pgid;
            errno=0;
            pgid=getpwnam(optarg);
            if(!pgid && errno) error("gid_getpwnam",NULL);
            if(pgid) target_gid=pgid->pw_gid;
        }
        break;

        case 'h':
            fprintf(stdout, "%s\n%s\n%s", HEADER, COPYLEFT, HELP);
            exit (0);

        case 'v':
            fprintf(stdout, "%s\n%s\n%s", HEADER, COPYLEFT, LICENSE);

            exit (0);

#ifdef DAEMON
        case 'd':
            fork_daemon=1;
            break;
#endif

        case 'l':
            fprintf(stdout,"%s\n%s\nList of compiled in authorizations:\n\n", HEADER, COPYLEFT);
            fprintf(stdout,"User\tUID\tGID\t%s\t\t%s\n",
                    "Command","Forced PATH");
            for (i = 0; rules[i].cmd != NULL; i++) {

Using ‘getpwuid’ in statically linked applications requires at runtime the shared libraries from the glibc version used for linking. But not in case of musl-libc.

                pw = getpwuid( rules[i].uid );
                fprintf (stdout, "%s\t%d\t%d\t%s\t%s\n",
                         pw?pw->pw_name:"", rules[i].uid, rules[i].gid,
                         rules[i].cmd, rules[i].path);
#ifdef HASH
                fprintf(stdout, "sha256: %s\n\n",rules[i].hash);
#endif
            }
            fprintf(stdout,"\nFlags: %s %s %s %s\n",
#ifdef HASH
                    HASH?"HASH":"",
#else
                    "",
#endif
#ifdef DAEMON
                    DAEMON?"DAEMON":"",
#else
                    "",
#endif
                    strlen(CHROOT)?"CHROOT":"",
                    strlen(CHRDIR)?"CHRDIR":"");
            exit (0);

        } // switch(opt)

    } // getopt

get the called UID and GID

    uid = getuid ();
    gid = getgid ();

copy the execv argument locally

    snprintf(fullcmd,MAXCMD,"%s",argv[optind]);

save a pointer to basename string in cmd

    cmd = basename(fullcmd);

get the username string from /etc/passwd

    pw = getpwuid( uid );
#ifdef DEBUG

one could maintain a log of calls here

    fprintf(stderr,"sup %s called by %s(%d) gid(%d)\n",
            cmd, pw?pw->pw_name:"", uid, gid);
#endif

Check that all rules match

loop over each rule

    for (i = 0; rules[i].cmd != NULL; i++) {

command is * or locked path matches

        if (*rules[i].cmd == '*' || !strcmp (cmd, rules[i].cmd)) {

if path is locked

            if (*rules[i].path != '*') {

and if path is specified

                if((fullcmd[0]=='.')||(fullcmd[0]=='/')) {

then check that path matches

                    if( strcmp(rules[i].path,fullcmd) )
                        return error("path","path not matching");

or if path is not specified

                } else {

get the default path with our getpath()

                    snprintf(fullcmd,MAXCMD,"%s",getpath(cmd));

check if the default environment path matches

                    if( strcmp(rules[i].path,fullcmd) )
                        return error("path","path not matching");
                }

or if path is not locked

            } else

and if path is not specified, getpath()

                if((fullcmd[0]!='.')&&(fullcmd[0]!='/'))
                    snprintf(fullcmd,MAXCMD,"%s",getpath(cmd));

#ifdef DEBUG
            fprintf(stderr,"path check passed\n");
            fprintf(stderr,"fullcmd: %s\n",fullcmd);
            fprintf(stderr,"cmd: %s\n",cmd);
#endif

Command binary check

command does not exist as binary on the filesystem

            if (lstat (fullcmd, &st) == -1)
                return error("lstat", "cannot stat program");

            if (st.st_mode & 0022)

command has wrong permissions (writable to others)

                return error("perm", "cannot run binaries others can write.");

user UID is not root

            if (uid != SETUID

and is not unlocked

                && rules[i].uid != -1

and is not the locked UID

                && rules[i].uid != uid)
                return error("uid", "user does not match");

user GID is not root

            if (gid != SETGID

and is not unlocked

                && rules[i].gid != -1

and is not the locked GID

                && rules[i].gid != gid)
                return error("gid", "group id does not match");


#ifdef HASH

Binary hash checksum

            if( strlen(rules[i].hash) ) {
                int c;
                uint32 sizeread;

                sizeread = getsha(fullcmd, digest);
                if(sizeread != st.st_size)
                    error("getsha", "binary file size differs from size read");

                for(c = 0; c<32; c++)
                    sprintf(output + (c * 2),"%02x",digest[c]);
                output[64] = '\0';

                if(strncmp(rules[i].hash, output, 64)!=0) {
                    fprintf(stderr,"%s\n%s\n", rules[i].hash, output);
                    return error("hash", "hash does not match");
                }
            }
#endif

Green light to privilege escalation

            if (setuid (target_uid) <0)
                return error("setuid",NULL);
            if (setgid (target_gid) <0)
                return error("setgid",NULL);
            if (seteuid (target_uid) <0)
                return error("seteuid",NULL);
            if (setegid (target_gid) <0)
                return error("setegid",NULL);

#ifdef CHROOT
            if (*CHROOT && (target_uid==0))
                if (chdir (CHROOT) == -1 || chroot (".") == -1)
                    return error("chroot", NULL);
            if (*CHRDIR)
                if (chdir (CHRDIR) == -1)
                    return error("chdir", NULL);
#endif

Fork as daemon if desired

#ifdef DAEMON
            if(fork_daemon) {

                pid_t pid;
                pid = fork();
                if(pid<0) return error("fork", NULL);

child

                else if(pid==0) {

                    int fd = open("/dev/tty", O_RDWR);
                    ioctl(fd, TIOCNOTTY, 0);
                    close(fd);
                    chdir("/");

secure default

                    umask(022);

process group

                    setpgid(0,0);

stdin

                    fd=open("/dev/null", O_RDWR);

stdout

                    dup(fd);

stderr

                    dup(fd);

Execute in foreground

                } else {

if pidfile is not an empty string (-p is used)

                    if( strncmp(pidfile,"",MAXFILEPATH) ) {

save the pid of the forked child. beware this does not work with some daemons that follow up with more forks.

                        FILE *fpid = fopen(pidfile,"w");
                        if(!fpid) error("pidfile", NULL);
                        fprintf(fpid,"%u\n",pid);
                        fclose(fpid);
                    }

leave us kids alone

                    _exit(0);
                }
            }
#endif

turn current process into the execution of command

            execv (fullcmd, &argv[optind]);

execv returns only on errors

            error("execv", NULL);

        }
    }

be polite

    fprintf(stderr,"Sorry.\n");
    exit(1);
}
h