aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFabian Groffen <grobian@gentoo.org>2018-05-18 12:12:40 +0200
committerFabian Groffen <grobian@gentoo.org>2018-05-18 12:12:40 +0200
commit04b4c6834fd83bf0329198c56894bd7dad6f7a6a (patch)
treec3d0adcec466650bbc55f312287b9caa243c4565
parentmkman.py: add additional authors to main authors section (diff)
downloadportage-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.h7
-rw-r--r--man/include/qtegrity-70-relevant-files.include9
-rw-r--r--man/include/qtegrity-authors.include1
-rw-r--r--man/include/qtegrity.desc8
-rw-r--r--man/qtegrity.181
-rw-r--r--qtegrity.c509
6 files changed, 614 insertions, 1 deletions
diff --git a/applets.h b/applets.h
index 93146c6..daf7047 100644
--- a/applets.h
+++ b/applets.h
@@ -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 0000000..742658f
--- /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 0000000..160ea6a
--- /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 0000000..5f9029b
--- /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 0000000..76ed731
--- /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 0000000..8af5b08
--- /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