diff options
author | Fabian Groffen <grobian@gentoo.org> | 2018-05-18 12:12:40 +0200 |
---|---|---|
committer | Fabian Groffen <grobian@gentoo.org> | 2018-05-18 12:12:40 +0200 |
commit | 04b4c6834fd83bf0329198c56894bd7dad6f7a6a (patch) | |
tree | c3d0adcec466650bbc55f312287b9caa243c4565 | |
parent | mkman.py: add additional authors to main authors section (diff) | |
download | portage-utils-04b4c6834fd83bf0329198c56894bd7dad6f7a6a.tar.gz portage-utils-04b4c6834fd83bf0329198c56894bd7dad6f7a6a.tar.bz2 portage-utils-04b4c6834fd83bf0329198c56894bd7dad6f7a6a.zip |
qtegrity: new applet by Sam Besselink for use with IMA, bug #619988
Bug: https://bugs.gentoo.org/619988
-rw-r--r-- | applets.h | 7 | ||||
-rw-r--r-- | man/include/qtegrity-70-relevant-files.include | 9 | ||||
-rw-r--r-- | man/include/qtegrity-authors.include | 1 | ||||
-rw-r--r-- | man/include/qtegrity.desc | 8 | ||||
-rw-r--r-- | man/qtegrity.1 | 81 | ||||
-rw-r--r-- | qtegrity.c | 509 |
6 files changed, 614 insertions, 1 deletions
@@ -31,6 +31,7 @@ DECLARE_APPLET(qatom) DECLARE_APPLET(qmerge) DECLARE_APPLET(qcache) DECLARE_APPLET(qglsa) /* disable */ +DECLARE_APPLET(qtegrity) #undef DECLARE_APPLET #define DEFINE_APPLET_STUB(applet) \ @@ -62,8 +63,9 @@ static const struct applet_t { {"qtbz2", qtbz2_main, "<misc args>", "manipulate tbz2 packages"}, {"quse", quse_main, "<useflag>", "find pkgs using useflags"}, {"qxpak", qxpak_main, "<misc args>", "manipulate xpak archives"}, + {"qtegrity", qtegrity_main, "<misc args>", "verify files with IMA"}, - /* aliases for equery capatability */ + /* aliases for equery compatibility */ {"belongs", qfile_main, NULL, NULL}, /*"changes"*/ {"check", qcheck_main, NULL, NULL}, @@ -82,6 +84,9 @@ static const struct applet_t { {"uickpkg", qpkg_main, NULL, NULL}, /* {"glsa", qglsa_main, NULL, NULL}, */ + /* alias for qtegrity */ + {"integrity", qtegrity_main, NULL, NULL}, + {NULL, NULL, NULL, NULL} }; diff --git a/man/include/qtegrity-70-relevant-files.include b/man/include/qtegrity-70-relevant-files.include new file mode 100644 index 00000000..742658fe --- /dev/null +++ b/man/include/qtegrity-70-relevant-files.include @@ -0,0 +1,9 @@ +.SH RELEVANT FILES +.PP +Central list of known good digests +.nf\fI + /var/db/QTEGRITY\fi +.PP +Linux kernel's recorded digests +.nf\fI + /sys/kernel/security/ima/ascii_runtime_measurements\fi diff --git a/man/include/qtegrity-authors.include b/man/include/qtegrity-authors.include new file mode 100644 index 00000000..160ea6aa --- /dev/null +++ b/man/include/qtegrity-authors.include @@ -0,0 +1 @@ +Sam Besselink diff --git a/man/include/qtegrity.desc b/man/include/qtegrity.desc new file mode 100644 index 00000000..5f9029b5 --- /dev/null +++ b/man/include/qtegrity.desc @@ -0,0 +1,8 @@ +The default behavior of \fBqtegrity\fP is to verify digests of performed +executables to a list of known good digests. This requires an IMA-enabled +linux kernel, which records digests of performed executables and exports them +through securityfs. Using \fB\-\-ignore-non-existent\fP suppresses messages +about recorded files that can't be accessed (assuming they got removed). +By using \fB\-\-add\fP, the program behaves differently. No verification is +performed, instead a digest is made of the provided file and appended to +the list of known good digests. diff --git a/man/qtegrity.1 b/man/qtegrity.1 new file mode 100644 index 00000000..76ed7314 --- /dev/null +++ b/man/qtegrity.1 @@ -0,0 +1,81 @@ +.\" generated by mkman.py, please do NOT edit! +.TH qtegrity "1" "May 2018" "Gentoo Foundation" "qtegrity" +.SH NAME +qtegrity \- verify files with IMA +.SH SYNOPSIS +.B qtegrity +\fI[opts] <misc args>\fR +.SH DESCRIPTION +The default behavior of \fBqtegrity\fP is to verify digests of performed +executables to a list of known good digests. This requires an IMA-enabled +linux kernel, which records digests of performed executables and exports them +through securityfs. Using \fB\-\-ignore-non-existent\fP suppresses messages +about recorded files that can't be accessed (assuming they got removed). +By using \fB\-\-add\fP, the program behaves differently. No verification is +performed, instead a digest is made of the provided file and appended to +the list of known good digests. +.SH OPTIONS +.TP +\fB\-a\fR \fI<arg>\fR, \fB\-\-add\fR \fI<arg>\fR +Add file to store of known-good digests. +.TP +\fB\-i\fR, \fB\-\-ignore\-non\-existent\fR +Be silent if recorded file no longer exists. +.TP +\fB\-s\fR, \fB\-\-show\-matches\fR +Show recorded digests that match with known-good digests. +.TP +\fB\-\-root\fR \fI<arg>\fR +Set the ROOT env var. +.TP +\fB\-v\fR, \fB\-\-verbose\fR +Make a lot of noise. +.TP +\fB\-q\fR, \fB\-\-quiet\fR +Tighter output; suppress warnings. +.TP +\fB\-C\fR, \fB\-\-nocolor\fR +Don't output color. +.TP +\fB\-h\fR, \fB\-\-help\fR +Print this help and exit. +.TP +\fB\-V\fR, \fB\-\-version\fR +Print version and exit. +.SH RELEVANT FILES +.PP +Central list of known good digests +.nf\fI + /var/db/QTEGRITY\fi +.PP +Linux kernel's recorded digests +.nf\fI + /sys/kernel/security/ima/ascii_runtime_measurements\fi +.SH "REPORTING BUGS" +Please report bugs via http://bugs.gentoo.org/ +.br +Product: Portage Development; Component: Tools, Assignee: +portage-utils@gentoo.org +.SH AUTHORS +.nf +Ned Ludd <solar@gentoo.org> +Mike Frysinger <vapier@gentoo.org> +Sam Besselink +.fi +.SH "SEE ALSO" +.BR q (1), +.BR qatom (1), +.BR qcache (1), +.BR qcheck (1), +.BR qdepends (1), +.BR qfile (1), +.BR qgrep (1), +.BR qlist (1), +.BR qlop (1), +.BR qmerge (1), +.BR qpkg (1), +.BR qsearch (1), +.BR qsize (1), +.BR qtbz2 (1), +.BR quse (1), +.BR qxpak (1) diff --git a/qtegrity.c b/qtegrity.c new file mode 100644 index 00000000..8af5b08f --- /dev/null +++ b/qtegrity.c @@ -0,0 +1,509 @@ +/* + * Copyright 2005-2018 Gentoo Foundation + * Distributed under the terms of the GNU General Public License v2 + * + * Copyright 2005-2010 Ned Ludd - <solar@gentoo.org> + * Copyright 2005-2014 Mike Frysinger - <vapier@gentoo.org> + * Copyright 2017-2018 Sam Besselink + */ + +#ifdef APPLET_qtegrity + +#define QTEGRITY_FLAGS "a:is" COMMON_FLAGS +static struct option const qtegrity_long_opts[] = { + {"add", a_argument, NULL, 'a'}, + {"ignore-non-existent", no_argument, NULL, 'i'}, + {"show-matches", no_argument, NULL, 's'}, +/* TODO, add this functionality + {"convert", a_argument, NULL, 'c'} +*/ + COMMON_LONG_OPTS +}; +static const char * const qtegrity_opts_help[] = { + "Add file to store of known-good digests", + "Be silent if recorded file no longer exists", + "Show recorded digests that match with known-good digests", +/* TODO + "Convert known good digests to different hash function", +*/ + COMMON_OPTS_HELP +}; +#define qtegrity_usage(ret) usage(ret, QTEGRITY_FLAGS, qtegrity_long_opts, qtegrity_opts_help, NULL, lookup_applet_idx("qtegrity")) + +struct qtegrity_opt_state { + bool ima; + bool add; + char* add_file; + bool ignore_non_exist; + bool show_matches; +/* TODO + bool convert; +*/ +}; + +#define FILE_SUCCESS 1 +#define FILE_EMPTY 2 +#define FILE_RELATIVE 3 + +#define SHA1_DIGEST_LENGTH 40 +#define SHA256_PREFIX_LENGTH 8 +#define SHA256_DIGEST_LENGTH 64 +#define SHA256_LENGTH (SHA256_PREFIX_LENGTH + SHA256_DIGEST_LENGTH) +#define SHA512_DIGEST_LENGTH 128 + +static void external_check_sha(char * ret_digest, char * filepath, char * algo) { + size_t size_digest = 1; + + if (strcmp(algo, "sha256") == 0) { + size_digest = 64; + } else if (strcmp(algo, "sha512") == 0) { + size_digest = 128; + } + + if ((strcmp(algo, "sha256") != 0) && (strcmp(algo, "sha512") != 0)) { + return; + } + + char cmd[11]; + snprintf(cmd, 10, "%ssum", algo); + + int pipefd[2]; + pid_t pid; + + if (pipe(pipefd) == -1) { + perror("Couldn't create pipe to shasum\n"); + exit(1); + } + if ((pid = fork()) == -1) { + perror("Couldn't fork to shasum\n"); + exit(1); + } + if (pid == 0) + { + /* Child. Redirect stdout and stderr to pipe, replace execution + * environment */ + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + dup2(pipefd[1], STDERR_FILENO); + execlp(cmd, cmd, filepath, NULL); + perror("Executing shasum failed\n"); + exit(1); + } + + /* Only parent gets here. Listen to pipe */ + close(pipefd[1]); + FILE* output = fdopen(pipefd[0], "r"); + if (output == NULL) { + printf("Failed to run command '%s'\n", cmd); + exit(1); + } + + /* Read pipe line for line */ + while (fgets(ret_digest, size_digest+1, output)) + { + if (strlen(ret_digest) == 64) /* Found what we need, can stop */ + { + kill(pid, SIGKILL); + break; + } + } + + pclose(output); + return; +} + +static void get_fname_from_line(char * line, char **ret, int digest_size, int offset) +{ + size_t dlenstr = strlen(line); + char *p; + /* Skip first 123 chars to get to file depends on digest_func in IMA */ + size_t skip = ((digest_size == SHA256_DIGEST_LENGTH) || + (digest_size == SHA512_DIGEST_LENGTH)) ? + digest_size+offset+8 : digest_size+offset+6; + + if (dlenstr > skip) { /* assume file is at least two chars long */ + int segment_size = dlenstr - skip - 1; + p = xmalloc(segment_size+1); + memcpy(p, line + skip, segment_size); + p[segment_size] = '\0'; + } else { + /* E.g. digest used wrong hash algo, or malformed input */ + p = NULL; + } + + *ret = p; +} + +static void get_digest_from_line(char * line, char * ret, int digest_size, int offset) +{ + size_t dlenstr = strlen(line); + /* Skip first chars to get to digest depends on digest_func in IMA */ + int skip = ((digest_size == SHA256_DIGEST_LENGTH) || + (digest_size == SHA512_DIGEST_LENGTH)) ? + offset+8 : offset+6; + + if (dlenstr > (digest_size+skip+1)) { + memcpy(ret, line+skip, digest_size); + ret[digest_size] = '\0'; + } +} + +static void get_known_good_digest(const char * fn_store, char * recorded_fname, char * ret, int recorded_digest_size) +{ + /* Open file with known good hashes */ + int fd_store; + FILE *fp_store; + + fd_store = open(fn_store, O_RDONLY|O_CLOEXEC, 0); + if (fd_store == -1) { + warnp("unable to open(%s)", fn_store); + exit(0); + } + if ((fp_store = fdopen(fd_store, "r")) == NULL) { + warnp("unable to fopen(%s, r)", fn_store); + close(fd_store); + exit(0); + } + + char *buffered_line, *line, *fname; + size_t linelen; + + /* Iterate over lines in known-good-hashes-file; per line: if fname + * matches, grab hash. */ + buffered_line = line = fname = NULL; + while (getline(&line, &linelen, fp_store) != -1) { + free(buffered_line); + buffered_line = xstrdup(line); + + get_fname_from_line(line, &fname, recorded_digest_size, 15); + + if (fname == NULL) { + /* probably line without digest (e.g. symlink) */ + continue; + } + + if (strcmp(recorded_fname, fname) == 0) { + get_digest_from_line(line, ret, recorded_digest_size, 9); + + free(fname); + break; + } + + free(fname); + } + + free(line); + free(buffered_line); + + close(fd_store); + fclose(fp_store); +} + +static int get_size_digest(char * line) +{ + int ret = 0; + + char *pfound; + /* find colon; it is boundary between end of hash func & begin of + * digest */ + pfound = strchr(line, ':'); + if (pfound != NULL) { + int dpfound = pfound - line; + int cutoff_prefix = 0; + + if (dpfound == 55 || dpfound == 6) { + ret = SHA1_DIGEST_LENGTH; + } else if (dpfound == 57) { + cutoff_prefix = 51; + } else if (dpfound == 8) { + cutoff_prefix = 0; + } + + int dsegment = dpfound - cutoff_prefix; + + char *line_segment; + line_segment = xmalloc(dsegment + 1); + /* chop off the first chars to get to the hash func */ + memcpy(line_segment, line + cutoff_prefix, dsegment); + line_segment[dsegment] = '\0'; + + /* If line segment equals name of hash func, then return + * relevant const. */ + if (strcmp(line_segment, "sha512") == 0) { + ret = SHA512_DIGEST_LENGTH; + } else if (strcmp(line_segment, "sha256") == 0) { + ret = SHA256_DIGEST_LENGTH; + } else { + printf("Expected sha algo, got %s", line_segment); + } + + free(line_segment); + } + + return ret; +} + +static int check_file(char * filename) +{ + /* TODO, this is 4096 too low, because this variable also holds + * path; for linux path is max 4096 chars */ + if (strlen(filename) > 255) + err("Filename too long"); + + if (filename[0] != '/') { + return FILE_RELATIVE; + } + + return FILE_SUCCESS; +} + +int qtegrity_main(int argc, char **argv) +{ + int i; + + struct qtegrity_opt_state state = { + .ima = true, + .add = false, + .ignore_non_exist = false, + .show_matches = false, +/* TODO + .convert = false; +*/ + }; + + while ((i = GETOPT_LONG(QTEGRITY, qtegrity, "")) != -1) { + switch (i) { + COMMON_GETOPTS_CASES(qtegrity) + case 'a': + state.ima = false; + state.add = true; + if (check_file(optarg) == FILE_SUCCESS) { + state.add_file = xstrdup(optarg); + } else { + err("Expected absolute file as argument, got '%s'", optarg); + } + break; + case 'i': state.ignore_non_exist = true; break; + case 's': state.show_matches = true; break; + } + } + + if (state.ima) { + const char *fn_ima = + "/sys/kernel/security/ima/ascii_runtime_measurements"; + int fd_ima; + FILE *fp_ima; + struct stat st; + + fd_ima = open(fn_ima, O_RDONLY|O_CLOEXEC, 0); + if (fd_ima == -1) { + /* TODO, shouldn't we explicitly remind user IMA/securityfs + * is needed? */ + warnp("Unable to open(%s)", fn_ima); + exit(0); + } + if ((fp_ima = fdopen(fd_ima, "r")) == NULL) { + warnp("Unable to fopen(%s, r)", fn_ima); + close(fd_ima); + exit(0); + } + + char *buffered_line, *line, *recorded_fname; + int recorded_digest_size = 0; + size_t linelen; + + /* Iterate over IMA file, grab fname and digest, get known good + * digest for fname and compare */ + buffered_line = line = recorded_fname = NULL; + while (getline(&line, &linelen, fp_ima) != -1) { + char *recorded_digest; + char *digest; + + free(buffered_line); + buffered_line = xstrdup(line); + + if (buffered_line[0] != '1' || buffered_line[1] != '0') + continue; + + recorded_digest_size = get_size_digest(buffered_line); + recorded_digest = xmalloc(recorded_digest_size+1); + recorded_digest[0] = '\0'; + + /* grab fname from IMA file line */ + get_fname_from_line(buffered_line, &recorded_fname, + recorded_digest_size, 51); + /* grab digest from IMA file line, @TODO, check whether + * digest == 000etc */ + get_digest_from_line(buffered_line, recorded_digest, + recorded_digest_size, 50); + + if (recorded_fname == NULL || recorded_digest == NULL) { + printf("Empty recorded filename: %s\n", line); + + if (recorded_fname != NULL) + free(recorded_fname); + + if (recorded_digest != NULL) + free(recorded_digest); + + continue; + } + + if (check_file(recorded_fname) == FILE_RELATIVE) { + printf("Seems like a kernel process: %s\n", recorded_fname); + + free(recorded_fname); + free(recorded_digest); + continue; + } + + if (stat(recorded_fname, &st) < 0) { + if (!state.ignore_non_exist) + printf("Couldn't access recorded file '%s'\n", + recorded_fname); + + free(recorded_fname); + free(recorded_digest); + continue; + } + + if (!(st.st_mode & S_IXUSR || + st.st_mode & S_IXGRP || + st.st_mode & S_IXOTH)) + { + free(recorded_fname); + free(recorded_digest); + continue; + } + + digest = xmalloc(recorded_digest_size+1); + digest[0] = '\0'; + + /* first try custom known good digests for fname */ + get_known_good_digest("/var/db/QTEGRITY_custom", + recorded_fname, digest, recorded_digest_size); + + if (digest[0] == '\0') { + digest[0] = '\0'; + /* then try from OS source */ + get_known_good_digest("/var/db/QTEGRITY", + recorded_fname, digest, recorded_digest_size); + + if (digest[0] == '\0') { + printf("No digest found for: %s\n", line); + + free(recorded_fname); + free(recorded_digest); + free(digest); + continue; + } + } + + if (strcmp(recorded_digest, digest) != 0) { + printf("Digest didn't match for %s\n", recorded_fname); + printf("Known-good: '%s'...\nRecorded: '%s'\n\n", + digest, recorded_digest); + } else if (state.show_matches) { + printf("Success! Digest matched for %s\n", recorded_fname); + } + + free(recorded_fname); + free(recorded_digest); + free(digest); + } + + free(line); + free(buffered_line); + + close(fd_ima); + fclose(fp_ima); + } else if (state.add) { + /* Add a single executable file+digest to the custom digest store */ + const char *fn_qtegrity_custom = "/var/db/QTEGRITY_custom"; + int fd_qtegrity_custom; + FILE *fp_qtegrity_custom; + struct stat st; + int flush_status; + + fd_qtegrity_custom = + open(fn_qtegrity_custom, O_RDWR|O_CREAT|O_CLOEXEC, 0); + if (fd_qtegrity_custom == -1) { + warnp("Unable to open(%s)", fn_qtegrity_custom); + exit(0); + } + if ((fp_qtegrity_custom = fdopen(fd_qtegrity_custom, "w+")) == NULL) { + warnp("Unable to fopen(%s, r)", fn_qtegrity_custom); + close(fd_qtegrity_custom); + exit(0); + } + + printf("Adding %s to %s\n", state.add_file, fn_qtegrity_custom); + + if (stat(state.add_file, &st) < 0) + err("Couldn't access file '%s'\n", state.add_file); + + if (!(st.st_mode & S_IXUSR || + st.st_mode & S_IXGRP || + st.st_mode & S_IXOTH)) + err("File '%s' is not executable\n", state.add_file); + + /* add digest */ + char *hash_algo = (char *)"sha256"; + char *file_digest; + file_digest = xmalloc(SHA256_DIGEST_LENGTH+1); + file_digest[0] = '\0'; + external_check_sha(file_digest, state.add_file, hash_algo); + + /* Iterate over lines; if fname matches, exit-loop */ + char *line, *fname; + size_t linelen; + int recorded_digest_size = 0; + int skip = 0; + line = fname = NULL; + while (getline(&line, &linelen, fp_qtegrity_custom) != -1) { + recorded_digest_size = get_size_digest(line); + get_fname_from_line(line, &fname, recorded_digest_size, 5); + + /* probably line without digest (e.g. symlink) */ + if (fname == NULL) + continue; + + if (strcmp(state.add_file, fname) == 0) { + printf("Executable already recorded, " + "replacing digest with %s\n", file_digest); + skip = ((recorded_digest_size == SHA256_DIGEST_LENGTH) || + (recorded_digest_size == SHA512_DIGEST_LENGTH)) ? + recorded_digest_size+6+8 : recorded_digest_size+6+6; + fseek(fp_qtegrity_custom, -skip-strlen(fname), SEEK_CUR); + free(fname); + break; + } + + free(fname); + } + + free(line); + + fputs(hash_algo, fp_qtegrity_custom); + fputs(":", fp_qtegrity_custom); + fputs(file_digest, fp_qtegrity_custom); + fputs(" file:", fp_qtegrity_custom); + fputs(state.add_file, fp_qtegrity_custom); + fputs("\n", fp_qtegrity_custom); + + flush_status = fflush(fp_qtegrity_custom); + if (flush_status != 0) + puts("Error flushing stream!"); + + free(file_digest); + } + + if (state.add) + free(state.add_file); + + return EXIT_SUCCESS; +} + +#else +DEFINE_APPLET_STUB(qtegrity) +#endif |