From bfe686a1c58eaec9cf714f965369925120ae4ed4 Mon Sep 17 00:00:00 2001 From: Renzo Davoli Date: Tue, 9 Aug 2016 17:46:05 +0200 Subject: [PATCH] new features: scado and conditions in cado.conf --- Makefile.am | 44 ++++- cado.1 | 21 ++- cado.c | 74 ++++++-- cado_const.h | 39 +++++ cado_paths.h | 11 ++ cado_scado_check.c | 213 +++++++++++++++++++++++ cado_scado_check.h | 6 + caprint.c | 4 +- capset_from_namelist.c | 10 +- compute_digest.c | 92 ++++++++++ compute_digest.h | 20 +++ configure.ac | 27 ++- file_utils.c | 75 ++++++++ file_utils.h | 13 ++ get_scado_file.h | 11 ++ get_user_groups.c | 7 + pam_check.c | 6 +- pam_check.h | 2 + read_conf.c | 7 +- scado.1 | 140 +++++++++++++++ scado.c | 377 +++++++++++++++++++++++++++++++++++++++++ scado_parse.c | 315 ++++++++++++++++++++++++++++++++++ scado_parse.h | 17 ++ set_ambient_cap.c | 4 + 24 files changed, 1507 insertions(+), 28 deletions(-) create mode 100644 cado_const.h create mode 100644 cado_paths.h create mode 100644 cado_scado_check.c create mode 100644 cado_scado_check.h create mode 100644 compute_digest.c create mode 100644 compute_digest.h create mode 100644 file_utils.c create mode 100644 file_utils.h create mode 100644 get_scado_file.h create mode 100644 scado.1 create mode 100644 scado.c create mode 100644 scado_parse.c create mode 100644 scado_parse.h diff --git a/Makefile.am b/Makefile.am index 652ea14..47f4a41 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,9 +1,45 @@ -bin_PROGRAMS = cado caprint +bin_PROGRAMS = cado scado caprint -cado_SOURCES = cado.c pam_check.c get_user_groups.c capset_from_namelist.c read_conf.c set_ambient_cap.c +cado_SOURCES = cado.c pam_check.c get_user_groups.c capset_from_namelist.c read_conf.c set_ambient_cap.c \ + compute_digest.c file_utils.c scado_parse.c cado_scado_check.c -cado_LDADD = -lpam -lpam_misc -lcap +cado_LDADD = -lpam -lpam_misc -lcap -lmhash caprint_LDADD = -lcap -man_MANS = cado.1 caprint.1 cado.conf.5 +scado_SOURCES = scado.c pam_check.c file_utils.c compute_digest.c capset_from_namelist.c scado_parse.c + +scado_LDADD = -lpam -lpam_misc -lcap -lmhash + +common_nodist = cado_paths.h +BUILT_SOURCES = $(common_nodist) + +man_MANS = cado.1 caprint.1 scado.1 cado.conf.5 + +install-exec-hook: + (useradd -r -s /bin/nologin -g `getent passwd | grep cado | cut -f 3 -d ':'` cado ||\ + useradd -r -s /bin/nologin -U cado) || true + (mkdir -p ${SPOOL_DIR} ; chown root:cado ${SPOOL_DIR} && chmod 4770 $(SPOOL_DIR)) + chown :cado $(DESTDIR)$(bindir)/scado + chmod g+s $(DESTDIR)$(bindir)/scado + chown cado $(DESTDIR)$(bindir)/cado + chmod u+s $(DESTDIR)$(bindir)/cado + $(DESTDIR)$(bindir)/cado -s + +CLEANFILES = cado_paths.h +cado_paths.h: Makefile + @echo 'creating $@' + @sed >$@ 's/ *\\$$//' <<\END #\ + /* This file has been automatically generated. Do not edit. */ \ + #ifndef _CADO_PATHS_H \ + #define _CADO_PATHS_H \ + \ + /* Spool directory path */ \ + #define SPOOL_DIR "$(SPOOL_DIR)" \ + \ + /* Cado temporary exe directory path */ \ + #define CADO_EXE_DIR "$(CADO_EXE_DIR)" \ + \ + #endif /* _SCADO_PATHS_H */\ + END + diff --git a/cado.1 b/cado.1 index 40c4ba8..c68269e 100644 --- a/cado.1 +++ b/cado.1 @@ -26,7 +26,7 @@ For brevity, the \fBcap_\fR prefix of capability names can be omitted (e.g. \fBn have the same meaning). If it is allowed for the current user to run processes with the requested capabilities, the user is asked to -type their password (or to authenticate themselves as required by pam). +type their password (or to authenticate themselves as required by pam unless \fB-S\fR or \fB--scado\fR). Once the authentication succeeds, \fBcado\fR executes the command granting the required ambient capabilities. The file /etc/cado.conf (see \fBcado.conf\fR(5)) defines which capabilities can be provided by \fBcado\fR to each user. @@ -34,6 +34,18 @@ Cado itself is not a setuid executable, it uses the capability mechanism and it set its own capabilities. So after each change in the /etc/cado.conf, the capability set should be recomputed by root using the command \fBcado -s\fR or \fBcado --setcap\fR. +When \fBcado\fR runs is scado mode (by the option \fB-S\fR or \fB--scado\fR), if +.br +\ \ - the current user is allowed to run processes with the requested capabilities, +.br +\ \ - the \fBcommand\fR argument is an absolute pathname and +.br +\ \ - there is a specific authorization line in the user's scado file, +.br +\fBcado\fR runs the command granting the required ambient capabilities without any further authentication request +(it does not prompt for a password). +.SP + .SH OPTIONS .I cado accepts the following options: @@ -55,6 +67,12 @@ set of requested cababilities and the set of allowed capabilities \fB\-\-setcap \fBcado\fR computes the miminal set of capability required by itself and sets the file capability of the cado executable. .TP +\fB\-S +.TQ +\fB\-\-scado +launch \fBcado\fR with \fBscado\fR(1) support. \fRcommand\fI must be an absolute pathname and a specific authorization line must +appear in the user's scado file. +.TP \fB\-h .TQ \fB\-\-help @@ -63,5 +81,6 @@ print a short usage banner and exit. .SH SEE ALSO \fBcado.conf\fR(5), \fBcaprint\fR(1), +\fBscado\fR(1), \fBcapabilities\fR(7) diff --git a/cado.c b/cado.c index c2c99d7..57ed000 100644 --- a/cado.c +++ b/cado.c @@ -25,32 +25,41 @@ #include #include #include +#include +#include + #include #include #include #include #include -#include +#include +/* print a capset (in case of -v, verbose mode). */ static void printcapset(uint64_t capset, char *indent) { + if (capset) { cap_value_t cap; int count=0; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { if (capset & (1ULL << cap)) { count ++; - printf("%s%2d %016llx %s\n",indent,cap,1ULL< 1) - printf("%s %016" PRIx64 "\n",indent,capset); + printf("%s %016" PRIx64 "\n", indent, capset); + } else + printf("%s %016" PRIx64 " NONE\n",indent, UINT64_C(0)); } -#define OPTSTRING "hqvs" +/* command line args management */ +#define OPTSTRING "hqvsS" struct option long_options[]={ {"help", no_argument, NULL, 'h'}, {"quiet", no_argument, NULL, 'q'}, {"verbose", no_argument, NULL, 'v'}, {"setcap", no_argument, NULL, 'v'}, + {"scado", no_argument, NULL, 'S'} }; void usage(char *progname) { @@ -60,6 +69,7 @@ void usage(char *progname) { fprintf(stderr," -h, --help display help message and exit\n"); fprintf(stderr," -q, --quiet do not display warnings, do what it is allowed\n"); fprintf(stderr," -v, --verbose generate extra output\n"); + fprintf(stderr," -S, --scado check scado pre-authorization for scripts\n"); fprintf(stderr," -s, --setcap set the minimun caps for %s (root access)\n",progname); exit(1); } @@ -69,11 +79,14 @@ int main(int argc, char*argv[]) char *progname=basename(argv[0]); char **user_groups=get_user_groups(); uint64_t okcaps; - uint64_t reqcaps=0; + uint64_t reqcaps; uint64_t grantcap=0; int verbose=0; int quiet=0; int setcap=0; + int scado=0; + int pam_check_required = 1; + char copy_path[PATH_MAX] = ""; while (1) { int c=getopt_long(argc, argv, OPTSTRING, long_options, NULL); @@ -88,12 +101,16 @@ int main(int argc, char*argv[]) break; case 's': setcap=1; break; + case 'S': scado=1; + break; } } + /* setcap mode: cado sets the minimal required set of capability required by itself */ + if (setcap) { - if (geteuid() != 0) { - fprintf(stderr, "setcap requires root access\n"); + if (setuid(0) != 0 || geteuid() != 0) { + fprintf(stderr, "setcap requires root access %d\n",geteuid()); exit(2); } okcaps = get_authorized_caps(NULL, -1LL); @@ -109,6 +126,12 @@ int main(int argc, char*argv[]) exit(0); } + if (user_groups == NULL) { + fprintf(stderr, "No passwd entry for user '%d'\n",getuid()); + exit(2); + } + + /* -v without any other parameter: cado shows the set of ambient capabilities allowed for the current user/group */ if (verbose && (argc == optind)) { okcaps=get_authorized_caps(user_groups, -1LL); printf("Allowed ambient capabilities:\n"); @@ -119,21 +142,40 @@ int main(int argc, char*argv[]) if (argc - optind < 2) usage(progname); - if (capset_from_namelist(argv[optind], &reqcaps)) + /* parse the set of requested capabilities */ + if (capset_from_namelist(argv[optind], &reqcaps)) { + fprintf(stderr, "List of capabilities: syntax error\n"); exit(2); + } if (verbose) { printf("Requested ambient capabilities:\n"); printcapset(reqcaps, " "); } + /* check if the capability requested are also allowed */ okcaps=get_authorized_caps(user_groups, reqcaps); + optind++; + + /* scado mode, check if there is a pre-authorization for the command */ + if (scado) { + uint64_t scado_caps = cado_scado_check(user_groups[0], argv[optind], copy_path); + if (verbose) { + printf("Scado permitted capabilities for %s:\n", argv[optind]); + printcapset(scado_caps, " "); + } + okcaps &= scado_caps; + pam_check_required = 0; + } + + /* the user requested capabilities which are not allowed */ if (reqcaps & ~okcaps) { if (verbose) { printf("Unavailable ambient capabilities:\n"); printcapset(reqcaps & ~okcaps, " "); } + /* if not in "quiet" mode, do not complaint */ if (!quiet) { fprintf(stderr,"%s: Permission denied\n",progname); exit(2); @@ -142,17 +184,25 @@ int main(int argc, char*argv[]) grantcap = reqcaps & okcaps; - optind++; + /* revert setgid mode */ + setuid(getuid()); - if (pam_check(user_groups[0]) != 0) { + /* ask for pam authorization (usually password) if required */ + if (pam_check_required && pam_check(user_groups[0]) != PAM_SUCCESS) { fprintf(stderr,"%s: Authentication failure\n",progname); exit(2); } - set_ambient_cap(grantcap); + + /* okay: grantcap can be granted, do it! */ + if (grantcap) + set_ambient_cap(grantcap); + if (verbose && (reqcaps & ~okcaps)) { printf("Granted ambient capabilities:\n"); printcapset(grantcap, " "); } - execvp(argv[optind],argv+optind); + + /* exec the command in the new ambient capability environment */ + execvp(copy_path[0] == 0 ? argv[optind] : copy_path, argv+optind); exit(2); } diff --git a/cado_const.h b/cado_const.h new file mode 100644 index 0000000..f701179 --- /dev/null +++ b/cado_const.h @@ -0,0 +1,39 @@ +#ifndef _CADO_CONST_H +#define _CADO_CONST_H + +#include + +/* Default message to print in a new scado file. */ +#define DEFAULT_MESSAGE \ + "# This is a scado file.\n"\ + "# format: executable : capabilities_list [:]\n"\ + "# If you specify :,\n"\ + "# scado will automatically put the checksum of the file at the end of the line\n"\ + "# (for subsequent checks).\n" + +/* Tmp file template */ +#define TMP_TEMPLATE "cado-scado.XXXXXX" + +/* Tmp Directory */ +#define TMPDIR "/tmp" + +#define BUFSIZE 4096 + +#define DIGEST_WARNING "***********************************************************\n"\ + "* WARNING: EXECUTABLE DIGEST HAS CHANGED! *\n"\ + "***********************************************************\n"\ + "IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!\n"\ + "Someone could be changed your executable, and it's trying to execute a malware!\n"\ + "It is also possible that the executable is just changed (maybe a system upgrade?).\n" + +#define COPY_DIR CADO_EXE_DIR + +#define COPY_TEMPLATE "cado-exe-copy.XXXXXX" + +#define COMMENT_LINE 0 +#define NO_CHECKSUM_LINE 1 +#define CALCULATE_CHECKSUM_LINE 2 +#define CHECKSUM_LINE 3 +#define NOT_CONSIDERED_LINE 4 + +#endif /* _CADO_CONST_H */ diff --git a/cado_paths.h b/cado_paths.h new file mode 100644 index 0000000..3d07c15 --- /dev/null +++ b/cado_paths.h @@ -0,0 +1,11 @@ +/* This file has been automatically generated. Do not edit. */ +#ifndef _CADO_PATHS_H +#define _CADO_PATHS_H + +/* Spool directory path */ +#define SPOOL_DIR "/usr/local/var/spool/cado" + +/* Cado temporary exe directory path */ +#define CADO_EXE_DIR "/tmp" + +#endif /* _SCADO_PATHS_H */ diff --git a/cado_scado_check.c b/cado_scado_check.c new file mode 100644 index 0000000..25e499c --- /dev/null +++ b/cado_scado_check.c @@ -0,0 +1,213 @@ +/* + * cado: execute a command in a capability ambient + * Copyright (C) 2016 Davide Berardi and Renzo Davoli, University of Bologna + * + * This file is part of cado. + * + * Cado is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static void print_digest_warning(void) { + fprintf(stderr, "%s", DIGEST_WARNING); +} + +/* Avoiding TOCTOU attacks: + The executable file is copied in a safe place where the user cannot modify it. + (cado is setuid cado). + The digest of the copy is compared against the digest reported in the scado configuration file. + If the digests are the same, cado runs the copy */ +/* A watchdog process unlinks the copy as soon as either cado terminates or has started the command (execve system call) */ + +/* copy_and_check_digest returns 0 if it succeeds, i.e. + - a file at path exists and it is readable + - the hash digest of the file is equal to digest + and in this case newpath returns the newpath of the tmpfile to run */ +/* if scado does not require to check the digest, copy_and_check_digest + returns an empty string in newpath, i.e *newpath == 0 */ +/* if copy_and_check_digest fails, it returns -1 */ +static int copy_and_check_digest(const char *path, char *digest, char *newpath) { + size_t newpathlen; + /* grandchild -> grandparent pipe: + write the path of the tmp copy and close + in case of error: close (without writing anything) + in case of hash mismatch write a NULL byte and close */ + int gc_pipe[2]; + pid_t childpid; + + /* grandparent -> grandchild pipe: used to check when the exec occours. */ + /* this pipe has the FD_CLOEXEC bit set. + it is unvoluntarily closed either when an exec succeeds or when the grandparent terminates + (end or abend, it does not matter) */ + /* when the grandchild gets the EOF it unlinks the temporary file */ + int gp_pipe[2]; + + if (!path || !digest || !newpath) { + return -1; + } + + if (snprintf(newpath, PATH_MAX, "%s/%s", COPY_DIR, COPY_TEMPLATE) < 0) { + return -1; + } + newpathlen = strlen(newpath); + + if (pipe(gc_pipe)) { + perror("pipe"); + return -1; + } + + if (pipe(gp_pipe)) { + perror("pipe"); + return -1; + } + + if (fcntl(gp_pipe[0], F_SETFD, FD_CLOEXEC) || + fcntl(gp_pipe[1], F_SETFD, FD_CLOEXEC)) { + perror("fdcntl cloexec"); + return -1; + } + + /* Start garbage collector + * Double fork to avoid zombie processes and to remove the temporary file when it is not needed any more */ + if(!(childpid=fork())) { + /* Child */ + if(!fork()) { + /* Grandchild */ + char newdigest[DIGESTSTRLEN + 1]; + char c; + int exit_status = 1; + + /* if the grandparent terminates before reading the newpath, + write returns EPIPE. So even in this situation the garbage gets collected i.e. unlink(newpath)*/ + /* setsid to avoid the propagation of signals to the process group (e.g. ^C) */ + if (close(gc_pipe[0]) || close(gp_pipe[1]) || setsid() < 0 || signal(SIGPIPE, SIG_IGN) == SIG_ERR) + exit(-1); + + if (copytemp_digest(path, newpath, newdigest) < 0) { + perror("copytemp_hash"); + exit(-1); + } + + //debug only + //printf("GS %s\n%s\n%s\n", newpath,digest,newdigest); + if (chmod(newpath, 00611) < 0) + goto end; + + if (strcmp(digest,newdigest) == 0) { + if (write(gc_pipe[1], newpath, newpathlen + 1) < 1) + goto end; + } else { + char err = 0; + if (write(gc_pipe[1], &err, 1) < 1) + goto end; + } + + if (close(gc_pipe[1])) + goto end; + + /* No data should be written on the pipe from the other end. This is only + * to check when the parent calls exec() (or terminates)*/ + while (read(gp_pipe[0], &c, sizeof(char)) > 0) + ; + + close(gp_pipe[0]); + exit_status = 0; +end: + unlink(newpath); + exit(exit_status); + } else { + exit(0); + } + } + waitpid(childpid, NULL, 0); + + /* (grand) Parent */ + if (close(gc_pipe[1]) || close(gp_pipe[0])) { + perror("close pipe"); + return -1; + } + + /* it should read a string of the same length of the original newpath which is the + template for the tmp file (as described in mkstemp(3) */ + int n; + if ((n=read(gc_pipe[0], newpath, newpathlen + 1)) <= 0) { + return -1; + } + + if (close(gc_pipe[0])) { + perror("close"); + } + + if (newpath[0] == 0) { + print_digest_warning(); + return -1; + } + + return 0; +} + +/* given the username and the command path, cado_scado_check returns the set of + permitted capabilities, as defined by the scado(1) command */ +uint64_t cado_scado_check(const char *username, const char *exe_path, char *new_path) { + char scado_file[PATH_MAX]; + char digest[DIGESTSTRLEN + 1]; + int rv; + uint64_t capset = 0; + + if (!username || !exe_path || !new_path) + return 0; + + if (get_scado_file(username, scado_file) <= 0){ + perror("get scado file"); + return 0; + } + + raise_cap_dac_read_search(); + rv = scado_path_getinfo(scado_file, exe_path, &capset, digest); + lower_cap_dac_read_search(); + + /* default value: do not run a copy, directly run the command */ + *new_path = 0; + if (rv <= 0) { + /* error: no capabilities canbe granted */ + if (rv < 0) + perror("error opening scado file"); + return 0; + } else { + /* if no digest was specified in the scado configuration line for the current command: + the capabilities in capset can be granted. + otherwise copy the executable file to avoid TOCTOU attacks */ + if (*digest == 0 || copy_and_check_digest(exe_path, digest, new_path) == 0) + return capset; + else + return 0; + } +} diff --git a/cado_scado_check.h b/cado_scado_check.h new file mode 100644 index 0000000..6c7d474 --- /dev/null +++ b/cado_scado_check.h @@ -0,0 +1,6 @@ +#ifndef CADO_SCADO_CHECK_H +#define CADO_SCADO_CHECK_H + +uint64_t cado_scado_check(const char *username, const char *exe_path, char *new_path); + +#endif diff --git a/caprint.c b/caprint.c index 6a0a1a2..9443d08 100644 --- a/caprint.c +++ b/caprint.c @@ -1,5 +1,5 @@ /* - * cado: execute a command in a capability ambient + * caprint: print the set of ambient capability of a process. * Copyright (C) 2016 Renzo Davoli, University of Bologna * * This file is part of cado. @@ -48,7 +48,7 @@ uint64_t get_capamb(pid_t pid) { status++; if (status == target) { int fields = 0; - if ((fields = fscanf(f,"%" PRIx64 "",&capamb)) != 1) + if ((fields = fscanf(f,"%" SCNx64 "",&capamb)) != 1) fprintf(stderr, "WARNING: fscanf on %s return %d fields.\n", filename, fields); break; } diff --git a/capset_from_namelist.c b/capset_from_namelist.c index 14f52a9..df5efc6 100644 --- a/capset_from_namelist.c +++ b/capset_from_namelist.c @@ -48,12 +48,16 @@ static int addcap(char *name, uint64_t *capset) { } } +/* convert a list of comma separated capability tags to a bitmask of capabilities */ +/* capset_from_namelist allows capability names with or without the "cap_" prefix. */ int capset_from_namelist(char *namelist, uint64_t *capset) { int rv=0; char *onecap; - char *tmptok = NULL; - for (; (onecap=strtok_r(namelist,",",&tmptok)) != NULL; namelist=NULL) - rv |= addcap(onecap,capset); + char *tmptok; + char *spacetok; + *capset = 0; + for (; (onecap = strtok_r(namelist,",",&tmptok)) != NULL; namelist = NULL) + rv |= addcap(strtok_r(onecap," \t",&spacetok), capset); return rv; } diff --git a/compute_digest.c b/compute_digest.c new file mode 100644 index 0000000..7e93373 --- /dev/null +++ b/compute_digest.c @@ -0,0 +1,92 @@ +/* + * scado: setuid in capability sauce. + * Copyright (C) 2016 Davide Berardi and Renzo Davoli, University of Bologna + * + * This file is part of cado. + * + * Cado is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#define BUFSIZE 1024 + +/* compute the hash digest for file whose descriptor is infd. + if outfd >= 0 copy the contents to the file whose descriptor is outfd. + the return value is the size of the file (or a negative value in case of error). + the size of ascii_digest must be at least DIGESTSTRLEN + 1*/ +static ssize_t fcompute_digest(int infd, int outfd, char *ascii_digest) { + char buf[BUFSIZE]; + unsigned char binary_digest[DIGESTLEN]; + MHASH td; + + ssize_t n; + ssize_t rv = 0; + td = mhash_init(DIGESTTYPE); + + while ((n=read(infd,buf,BUFSIZE)) > 0) { + mhash(td, buf, n); + if (outfd >= 0) write(outfd, buf, n); + rv += n; + } + + mhash_deinit(td, binary_digest); + if (n >= 0) { + int i; + for (i = 0; i < DIGESTLEN; i++, ascii_digest += 2) + sprintf(ascii_digest, "%.2x", binary_digest[i]); + *ascii_digest = 0; + } + return (n < 0) ? n : rv; +} + +/* compute the hash digest of the file (the first arg is the pathname) */ +ssize_t compute_digest(const char *path, char *digest) { + int fd=open(path, O_RDONLY); + if (fd >= 0) { + ssize_t rv = fcompute_digest(fd, -1, digest); + close(fd); + return rv; + } else + return -1; +} + +/* compute the hash digest of the file (the first arg is the pathname) + while copying it in a temporary file. The second arg is a template for tmp files + as explained in mkstemp(3) + */ +ssize_t copytemp_digest(const char *inpath, char *outtemplate, char *digest) { + int infd=open(inpath, O_RDONLY); + int outfd; + mode_t oldmask; + ssize_t rv; + if (infd < 0) + return -1; + oldmask = umask(027); + outfd = mkstemp(outtemplate); + umask(oldmask); + if (outfd < 0) + return -1; + rv = fcompute_digest(infd, outfd, digest); + close(infd); + close(outfd); + return rv; +} + diff --git a/compute_digest.h b/compute_digest.h new file mode 100644 index 0000000..243c297 --- /dev/null +++ b/compute_digest.h @@ -0,0 +1,20 @@ +#ifndef _COMPUTE_DIGEST_H +#define _COMPUTE_DIGEST_H +#include + +#define DIGESTTYPE MHASH_SHA256 +#define DIGESTLEN (mhash_get_block_size(DIGESTTYPE)) +#define DIGESTSTRLEN (2*DIGESTLEN) + +/* compute the hash digest. + store the result (hex string) in hashstr: an array of at least DIGESTSTRLEN chars + return the size of the file in case of success, -1 in case of error */ +ssize_t compute_digest(const char *path, char *hashstr); + +/* copytemp_hash copies the file whose path is 'inpath' in a temporary file whose path is + created by mkstemp using 'outtemplate' */ +/* During the copy, copytemp_hash computes the hash and stores the (hex) result in hashstr */ +ssize_t copytemp_digest(const char *inpath, char *outtemplate, char *hashstr); + +#endif /* _COMPUTE_DIGEST_H */ + diff --git a/configure.ac b/configure.ac index 9821c18..9d08f65 100644 --- a/configure.ac +++ b/configure.ac @@ -2,10 +2,11 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.69]) -AC_INIT([cado], [0.9], [info@v2.cs.unibo.it]) +AC_INIT([cado], [0.9.1], [info@v2.cs.unibo.it]) AM_INIT_AUTOMAKE([foreign dist-bzip2]) AC_CONFIG_SRCDIR([pam_check.h]) AC_CONFIG_HEADERS([config.h]) +CFLAGS="$CFLAGS -Wall" # Checks for programs. AC_PROG_CC @@ -14,7 +15,7 @@ AC_PROG_INSTALL # Checks for libraries. AC_CHECK_LIB([s2argv], [s2argv], [], [ - AC_MSG_ERROR([Could not find S2ARGV library]) + AC_MSG_ERROR([Could not find S2ARGV library (https://github.com/rd235/s2argv-execs)]) ]) # Checks for header files. @@ -38,4 +39,26 @@ AC_TYPE_UINT64_T # Checks for library functions. AC_CHECK_FUNCS([strdup strtoull]) +AC_DEFUN([CADO_CONF_VAR], +[AC_ARG_VAR([$1], [$2 @<:@$3@:>@]) +if test "$$1" = ""; then + $1='$3' +fi +]) + +AC_ARG_WITH([editor], + [AC_HELP_STRING([--with-editor=EDITOR], [path to default editor])], + [editor_defined="$with-editor"], + [editor_defined="no"]) + +AS_IF([test "x$editor_defined" = "xno"], [ + AC_PATH_PROG([editor_defined], [vi], [/usr/bin/vi]) +]) + +AC_DEFINE_UNQUOTED([EDITOR], ["$editor_defined"], [default editor]) + +# Set the paths. +CADO_CONF_VAR([SPOOL_DIR], [the directory where all the user scado files reside],[${localstatedir}/spool/cado]) +CADO_CONF_VAR([CADO_EXE_DIR], [the directory where all the temporary executable files reside],[/tmp]) + AC_OUTPUT([Makefile]) diff --git a/file_utils.c b/file_utils.c new file mode 100644 index 0000000..68f2508 --- /dev/null +++ b/file_utils.c @@ -0,0 +1,75 @@ +/* + * cado: execute a command in a capability ambient + * Copyright (C) 2016 Renzo Davoli, University of Bologna + * + * This file is part of cado. + * + * Cado is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + * + */ + +#include +#include +#include +#include +#include +#include + +#undef __linux__ +#ifdef __linux__ +#include +#define MAXSENDFILE 0x7ffff000 +#else +#define BUFSIZE 1024 +#endif + +/* copy a file: when possible use a fast system call */ +ssize_t copyfile(int infd, int outfd) { + ssize_t rv=0; + ssize_t n; +#ifdef __linux__ + do + rv += n = sendfile(outfd, infd, NULL, MAXSENDFILE); + while (n == MAXSENDFILE); +#else + static int pagesize; + if (__builtin_expect((pagesize == 0),0)) pagesize=sysconf(_SC_PAGESIZE); + char buf[BUFSIZE]; + while ((n = read(infd, buf, BUFSIZE)) > 0) + rv += write(outfd, buf, n); +#endif + return (n < 0) ? n : rv; +} + +/* create a temporary copy of a file (using copyfile) */ +/* outtemplate is a template for temporary files as explained in mkstemp(3) */ +ssize_t copytemp(char *inpath, char *outtemplate) { + int infd=open(inpath, O_RDONLY); + int outfd; + mode_t oldmask; + ssize_t rv; + if (infd < 0) + return -1; + oldmask = umask(077); + outfd = mkstemp(outtemplate); + umask(oldmask); + if (outfd < 0) { + close(infd); + return -1; + } + rv = copyfile(infd, outfd); + close(infd); + close(outfd); + return rv; +} diff --git a/file_utils.h b/file_utils.h new file mode 100644 index 0000000..500a3cd --- /dev/null +++ b/file_utils.h @@ -0,0 +1,13 @@ +#ifndef _FILE_UTILS_H +#define _FILE_UTILS_H + +/* copyfile copies the file infd (from the current offset) to + outfd (from the current offset) */ +/* the return value is the number of bytes copied */ +ssize_t copyfile(int infd, int outfd); + +/* copytemp copies the file whose path is 'inpath' in a temporary file + whose pathname has been created by mkstemp(3) using 'outtemplate; */ +ssize_t copytemp(char *inpath, char *outtemplate); + +#endif diff --git a/get_scado_file.h b/get_scado_file.h new file mode 100644 index 0000000..4d80e42 --- /dev/null +++ b/get_scado_file.h @@ -0,0 +1,11 @@ +#ifndef _GET_SPOOL_FILE_H +#define _GET_SPOOL_FILE_H +#include +#include + +/* Get the user scado file */ +static inline int get_scado_file(const char *username, char *path) { + return snprintf(path, PATH_MAX, "%s/%s", SPOOL_DIR, username); +} +#endif /* _GET_SPOOL_FILE_H */ + diff --git a/get_user_groups.c b/get_user_groups.c index 16c33f2..81c3aa2 100644 --- a/get_user_groups.c +++ b/get_user_groups.c @@ -27,11 +27,18 @@ #include #include +/* if the user does not exist get_user_groups returns NULL, + otherwise it returns a dynamic allocated array whose + first element (0) is the username of the current user. + The following elements are the names of all the groups the current user belongs to. + A NULL element tags the end of the array */ char **get_user_groups(void) { uid_t uid=getuid(); struct passwd *pwd=getpwuid(uid); int ngroups=0; char **user_groups=NULL; + if (pwd == NULL) + return NULL; getgrouplist(pwd->pw_name, pwd->pw_gid, NULL, &ngroups); if (ngroups > 0) { gid_t gids[ngroups]; diff --git a/pam_check.c b/pam_check.c index 035faec..6693ec7 100644 --- a/pam_check.c +++ b/pam_check.c @@ -27,13 +27,17 @@ #include #include +/* call PAM to authorize the current user. + usually this means to prompt the user for a password, + but it can be configured using PAM */ + int pam_check(char *username) { pam_handle_t* pamh; struct pam_conv pamc={.conv=&misc_conv, .appdata_ptr=NULL}; int rv; - pam_start ("capdo", username, &pamc, &pamh); + pam_start ("cado", username, &pamc, &pamh); rv= pam_authenticate (pamh, 0); pam_end (pamh, 0); diff --git a/pam_check.h b/pam_check.h index c96bc3d..6f75191 100644 --- a/pam_check.h +++ b/pam_check.h @@ -1,4 +1,6 @@ #ifndef PAM_CHECK_H #define PAM_CHECK_H +#include + int pam_check(char *username); #endif diff --git a/read_conf.c b/read_conf.c index c6fe7b5..482dc56 100644 --- a/read_conf.c +++ b/read_conf.c @@ -37,6 +37,8 @@ #define CADO_CONF CONFDIR "/cado.conf" +/* cado.conf management */ + /* groupmatch returns 1 if group belongs to grouplist */ static int groupmatch (char *group, char **grouplist) { for (;*grouplist; grouplist++) { @@ -64,10 +66,10 @@ uint64_t get_authorized_caps(char **user_groups, uint64_t reqset) { f=fopen(CADO_CONF, "r"); if (f) { char *line=NULL; - ssize_t len,n=0; + size_t n=0; /* set s2argv security, children must drop their capabilities */ s2_fork_security=drop_capabilities; - while ((len=getline(&line, &n, f)) > 0 && (reqset & ~ok_caps)) { + while (getline(&line, &n, f) > 0 && (reqset & ~ok_caps)) { //printf("%s",line); char *scan=line; char *tokencap; @@ -87,7 +89,6 @@ uint64_t get_authorized_caps(char **user_groups, uint64_t reqset) { //printf("UG %s\n",tokenusergroup); tokencondition=strtok_r(NULL, ":\n", &tmptok); //printf("COND %s\n",tokencondition); - capset=0; if (capset_from_namelist(tokencap, &capset) < 0) continue; if (user_groups == NULL) { diff --git a/scado.1 b/scado.1 new file mode 100644 index 0000000..fb4af14 --- /dev/null +++ b/scado.1 @@ -0,0 +1,140 @@ +.TH SCADO 1 "June 23, 2016" "VirtualSquare Labs" +.SH NAME +scado \- Script Capability Ambient DO +.SH SYNOPSIS +.B scado +.B -D +| +.B -e +| +.B -l +.br +.B scado +.B -u +.I command +| +.B -U +.br +.B scado +.B -h + +.SH DESCRIPTION + +\fBcado(1)\fR permits to delegate capabilities to users. +Users can grant a subset of these ambient capabilities to trusted programs. +Each user can define their own list of trusted programs and which capabilities to grant, using a scado file. +\fBcado -S\fR or \fBcado --scado\fR run those trusted programs without any further authentication. +In this way it is also possible to run programs requiring specific capabilities within a bash script. + +\fBScado\fR is the command a user can run to create, edit, check or delete their own scado file. + +Each line of a scado file file has the following syntax: +.br +.RS 4 +.I path_of_the_executable_file : capability_list +.RE +.br +or +.br +.RS 4 +.I path_of_the_executable_file : capability_list : sha256_digest_of_the_executable +.RE +.br +(See the EXAMPLES section at the end f the man page for more info. All the trailing part of a line following a # sign is a comment.). + +The \fIpath_of_the_executable_file\fR must be absolute. + +The \fIcapability_list\fR is a comma separated list of capability names or capability +masks. For brevity, the \fBcap_\fR prefix of capabilities names can be omitted +(e.g. \fBnet_admin\fR and \fBcap_net_admin\fR have the same meaning). + +The \fIsha256_digest_of_the_executable\fR prevents \fITOCTTOU\fR attacks. When a user +wants to run the file at \fIpath_of_the_executable_file\fR granting it some of +the capabilities in the \fIcapability_list\fR, the permission is denied if +its sha256 digest does not match \fIsha256_digest_of_the_executable\R. + +If there are only two colon (\fB:\fR) separated fields in a line, it means that +the user trusts a priori the integrity of the file whose pathname is \fIpath_of_the_executable_file\fR. +It can be, for example, a program in /bin or /usr/bin not modifiable by users. + +If there are three fields (i.e. two colon characters), it means that the user wants the +cryptographic digest check on the executable file integrity. +When a user edits their scado file, if the field (\fIsha256_digest_of_the_executable\fR) is empty, \fBscado\fR +computes it automatically when the scado file is saved. + +\fBScado\fR asks for user authentication by PAM to confirm any modification of the scado file. + +There is also a \fITOCTTOU\fR protection at running time: \fDcado -S\fR copies the executable file +in a safe place, where the user cannot change it, and runs it only if the integrity check on it succeeds. +The user (or a malicious intruder acting as the user) cannot modify the file after the integrity check has completed +and before the program is loaded. + +.SH OPTIONS +.I scado +accepts the following options: +.TP +\fB\-l +.TQ +\fB\-\-list +Display the current scado file. The actual file in the file system is not accessible by unprivileged users, for security reasons. +.TP +\fB\-e +.TQ +\fB\-\-edit +Edit the scado file of the current user using the editor specified by either the +\fBVISUAL\fR or the \fBEDITOR\fR environment variable (checked in that order). +After you exit from the editor, the modified file will be installed automatically. +.TP +\fB\-D +.TQ +\fB\-\-delete +Delete the current user's scado file. +.TP +\fB\-u\fR \fIcommand\fR +.TQ +\fB\-\-update\fR \fIcommand\fR +Recompute the hash of the line which starts with \fIcommand\fR. +.TP +\fB\-U +.TQ +\fB\-\-update-all +Update all the digest entries. +.TP +\fB\-h +.TQ +\fB\-\-help +print a short usage banner and exit. + +.SH EXCEPTIONS FILES EXAMPLES + +.PP +Allow \fBcado -S\fR to run /bin/ping providing it with the cap_net_raw capability, without any integrity check: +.RS 4 +/bin/ping : cap_net_raw +.RE + +.PP +Allow the activation of ping with cap_net_raw provided it has a specific SHA256 digest +.RS 4 +/bin/ping : cap_net_raw : dcb237f1cb20ee7b1550900d1b524c554063fd17fc673c56d341736ced6bed4b +.RE + +.PP +Compute the SAH256 digest of (the current version of) ping so, +allow the activation of ping with cap_net_raw provided it has not been modified. +.RS 4 +/bin/ping : cap_net_raw : +.RE + +.PP +If one of the example lines here above has been inserted in the user scado file using +\fBscado -e\fR, it is possible to execute ping as follows: +.RS 4 +cado -S cap_net_raw /bin/ping +.RE + +.SH SEE ALSO +\fBcado\fR(1), +\fBcapabilities\fR(7) + + diff --git a/scado.c b/scado.c new file mode 100644 index 0000000..b4666b2 --- /dev/null +++ b/scado.c @@ -0,0 +1,377 @@ +/* + * scado: setuid in capability sauce. + * Copyright (C) 2016 Davide Berardi, University of Bologna + * + * This file is part of cado. + * + * Cado is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#define EDIT_PAM_MAXTRIES 3 + +#define SUCCESS 0 + +/* Generic error */ +#define ERROR_EXIT -1 + +/* Authentication error */ +#define ERROR_AUTH -2 + +const char *tmpdir; + +#define OPTSTRING "hu:UDle" +struct option long_options[] = { + {"help", no_argument, NULL, 'h'}, + {"update", required_argument, NULL, 'u'}, + {"update-all", no_argument, NULL, 'U'}, + {"delete", no_argument, NULL, 'D'}, + {"list", no_argument, NULL, 'l'}, + {"edit", no_argument, NULL, 'e'}, +}; + +enum scado_option {none, update, delete, list, edit, toomany}; + +void usage_n_exit(char *progname, char *message) { + fprintf(stderr, "%s - script cado, setuid in capability sauce\n", progname); + fprintf(stderr, "usage: %s OPTIONS\n", progname); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " -h --help display help message and exit\n"); + fprintf(stderr, " -u --update update the checksum of command\n"); + fprintf(stderr, " -U --update-all update the checksum of all commands\n"); + fprintf(stderr, " -D --delete delete the scado file\n"); + fprintf(stderr, " -l --list print the scado file\n"); + fprintf(stderr, " -e --edit edit the scado file\n"); + if (message) + fprintf(stderr, "\n%s\n",message); + exit(ERROR_EXIT); +} + +/* watchdog for garbage collection. + the temporary file for scado editing is removed in case of abend */ +static int editor_garbage_collect(char *path) { + int checkpipe[2]; + pid_t childpid; + if (pipe(checkpipe)) + return -1; + /* Start garbage collector + * Double fork to avoid zombie processes and to remove the temporary file when it is not needed any more */ + if(!(childpid = fork())) { + /* Child */ + if(!fork()) { + char c = 0; + /* Grandchild */ + if (close(checkpipe[1]) == 0 && setsid() > 0) + read(checkpipe[0], &c, 1); + if (c == 0) + unlink(path); + exit(0); + } else + exit(0); + } + waitpid(childpid, NULL, 0); + /* (grand) parent */ + close(checkpipe[0]); + return(checkpipe[1]); +} + +static void editor_garbage_collect_do_not_unlink(int fd) { + char c = 'K'; // keep it, any other non-null char would fit. + write(fd, &c, 1); +} + +/* command line selectable functions */ + +int scado_none_or_toomany(char *progname, char *username, char *program_path) { + usage_n_exit(progname, "select exactly one option among: -u -U -D -l -e"); + return 0; +} + +int scado_delete(char *progname, char *username, char *program_path) { + char scado_file[PATH_MAX]; + + if (get_scado_file(username, scado_file) < 0) + return ERROR_EXIT; + + /* Get the authorization. */ + if (pam_check(username) != PAM_SUCCESS) { + return ERROR_AUTH; + } + + return unlink(scado_file); +} + +int scado_update(char *progname, char *username, char *program_path) { + char tmp_file[PATH_MAX]; + char scado_file[PATH_MAX]; + + if (get_scado_file(username, scado_file) < 0) + return ERROR_EXIT; + + /* Get the authorization. */ + if (pam_check(username) != PAM_SUCCESS) { + return ERROR_AUTH; + } + + if (snprintf(tmp_file, PATH_MAX, "%s/%s", tmpdir, TMP_TEMPLATE) < 0) + return ERROR_EXIT; + + if (copytemp(scado_file, tmp_file) < 0) + return ERROR_EXIT; + + scado_copy_update(tmp_file, scado_file, program_path); + + if (unlink(tmp_file) < 0) + return ERROR_EXIT; + + return SUCCESS; +} + +int scado_list(char *progname, char *username, char *program_path) { + char scado_file[PATH_MAX]; + int fd; + int outfl = fcntl(STDOUT_FILENO, F_GETFL, 0); + if (outfl & O_APPEND) + fcntl(STDOUT_FILENO, F_SETFL, outfl & ~O_APPEND); + + if (get_scado_file(username, scado_file) < 0) + return ERROR_EXIT; + + if ((fd = open(scado_file, O_RDONLY)) < 0) + return fd; + + copyfile(fd, STDOUT_FILENO); + + close(fd); + return SUCCESS; +} + +int scado_edit(char *progname, char *username, char *program_path) { + char tmp_file[PATH_MAX]; + char scado_file[PATH_MAX]; + char *editor; + char *argv[]={NULL, tmp_file, NULL}; + int status = 0; + pid_t pid, xpid; + char digest_before[DIGESTSTRLEN + 1]; + char digest_after[DIGESTSTRLEN + 1]; + int garbage_collect_fd; + + /* Ignore signals. */ + (void) signal(SIGHUP, SIG_IGN); + (void) signal(SIGINT, SIG_IGN); + (void) signal(SIGQUIT, SIG_IGN); + + if (get_scado_file(username, scado_file) < 0) + return ERROR_EXIT; + + if (snprintf(tmp_file, PATH_MAX, "%s/%s", tmpdir, TMP_TEMPLATE) < 0) + return ERROR_EXIT; + + if (copytemp_digest(scado_file, tmp_file, digest_before) < 0) { + *digest_before = 0; + if (errno == ENOENT) { + int tmpfd=mkstemp(tmp_file); + if (tmpfd < 0) + return ERROR_EXIT; + if (write(tmpfd, DEFAULT_MESSAGE, sizeof(DEFAULT_MESSAGE)-1) < 0) + return ERROR_EXIT; + close(tmpfd); + } else + return ERROR_EXIT; + } + + garbage_collect_fd = editor_garbage_collect(tmp_file); + + /* Get the editor from the configuration. */ + if (((editor = getenv("VISUAL")) == NULL || *editor == '\0') && + ((editor = getenv("EDITOR")) == NULL || *editor == '\0')) { + editor = EDITOR; + } + argv[0] = editor; + + switch (pid = fork()) { + case -1: + perror("fork"); + exit(ERROR_EXIT); + case 0: + /* XXX secure? */ + if (setgid(getgid()) < 0) { + perror("setgid(getgid())"); + exit(ERROR_EXIT); + } + if (setuid(getuid()) < 0) { + perror("setuid(getuid())"); + exit(ERROR_EXIT); + } + execvp(argv[0], argv); + perror(editor); + exit(ERROR_EXIT); + default: + /* parent */ + break; + } + /* parent */ + for (;;) { + xpid = waitpid(pid, &status, 0); + if (xpid == -1) { + if(errno != EINTR) { + fprintf(stderr, "wait pid: error waiting for PID %ld (%s): %s \n", + (long) xpid, editor, strerror(errno)); + return ERROR_EXIT; + } + } + else if (WIFEXITED(status) && WEXITSTATUS(status)) { + fprintf(stderr,"wait pid: exit status"); + return ERROR_EXIT; + } + else if(WIFSIGNALED(status)) { + fprintf(stderr, "%s killed: signal %d (%score dumped)\n", + editor, WTERMSIG(status), WCOREDUMP(status) ? "": "no "); + } + else { + break; + } + } + + /* Restore signals. */ + (void) signal(SIGHUP, SIG_DFL); + (void) signal(SIGINT, SIG_DFL); + (void) signal(SIGQUIT, SIG_DFL); + + if(compute_digest(tmp_file, digest_after) < 0) + return ERROR_EXIT; + + if (strcmp(digest_before, digest_after) != 0) { + int count; + + /* Get the authorization. */ + for (count = 0; count < EDIT_PAM_MAXTRIES; count++) { + if (pam_check(username) == PAM_SUCCESS) { + break; + } + if (count < EDIT_PAM_MAXTRIES - 1) + fprintf(stderr, "PAM authorization failed\n"); + else { + editor_garbage_collect_do_not_unlink(garbage_collect_fd); + fprintf(stderr, "A copy of the edited scado file has been saved as %s\n", tmp_file); + return ERROR_AUTH; + } + } + scado_copy_update(tmp_file, scado_file, NULL); + } + + close(garbage_collect_fd); + return SUCCESS; +} + +typedef int (* option_function) (char *progname, char *username, char *program_path); + +/* array to select the requested option */ +option_function option_map[] = { + scado_none_or_toomany, + scado_update, + scado_delete, + scado_list, + scado_edit, + scado_none_or_toomany}; + +/* update the chosen option. If one was already chosen, switch to "toomany" */ +static inline enum scado_option set_option(enum scado_option option, enum scado_option newvalue) { + return option == none ? newvalue : toomany; +} + +int main(int argc, char **argv) +{ + struct passwd *pw; + char *progname = basename(argv[0]); + char *username; + char *program_path = ""; /* "" means ALL */ + enum scado_option option=none; + + int rval = 1; + + if ((pw = getpwuid(getuid())) == NULL) { + fprintf(stderr, "Your UID isn't in the passwd file.\n"); + exit(ERROR_EXIT); + } + username = pw->pw_name; + + if ((tmpdir = getenv("TMPDIR")) == NULL || *tmpdir== '\0') { + tmpdir = TMPDIR; + } + + while(1) { + int c = getopt_long(argc, argv, OPTSTRING, long_options, NULL); + if (c < 0) + break; + switch(c) { + case 'h': + usage_n_exit(progname, NULL); + break; + case 'u': + program_path = optarg; + case 'U': + option=set_option(option,update); + break; + case 'D': + option=set_option(option,delete); + break; + case 'l': + option=set_option(option,list); + break; + case 'e': + option=set_option(option,edit); + break; + default: + usage_n_exit(progname, NULL); + } + } + if (optind != argc) /* unknown trailing arguments */ + usage_n_exit(progname, "unknown trailing arguments"); + + rval = option_map[option](progname, username, program_path); + + if (rval == ERROR_AUTH) { + fprintf(stderr, "%s: Authorization failure.\n", progname); + } else if (rval) { + fprintf(stderr, "%s returned: %d, %s\n", progname, rval, strerror(errno)); + } + + return rval; +} diff --git a/scado_parse.c b/scado_parse.c new file mode 100644 index 0000000..343165e --- /dev/null +++ b/scado_parse.c @@ -0,0 +1,315 @@ +/* + * cado: execute a command in a capability ambient + * Copyright (C) 2016 Renzo Davoli and Davide Berardi, University of Bologna + * + * This file is part of cado. + * + * Cado is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program 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. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* this structure stores the output of the "parsing" of a scado line. + a line has (at most) three colon ":" separated fields. + all field and end pointers are addresses within the string "line". + the i-th field begins at field[i] and terminates just before end[i] */ + +struct scado_line { + char *line; + char *field[3]; + char *end[3]; +}; + +/* parse a scado line */ +static struct scado_line scado_parse(char *line) { + struct scado_line out={line, {NULL, NULL, NULL}, {NULL, NULL, NULL}}; + char *scan=line; + int i; + for (i=0; i<3; i++) { + while (isspace(*scan)) scan++; + out.field[i] = scan; + if (*scan == '#' || *scan == 0) goto end; + while (*scan != ':') { + if (scan[0] == '\\' && scan[1] != 0) scan++; + if (*scan == '#' || *scan == 0) goto end; + scan++; + } + out.end[i] = scan; + scan++; + } + return out; +end: + out.end[i] = scan; + return out; +} + +/* true if the current line matches path, false otherwise. + scado_path_match manages the escape chars '\' in the scado line + without any copy */ +static int scado_path_match(const char *path, struct scado_line *line) { + char *lpath; + for (lpath = line->field[0]; *path && *lpath; path++, lpath++) { + if (lpath[0] == '\\' && lpath[1] != 0) lpath++; + if (*lpath != *path) + break; + } + return (*path == 0 && (*lpath == 0 || isspace(*lpath) || *lpath == ':')); +} + +/* get the capability set from a scado file line */ +static int scado_get_caps(struct scado_line *line, uint64_t *capset) { + if (line->field[1]) { + size_t caplen = line->end[1] - line->field[1]; + char caps[caplen+1]; + strncpy(caps,line->field[1],caplen); + caps[caplen]=0; + return capset_from_namelist(caps, capset); + } else + return -1; +} + +/* scado_check_digest_syntax returns: + -1 if the digest field has syntax errors, + 0 if it is an ampty field (but the field exists!) + 1 if the line has just two fields (one ':') OR there is a well-formed hex digest */ + +static int scado_check_digest_syntax(struct scado_line *line) { + if (line->field[2]) { + int i; + size_t digestlen = line->end[2] - line->field[2]; + char digest[digestlen+1]; + strncpy(digest,line->field[2],digestlen); + digest[digestlen]=0; + if (*digest == 0) + return 0; + if (strlen(digest) < DIGESTSTRLEN) + return -1; + for (i = 0; i < DIGESTSTRLEN && isxdigit(digest[i]); i++) + ; + if (i < DIGESTSTRLEN) + return -1; + for (; digest[i] != 0; i++) + if (!isspace(digest[i])) + return -1; + return 1; + } else + return 1; +} + +/* true if the field exists and it is not empty */ +static inline int fieldok(struct scado_line *line, int i) { + return line->field[i] && line->end[i] != line->field[i]; +} + +/* some syntax sanity checks */ +static int scado_syntax_check(char *path, int lineno, struct scado_line *line) { + uint64_t capset; + int rv=0; + if (line->end[2] && line->end[2][0] == ':') { + fprintf(stderr,"%s line %d: extra trailing chars\n", path, lineno); + rv = -1; + } + if (fieldok(line, 0) && line->field[0][0] != '/') { + fprintf(stderr,"%s line %d: pathname is not absolute\n", path, lineno); + rv = -1; + } + if (fieldok(line, 0) && !fieldok(line, 1)) { + fprintf(stderr,"%s line %d: missing capability set\n", path, lineno); + rv = -1; + } + if (fieldok(line, 1) && scado_get_caps(line, &capset) < 0) { + fprintf(stderr,"%s line %d: wrong capability syntax\n", path, lineno); + rv = -1; + } + if (fieldok(line, 2) && scado_check_digest_syntax(line) < 0) { + fprintf(stderr,"%s line %d: wrong digest syntax\n", path, lineno); + rv = -1; + } + return rv; +} + +/* clean the path (parsing escape chars) in place */ +static void cleanpath(char *path) { + char *out=path; + while(*path != 0 && !isspace(*path)) { + if (path[0] == '\\' && path[1] != 0) path++; + *out++ = *path++; + } + *out=0; +} + +/* update a line */ +/* inpath == NULL -> add the digest it if is missing + inpath == "" -> update the digest in any case + inpath == "/....." (a valid path) -> update the digest if the path of this line matches */ +static void scado_update_line(FILE *fout, struct scado_line *line, char *inpath) { + if (line->field[2] != NULL) { + /* there is the digest field */ + size_t pathlen = line->end[0] - line->field[0]; + char path[pathlen+1]; + /* create a temporary copy of the path on the stack and clean it*/ + strncpy(path,line->field[0],pathlen); + path[pathlen]=0; + cleanpath(path); + int digestok=scado_check_digest_syntax(line); + if (digestok < 0) // do not update in case of syntax error + fprintf(fout, "%s\n", line->line); + else if (digestok == 0) { //missing digest -> fill in! + char digest[DIGESTSTRLEN + 1]; + int len = line->field[2] - line->line; + if (compute_digest(path, digest) < 0) + fprintf(fout, "%s\n", line->line); + else if (line->end[2] != 0) + fprintf(fout,"%*.*s%s %s\n",len,len,line->line,digest,line->end[2]); + else + fprintf(fout,"%*.*s%s\n",len,len,line->line,digest); + } else if (inpath && (*inpath == 0 || strcmp(path, inpath) == 0)) { /* if ALL or this path */ + char digest[DIGESTSTRLEN + 1]; + int len = line->field[2] - line->line; + if (compute_digest(path, digest) < 0) + fprintf(fout, "%s\n", line->line); + else + fprintf(fout,"%*.*s%s%s\n",len,len,line->line,digest,line->field[2]+DIGESTSTRLEN); + } else + fprintf(fout, "%s\n", line->line); + } else + fprintf(fout, "%s\n", line->line); +} + +/* copy the file from inpath to outpath, updating the digest where required: + path == NULL -> add missing digests + path == "" -> all +*/ +void scado_copy_update(char *inpath, char *outpath, char *path) { + FILE *fin = fopen(inpath, "r"); + mode_t oldmask; + FILE *fout; + char *line; + size_t len=0; + int lineno=0; + ssize_t n; + if (fin == NULL) + return; + oldmask = umask(027); + fout = fopen(outpath, "w"); + umask(oldmask); + if (fout == NULL) { + fclose(fin); + return; + } + while ((n = getline(&line, &len, fin)) > 0) { + struct scado_line scado_line; + line[n-1] = 0; + scado_line = scado_parse(line); + lineno++; + scado_syntax_check(outpath, lineno, &scado_line); + scado_update_line(fout, &scado_line, path); + } + fclose(fin); + fclose(fout); + free(line); +} + +/* scan the scado file whose pathname is inpath seeking for the line matching with 'path' + scado_path_getinfo returns: + -1 in case of error, 0 if the line is missing, 1 if the line has been found */ +/* if scado_path_getinfo succeeds, pcapset and digest are set to + the set of permitted capabilities and the hash digest, respectively. */ +int scado_path_getinfo(char *inpath, const char *path, uint64_t *pcapset, char *digest) { + FILE *fin = fopen(inpath, "r"); + char *line; + size_t len=0; + ssize_t n; + int lineno=0; + int rv = 0; + if (fin == NULL) + return -1; + while ((n = getline(&line, &len, fin)) > 0) { + struct scado_line scado_line; + line[n-1] = 0; + scado_line = scado_parse(line); + lineno++; + if (scado_path_match(path, &scado_line)) { + if (scado_syntax_check(inpath, lineno, &scado_line) == 0) { + if (scado_get_caps(&scado_line, pcapset) >= 0) { + if (scado_line.field[2]) { + int i; + for (i = 0; i < DIGESTSTRLEN ; i++) + digest[i] = tolower(scado_line.field[2][i]); + digest[i] = 0; + rv = 1; + } else { + digest[0] = 0; + rv = 1; + } + } + } + break; + } + } + fclose(fin); + free(line); + return rv; +} + +#if 0 +int main() { + char *line; + size_t len=0; + struct scado_line s; + int lineno=0; + while (1) { + ssize_t n=getline(&line, &len, stdin); + line[n-1]=0; + lineno++; + s=scado_parse(line); + printf("L %s\n",s.line); + printf("P %s|%s\n",s.field[0],s.end[0]); + printf("C %s|%s\n",s.field[1],s.end[1]); + printf("H %s|%s\n",s.field[2],s.end[2]); + uint64_t cap=0; + printf("%d ",scado_get_caps(&s,&cap)); + printf("%lx\n",cap); + scado_syntax_check(lineno, &s); + printf("NULL->");fflush(stdout); + scado_update_line(1, &s, NULL); + printf("ALL ->");fflush(stdout); + scado_update_line(1, &s, ""); + printf("bash ->");fflush(stdout); + scado_update_line(1, &s, "/bin/bash"); + } +} +#endif +#if 0 +int main(int argc, char *argv[]) { + scado_update(argv[1], argv[2], argv[3]); +} +#endif +#if 0 +int main(int argc, char *argv[]) { + char digest[DIGESTSTRLEN + 1]; + uint64_t capset; + if (scado_path_getinfo(argv[1], argv[2], &capset, digest)) { + printf("%lx %s\n",capset,digest); + } +} +#endif diff --git a/scado_parse.h b/scado_parse.h new file mode 100644 index 0000000..1a5529e --- /dev/null +++ b/scado_parse.h @@ -0,0 +1,17 @@ +#ifndef _SCADO_PARSE_H +#define _SCADO_PARSE_H + +#include + +/* copy file inpath to file outpath. + if path == NULL, add missing HASH digests + else if *path == 0, update all HASH digests + else update only the digest for path */ +void scado_copy_update(char *inpath, char *outpath, char *path); + +/* get info for file path. + if scado_path_getinfo returns 1 the path is authorized by scado, + pcapset and digest are the permitted set of capabilities and the digest respectively */ +int scado_path_getinfo(char *inpath, const char *path, uint64_t *pcapset, char *digest); + +#endif // _SCADO_PARSE.H diff --git a/set_ambient_cap.c b/set_ambient_cap.c index 6b4902f..b5fc57f 100644 --- a/set_ambient_cap.c +++ b/set_ambient_cap.c @@ -36,6 +36,9 @@ #define PR_CAP_AMBIENT_LOWER 3 #endif +/* set the ambient capabilities to match the bitmap capset. + the capability #k is active if and only if the (k+1)-th least significative bit in capset is 1. + (i.e. if and only if (capset & (1ULL << k)) is not zero. */ void set_ambient_cap(uint64_t capset) { cap_value_t cap; @@ -59,6 +62,7 @@ void set_ambient_cap(uint64_t capset) } } +/* turn cap_dac_read_search on and off to have "extra" powers only when needed */ void raise_cap_dac_read_search(void) { cap_value_t cap=CAP_DAC_READ_SEARCH; cap_t caps=cap_get_proc();