LWN.net Logo

audit logging hashes of exec'd binaries.

From:  Peter Moody <pmoody@google.com>
To:  linux-security-module@vger.kernel.org
Subject:  [RFC] audit logging hashes of exec'd binaries.
Date:  Fri, 18 May 2012 15:58:42 -0700
Message-ID:  <CALnj_=4kBTuCrvhdS1yLdSE6R1TfB0Qxu3EQqSvo62-hK6UtWA@mail.gmail.com>
Archive-link:  Article, Thread

For the purposes of tracking malware in an enterprise, I'm interested
in logging the hash of binaries as they are exec'd. The following
patch (mostly) adds this functionality.

When a binary is executed, gih_file_check is called which checks to
see if this inode has been exec'd before. If not, it's sha1 hash is
taken and logged to auditd along with its pid, ppid, uid, euid,
loginuid & path and the inode is stored the list of previously hashed
binaries. Subsequent exec's incur no extra hashing.

There's still some work to be done on this, eg finding a more
efficient way to search exec'd inodes, making sure there are no memory
leaks, etc. In the meantime, I'm interested in knowing if other
people/organizations are interested in this feature and also on my
approach to solving the problem. Are there any obvious issues that I'm
missing because I'm new at this, etc?

If code looks similar to IMA at all, that's because I borrowed a fair
bit of it directly from IMA. Since IMA already does the hashing I've
was looking for, I asked in Feb. if it would be possible to audit log
the calculated hashes. The result was that it didn't seem like
modifying IMA was the right approach.

My very unscientific (*) showed about a 1%-2% performance hit.

(*) the test was running 'make -j 4' of the kernel in a qemu instance
running on my desktop while I was using the desktop for regular
desktop-y stuff.

Cheers,
peter

Signed-off-by: Peter Moody <pmoody@google.com>
---
 fs/namei.c                          |    2 +
 include/linux/gih.h                 |   25 +++++
 include/linux/sched.h               |    8 ++
 security/integrity/Kconfig          |    1 +
 security/integrity/Makefile         |    2 +
 security/integrity/gih/Kconfig      |    6 +
 security/integrity/gih/Makefile     |    7 ++
 security/integrity/gih/gih.h        |   18 ++++
 security/integrity/gih/gih_crypto.c |   64 ++++++++++++
 security/integrity/gih/gih_fs.c     |  149 +++++++++++++++++++++++++++
 security/integrity/gih/gih_main.c   |  189 +++++++++++++++++++++++++++++++++++
 11 files changed, 471 insertions(+), 0 deletions(-)
 create mode 100644 include/linux/gih.h
 create mode 100644 security/integrity/gih/Kconfig
 create mode 100644 security/integrity/gih/Makefile
 create mode 100644 security/integrity/gih/gih.h
 create mode 100644 security/integrity/gih/gih_crypto.c
 create mode 100644 security/integrity/gih/gih_fs.c
 create mode 100644 security/integrity/gih/gih_main.c

diff --git a/fs/namei.c b/fs/namei.c
index c427919..fc208ef 100644
--- a/fs/namei.c
+++ b/fs/namei.c
@@ -23,6 +23,7 @@
 #include <linux/fsnotify.h>
 #include <linux/personality.h>
 #include <linux/security.h>
+#include <linux/gih.h>
 #include <linux/ima.h>
 #include <linux/syscalls.h>
 #include <linux/mount.h>
@@ -2336,6 +2337,7 @@ common:
 		goto exit;
 	filp = nameidata_to_filp(nd);
 	if (!IS_ERR(filp)) {
+		gih_file_check(filp, op->acc_mode);
 		error = ima_file_check(filp, op->acc_mode);
 		if (error) {
 			fput(filp);
diff --git a/include/linux/gih.h b/include/linux/gih.h
new file mode 100644
index 0000000..5d7120b
--- /dev/null
+++ b/include/linux/gih.h
@@ -0,0 +1,25 @@
+#ifndef _LINUX_GIH_H
+#define _LINUX_GIH_H
+
+#include <linux/fs.h>
+#include <linux/list.h>
+#include <linux/path.h>
+
+#ifdef CONFIG_GOOGLE_IMAGE_HASH
+
+/* saved hashes keyed on inode */
+struct gih_entry {
+	unsigned int ref;
+	struct inode *inode;
+	struct path path;
+	struct list_head cache_entries;
+};
+
+extern void gih_file_check(struct file *file, int mask);
+
+#else
+
+static inline void gih_file_check(struct file *file, int mask) { }
+
+#endif
+#endif
diff --git a/include/linux/sched.h b/include/linux/sched.h
index 81a173c..6f73279 100644
--- a/include/linux/sched.h
+++ b/include/linux/sched.h
@@ -93,6 +93,10 @@ struct sched_param {

 #include <asm/processor.h>

+#ifdef CONFIG_GOOGLE_IMAGE_HASH
+#include <crypto/sha.h>
+#endif
+
 struct exec_domain;
 struct futex_pi_state;
 struct robust_list_head;
@@ -1354,6 +1358,10 @@ struct task_struct {
 	pid_t pid;
 	pid_t tgid;

+#ifdef CONFIG_GOOGLE_IMAGE_HASH
+	/* md5/sha1 hash of the image */
+	u8 digest[SHA1_DIGEST_SIZE];
+#endif
 #ifdef CONFIG_CC_STACKPROTECTOR
 	/* Canary value for the -fstack-protector gcc feature */
 	unsigned long stack_canary;
diff --git a/security/integrity/Kconfig b/security/integrity/Kconfig
index 5bd1cc1..481b4a3 100644
--- a/security/integrity/Kconfig
+++ b/security/integrity/Kconfig
@@ -19,3 +19,4 @@ config INTEGRITY_SIGNATURE

 source security/integrity/ima/Kconfig
 source security/integrity/evm/Kconfig
+source security/integrity/gih/Kconfig
diff --git a/security/integrity/Makefile b/security/integrity/Makefile
index d43799c..16ed8c6 100644
--- a/security/integrity/Makefile
+++ b/security/integrity/Makefile
@@ -11,3 +11,5 @@ subdir-$(CONFIG_IMA)			+= ima
 obj-$(CONFIG_IMA)			+= ima/built-in.o
 subdir-$(CONFIG_EVM)			+= evm
 obj-$(CONFIG_EVM)			+= evm/built-in.o
+subdir-$(CONFIG_GOOGLE_IMAGE_HASH) += gih
+obj-$(CONFIG_GOOGLE_IMAGE_HASH) += gih/built-in.o
diff --git a/security/integrity/gih/Kconfig b/security/integrity/gih/Kconfig
new file mode 100644
index 0000000..701c298
--- /dev/null
+++ b/security/integrity/gih/Kconfig
@@ -0,0 +1,6 @@
+config GOOGLE_IMAGE_HASH
+	bool "binary image hashing"
+	def_bool y
+	depends on SECURITY
+	help
+	  store sha1 hash of binaries.
diff --git a/security/integrity/gih/Makefile b/security/integrity/gih/Makefile
new file mode 100644
index 0000000..e1fbb2b
--- /dev/null
+++ b/security/integrity/gih/Makefile
@@ -0,0 +1,7 @@
+#
+# Makefile for building mah own shit.
+#
+
+obj-$(CONFIG_GOOGLE_IMAGE_HASH) += gih.o
+
+gih-y := gih_fs.o gih_crypto.o gih_main.o
diff --git a/security/integrity/gih/gih.h b/security/integrity/gih/gih.h
new file mode 100644
index 0000000..6a981a2
--- /dev/null
+++ b/security/integrity/gih/gih.h
@@ -0,0 +1,18 @@
+#ifndef _GIH_H
+#define _GIH_H
+
+#include <linux/gih.h>
+#include <linux/audit.h>
+
+#define MAX_GIH_CACHE_ENTRIES 500
+
+extern int gih_enabled;
+extern int gih_initialized;
+extern int gih_cache_size;
+extern int count;
+
+int gih_calc_hash(struct file *, u8 *);
+int gih_init_fs(void);
+void gih_audit_msg(struct gih_entry *, int, const char *);
+
+#endif
diff --git a/security/integrity/gih/gih_crypto.c
b/security/integrity/gih/gih_crypto.c
new file mode 100644
index 0000000..2cc56da
--- /dev/null
+++ b/security/integrity/gih/gih_crypto.c
@@ -0,0 +1,64 @@
+#include <linux/crypto.h>
+#include <linux/scatterlist.h>
+
+#include "gih.h"
+
+static int init_desc(struct hash_desc *desc)
+{
+	int rc;
+	desc->tfm = crypto_alloc_hash("sha1", 0, CRYPTO_ALG_ASYNC);
+	if (IS_ERR(desc->tfm)) {
+		pr_info("image_hash: failed to load sha1 transform: %ld\n",
+				PTR_ERR(desc->tfm));
+		rc = PTR_ERR(desc->tfm);
+		return rc;
+	}
+	desc->flags = 0;
+	rc = crypto_hash_init(desc);
+	if (rc)
+		crypto_free_hash(desc->tfm);
+	return rc;
+}
+
+int gih_calc_hash(struct file *file, u8 *digest)
+{
+	struct hash_desc desc;
+	struct scatterlist sg[1];
+	loff_t i_size, offset = 0;
+	char *rbuf;
+	int rc;
+
+	rc = init_desc(&desc);
+	if (rc != 0)
+		return rc;
+
+	rbuf = kzalloc(PAGE_SIZE, GFP_KERNEL);
+	if (!rbuf) {
+		rc = -ENOMEM;
+		goto out;
+	}
+
+	i_size = i_size_read(file->f_dentry->d_inode);
+	while (offset < i_size) {
+		int rbuf_len;
+		rbuf_len = kernel_read(file, offset, rbuf, PAGE_SIZE);
+		if (rbuf_len < 0) {
+			rc = rbuf_len;
+			break;
+		}
+		if (rbuf_len == 0)
+			break;
+		offset += rbuf_len;
+		sg_init_one(sg, rbuf, rbuf_len);
+
+		rc = crypto_hash_update(&desc, sg, rbuf_len);
+		if (rc)
+			break;
+	}
+	kfree(rbuf);
+	if (!rc)
+		rc = crypto_hash_final(&desc, digest);
+out:
+	crypto_free_hash(desc.tfm);
+	return rc;
+}
diff --git a/security/integrity/gih/gih_fs.c b/security/integrity/gih/gih_fs.c
new file mode 100644
index 0000000..b06ca42
--- /dev/null
+++ b/security/integrity/gih/gih_fs.c
@@ -0,0 +1,149 @@
+#include <linux/fs.h>
+#include <linux/security.h>
+#include <linux/seq_file.h>
+#include <linux/uaccess.h>
+#include <linux/kernel.h>
+
+#include "gih.h"
+
+static struct dentry *gih_dir;
+static struct dentry *on_off_switch_d;
+static struct dentry *cache_size_d;
+int gih_cache_size;
+
+static ssize_t gih_on_off_switch_write(struct file *file, const char
__user *buf,
+									   size_t datalen, loff_t *ppos)
+{
+	ssize_t result;
+	char *data = NULL;
+	int val;
+
+	if (datalen >= PAGE_SIZE)
+		datalen = PAGE_SIZE -1;
+
+	result = -EINVAL;
+	if (*ppos != 0)
+		goto out;
+
+	result = -ENOMEM;
+	data = (char*)kmalloc(datalen + 1, GFP_KERNEL);
+	if (!data)
+		goto out;
+
+	*(data + datalen) = '\0';
+
+	result = -EFAULT;
+	if (copy_from_user(data, buf, datalen))
+		goto out;
+
+	result = datalen;
+	val = simple_strtol(data, NULL, 10);
+	if (val > 0)
+		val = 1;
+	else if (val < 0)
+		val = 0;
+
+	if (val == 1 && gih_enabled == 0) {
+		gih_audit_msg(NULL, AUDIT_CONFIG_CHANGE, "enabling gih");
+		gih_enabled = 1;
+	} else if (val == 0 && gih_enabled == 1) {
+		gih_audit_msg(NULL, AUDIT_CONFIG_CHANGE, "disabling gih");
+		gih_enabled = 0;
+	}
+
+out:
+	if (data)
+		kfree(data);
+	return result;
+}
+
+static int gih_on_off_switch_show(struct seq_file *m, void *v)
+{
+	seq_printf(m, "%d\n", gih_enabled);
+	return 0;
+}
+
+static int gih_on_off_switch_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, gih_on_off_switch_show, inode);
+}
+
+static const struct file_operations gih_on_off_switch_ops = {
+	.open = gih_on_off_switch_open,
+	.write = gih_on_off_switch_write,
+	.read = seq_read,
+	.release = single_release,
+	.llseek = generic_file_llseek,
+};
+
+static int gih_cache_size_show(struct seq_file *m, void *v)
+{
+	seq_printf(m, "%d\n", count);
+	return 0;
+}
+
+static ssize_t gih_cache_size_write(struct file *file, const char __user *buf,
+				    size_t datalen, loff_t *ppos)
+{
+	ssize_t result;
+	char *data = NULL;
+	int cache_size;
+
+	if (datalen >= PAGE_SIZE)
+		datalen = PAGE_SIZE -1;
+
+	result = -EINVAL;
+	if (*ppos != 0)
+		goto out;
+
+	result = -ENOMEM;
+	data = (char*)kmalloc(datalen + 1, GFP_KERNEL);
+	if (!data)
+		goto out;
+
+	*(data + datalen) = '\0';
+
+	result = -EFAULT;
+	if (copy_from_user(data, buf, datalen))
+		goto out;
+
+	result = datalen;
+
+	cache_size = simple_strtol(data, NULL, 0);
+	if (cache_size < MAX_GIH_CACHE_ENTRIES)
+		cache_size = MAX_GIH_CACHE_ENTRIES;
+	gih_cache_size = cache_size;
+
+out:
+	if (data)
+		kfree(data);
+	return result;
+}
+
+static int gih_cache_size_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, gih_cache_size_show, inode);
+}
+
+static const struct file_operations gih_cache_size_ops = {
+	.open = gih_cache_size_open,
+	.read = seq_read,
+	.write = gih_cache_size_write,
+	.release = single_release,
+	.llseek = generic_file_llseek,
+};
+
+int __init gih_init_fs(void)
+{
+	gih_dir = securityfs_create_dir("gih", NULL);
+	if (IS_ERR(gih_dir))
+		return -1;
+
+	on_off_switch_d = securityfs_create_file("toggle", S_IRUSR | S_IRGRP,
+						 gih_dir, NULL,
+						 &gih_on_off_switch_ops);
+	cache_size_d = securityfs_create_file("cache_size", S_IRUSR | S_IRGRP,
+					      gih_dir, NULL,
+					      &gih_cache_size_ops);
+	return 0;
+}
diff --git a/security/integrity/gih/gih_main.c
b/security/integrity/gih/gih_main.c
new file mode 100644
index 0000000..116956b
--- /dev/null
+++ b/security/integrity/gih/gih_main.c
@@ -0,0 +1,189 @@
+#include <linux/module.h>
+#include <linux/hash.h>
+#include <linux/crypto.h>
+#include <linux/scatterlist.h>
+#include <crypto/sha.h>
+#include <linux/seq_file.h>
+#include <linux/fs.h>
+#include <linux/mm.h>
+#include <linux/pid.h>
+#include <linux/sched.h>
+#include <linux/proc_fs.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/gih.h>
+#include <linux/list.h>
+#include <linux/security.h>
+
+#include "gih.h"
+
+static struct kmem_cache *gih_cache __read_mostly;
+static DEFINE_SPINLOCK(gih_cache_lock);
+static LIST_HEAD(cache_entries);
+
+int gih_initialized;
+int gih_enabled;
+int cache_size;
+int count;
+
+void gih_audit_msg(struct gih_entry *entry, int msgno, const char *msg)
+{
+	struct audit_buffer *ab;
+
+	if (!current->digest) {
+		return;
+	} else if (!entry) {
+		/* Messages about gih starting/stopping */
+		ab = audit_log_start(current->audit_context, GFP_KERNEL, msgno);
+		if (!ab)
+			return;
+		audit_log_format(ab, "%s", msg);
+		audit_log_task_context(ab);
+		audit_log_end(ab);
+	} else {
+		char *hash;
+		int i;
+		hash = (char*)kmalloc(PAGE_SIZE, GFP_TEMPORARY);
+		if (!hash)
+			goto out;
+		memset(hash, 0, PAGE_SIZE);
+
+		for (i = 0; i < SHA1_DIGEST_SIZE; i++) {
+			snprintf(hash + (i*2), 4, "%02x", current->digest[i]);
+		}
+
+		ab = audit_log_start(current->audit_context, GFP_KERNEL,
+				     AUDIT_INTEGRITY_RULE);
+		if (!ab)
+			goto out;
+		audit_log_format(ab, "pid=%d ppid=%d uid=%d euid=%d loginuid=%d ",
+				 current->pid, current->real_parent->pid,
+				 current->real_cred->uid, current->cred->uid,
+				 current->loginuid);
+		audit_log_d_path(ab, "path=", &entry->path);
+		audit_log_format(ab, " hash=%s", hash);
+		audit_log_task_context(ab);
+		audit_log_end(ab);
+	out:
+		if (hash)
+			kfree(hash);
+	}
+}
+
+static struct gih_entry *gih_inode_alloc(struct file *file, u8 *digest)
+{
+	struct gih_entry *entry;
+
+	entry = kmem_cache_alloc(gih_cache, GFP_NOFS);
+	if (!entry)
+		return NULL;
+	__iget(file->f_path.dentry->d_inode);
+	entry->inode = file->f_path.dentry->d_inode;
+	entry->path = file->f_path;
+	entry->ref = 1;
+	return entry;
+}
+
+static struct gih_entry *__gih_find_entry(struct inode *inode)
+{
+	struct gih_entry *entry = NULL;
+
+	list_for_each_entry(entry, &cache_entries, cache_entries)
+		if (entry->inode->i_ino == inode->i_ino) {
+			return entry;
+		}
+	return NULL;
+}
+
+static struct gih_entry *gih_find_entry(struct inode *inode)
+{
+	struct gih_entry *entry;
+	if (count < 1)
+		return NULL;
+
+	spin_lock(&gih_cache_lock);
+	entry = __gih_find_entry(inode);
+	spin_unlock(&gih_cache_lock);
+
+	return entry;
+}
+
+static void gih_add_entry(struct gih_entry *entry)
+{
+	spin_lock(&gih_cache_lock);
+	list_add(&(entry->cache_entries), &cache_entries);
+	count++;
+	spin_unlock(&gih_cache_lock);
+}
+
+void gih_file_check(struct file *file, int mask)
+{
+	if (!gih_initialized || !gih_enabled)
+		return;
+
+	if (mask & MAY_EXEC) {
+		struct gih_entry *entry;
+		struct inode *inode = file->f_path.dentry->d_inode;
+		entry = gih_find_entry(inode);
+		if (entry == NULL) {
+			u8 *digest;
+
+			digest = (u8*)kmalloc(SHA1_DIGEST_SIZE, GFP_TEMPORARY);
+			if (!digest)
+				return;
+			memset(digest, 0, SHA1_DIGEST_SIZE);
+
+			gih_calc_hash(file, digest);
+
+			entry = gih_inode_alloc(file, digest);
+			if (!entry) {
+				printk(KERN_ERR "error allocating new entry");
+				kfree(digest);
+				return;
+			}
+
+			gih_add_entry(entry);
+			memcpy(&current->digest, digest, SHA1_DIGEST_SIZE);
+			gih_audit_msg(entry, AUDIT_INTEGRITY_RULE, NULL);
+		} else {
+			entry->ref++;
+		}
+	}
+}
+
+EXPORT_SYMBOL_GPL(gih_file_check);
+
+static int __exit cleanup_image_hash(void)
+{
+	struct gih_entry *entry = NULL;
+
+	spin_lock(&gih_cache_lock);
+	list_for_each_entry(entry, &cache_entries, cache_entries) {
+		kfree(entry);
+	}
+	spin_unlock(&gih_cache_lock);
+	count = 0;
+	gih_enabled = 0;
+	gih_initialized = 0;
+
+	return 0;
+}
+
+static int __init init_image_hash(void)
+{
+	gih_cache = kmem_cache_create("gih_cache", sizeof(struct gih_entry), 0,
+				      SLAB_PANIC, NULL);
+	count = 0;
+	gih_enabled = 1;
+	gih_init_fs();
+	gih_initialized = 1;
+	gih_cache_size = MAX_GIH_CACHE_ENTRIES;
+
+	printk(KERN_INFO "gih enabled\n");
+	return 0;
+}
+
+__initcall(init_image_hash);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Cryptographic image hashing");
-- 
1.7.7.3


-- 
Peter Moody      Google    1.650.253.7306
Security Engineer  pgp:0xC3410038
--
To unsubscribe from this list: send the line "unsubscribe linux-security-module" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html


Copyright © 2012, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds