diff --git a/security/brute/brute.c b/security/brute/brute.c
index 870db55332d4..38e5e050964a 100644
--- a/security/brute/brute.c
+++ b/security/brute/brute.c
@@ -3,15 +3,25 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <asm/current.h>
+#include <asm/rwonce.h>
+#include <asm/siginfo.h>
+#include <asm/signal.h>
+#include <linux/binfmts.h>
#include <linux/bug.h>
#include <linux/compiler.h>
+#include <linux/cred.h>
+#include <linux/dcache.h>
#include <linux/errno.h>
+#include <linux/fs.h>
#include <linux/gfp.h>
+#include <linux/if.h>
#include <linux/init.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/lsm_hooks.h>
#include <linux/math64.h>
+#include <linux/netdevice.h>
+#include <linux/path.h>
#include <linux/printk.h>
#include <linux/refcount.h>
#include <linux/rwlock.h>
@@ -19,9 +29,35 @@
#include <linux/sched.h>
#include <linux/sched/signal.h>
#include <linux/sched/task.h>
+#include <linux/signal.h>
+#include <linux/skbuff.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
+#include <linux/stat.h>
#include <linux/types.h>
+#include <linux/uidgid.h>
+
+/**
+ * struct brute_cred - Saved credentials.
+ * @uid: Real UID of the task.
+ * @gid: Real GID of the task.
+ * @suid: Saved UID of the task.
+ * @sgid: Saved GID of the task.
+ * @euid: Effective UID of the task.
+ * @egid: Effective GID of the task.
+ * @fsuid: UID for VFS ops.
+ * @fsgid: GID for VFS ops.
+ */
+struct brute_cred {
+ kuid_t uid;
+ kgid_t gid;
+ kuid_t suid;
+ kgid_t sgid;
+ kuid_t euid;
+ kgid_t egid;
+ kuid_t fsuid;
+ kgid_t fsgid;
+};
/**
* struct brute_stats - Fork brute force attack statistics.@@ -30,6 +66,9 @@
* @faults: Number of crashes.
* @jiffies: Last crash timestamp.
* @period: Crash period's moving average.
+ * @saved_cred: Saved credentials.
+ * @network: Network activity flag.
+ * @bounds_crossed: Privilege bounds crossed flag.
*
* This structure holds the statistical data shared by all the fork hierarchy
* processes.
@@ -40,6 +79,9 @@ struct brute_stats {
unsigned char faults;
u64 jiffies;
u64 period;
+ struct brute_cred saved_cred;
+ unsigned char network : 1;
+ unsigned char bounds_crossed : 1;
};
/*@@ -71,18 +113,25 @@ static inline struct brute_stats **brute_stats_ptr(struct task_struct *task)
/**
* brute_new_stats() - Allocate a new statistics structure.
+ * @network_to_local: Network activity followed by a fork or execve system call.
+ * @is_setid: The executable file has the setid flags set.
*
* If the allocation is successful the reference counter is set to one to
* indicate that there will be one task that points to this structure. Also, the
* last crash timestamp is set to now. This way, it is possible to compute the
* application crash period at the first fault.
*
+ * Moreover, the credentials of the current task are saved. Also, the network
+ * and bounds_crossed flags are set based on the network_to_local and is_setid
+ * parameters.
+ *
* Return: NULL if the allocation fails. A pointer to the new allocated
* statistics structure if it success.
*/
-static struct brute_stats *brute_new_stats(void)
+static struct brute_stats *brute_new_stats(bool network_to_local, bool is_setid)
{
struct brute_stats *stats;
+ const struct cred *cred = current_cred();
stats = kmalloc(sizeof(struct brute_stats), GFP_ATOMIC);
if (!stats)@@ -93,6 +142,16 @@ static struct brute_stats *brute_new_stats(void)
stats->faults = 0;
stats->jiffies = get_jiffies_64();
stats->period = 0;
+ stats->saved_cred.uid = cred->uid;
+ stats->saved_cred.gid = cred->gid;
+ stats->saved_cred.suid = cred->suid;
+ stats->saved_cred.sgid = cred->sgid;
+ stats->saved_cred.euid = cred->euid;
+ stats->saved_cred.egid = cred->egid;
+ stats->saved_cred.fsuid = cred->fsuid;
+ stats->saved_cred.fsgid = cred->fsgid;
+ stats->network = network_to_local;
+ stats->bounds_crossed = network_to_local || is_setid;
return stats;
}
@@ -137,6 +196,10 @@ static void brute_share_stats(struct brute_stats *src,
* this task and the new one being allocated. Otherwise, share the statistics
* that the current task already has.
*
+ * Also, if the shared statistics indicate a previous network activity, the
+ * bounds_crossed flag must be set to show that a network-to-local privilege
+ * boundary has been crossed.
+ *
* It's mandatory to disable interrupts before acquiring brute_stats_ptr_lock
* and brute_stats::lock since the task_free hook can be called from an IRQ
* context during the execution of the task_alloc hook.
@@ -155,11 +218,14 @@ static int brute_task_alloc(struct task_struct *task, unsigned long clone_flags)
if (likely(*p_stats)) {
brute_share_stats(*p_stats, stats);
+ spin_lock(&(*stats)->lock);
+ (*stats)->bounds_crossed |= (*stats)->network;
+ spin_unlock(&(*stats)->lock);
write_unlock_irqrestore(&brute_stats_ptr_lock, flags);
return 0;
}
- *stats = brute_new_stats();
+ *stats = brute_new_stats(false, false);
if (!*stats) {
write_unlock_irqrestore(&brute_stats_ptr_lock, flags);
return -ENOMEM;@@ -170,6 +236,61 @@ static int brute_task_alloc(struct task_struct *task, unsigned long clone_flags)
return 0;
}
+/**
+ * brute_is_setid() - Test if the executable file has the setid flags set.
+ * @bprm: Points to the linux_binprm structure.
+ *
+ * Return: True if the executable file has the setid flags set. False otherwise.
+ */
+static bool brute_is_setid(const struct linux_binprm *bprm)
+{
+ struct file *file = bprm->file;
+ struct inode *inode;
+ umode_t mode;
+
+ if (!file)
+ return false;
+
+ inode = file->f_path.dentry->d_inode;
+ mode = inode->i_mode;
+
+ return !!(mode & (S_ISUID | S_ISGID));
+}
+
+/**
+ * brute_reset_stats() - Reset the statistical data.
+ * @stats: Statistics to be reset.
+ * @is_setid: The executable file has the setid flags set.
+ *
+ * Reset the faults and period and set the last crash timestamp to now. This
+ * way, it is possible to compute the application crash period at the next
+ * fault. Also, save the credentials of the current task and update the
+ * bounds_crossed flag based on a previous network activity and the is_setid
+ * parameter.
+ *
+ * The statistics to be reset cannot be NULL.
+ *
+ * Context: Must be called with interrupts disabled and brute_stats_ptr_lock
+ * and brute_stats::lock held.
+ */
+static void brute_reset_stats(struct brute_stats *stats, bool is_setid)
+{
+ const struct cred *cred = current_cred();
+
+ stats->faults = 0;
+ stats->jiffies = get_jiffies_64();
+ stats->period = 0;
+ stats->saved_cred.uid = cred->uid;
+ stats->saved_cred.gid = cred->gid;
+ stats->saved_cred.suid = cred->suid;
+ stats->saved_cred.sgid = cred->sgid;
+ stats->saved_cred.euid = cred->euid;
+ stats->saved_cred.egid = cred->egid;
+ stats->saved_cred.fsuid = cred->fsuid;
+ stats->saved_cred.fsgid = cred->fsgid;
+ stats->bounds_crossed = stats->network || is_setid;
+}
+
/**
* brute_task_execve() - Target for the bprm_committing_creds hook.
* @bprm: Points to the linux_binprm structure.@@ -188,6 +309,11 @@ static int brute_task_alloc(struct task_struct *task, unsigned long clone_flags)
* only one task (the task that calls the execve function) points to the data.
* In this case, the previous allocation is used but the statistics are reset.
*
+ * Also, if the statistics of the process that calls the execve system call
+ * indicate a previous network activity or the executable file has the setid
+ * flags set, the bounds_crossed flag must be set to show that a network to
+ * local privilege boundary or setid boundary has been crossed respectively.
+ *
* It's mandatory to disable interrupts before acquiring brute_stats_ptr_lock
* and brute_stats::lock since the task_free hook can be called from an IRQ
* context during the execution of the bprm_committing_creds hook.
@@ -196,6 +322,8 @@ static void brute_task_execve(struct linux_binprm *bprm)
{
struct brute_stats **stats;
unsigned long flags;
+ bool network_to_local;
+ bool is_setid = false;
stats = brute_stats_ptr(current);
read_lock_irqsave(&brute_stats_ptr_lock, flags);@@ -206,12 +334,18 @@ static void brute_task_execve(struct linux_binprm *bprm)
}
spin_lock(&(*stats)->lock);
+ network_to_local = (*stats)->network;
+
+ /*
+ * A network_to_local flag equal to true will set the bounds_crossed
+ * flag. So, in this scenario the "is setid" test can be avoided.
+ */
+ if (!network_to_local)
+ is_setid = brute_is_setid(bprm);
if (!refcount_dec_not_one(&(*stats)->refc)) {
/* execve call after an execve call */
- (*stats)->faults = 0;
- (*stats)->jiffies = get_jiffies_64();
- (*stats)->period = 0;
+ brute_reset_stats(*stats, is_setid);
spin_unlock(&(*stats)->lock);
read_unlock_irqrestore(&brute_stats_ptr_lock, flags);
return;@@ -222,7 +356,7 @@ static void brute_task_execve(struct linux_binprm *bprm)
read_unlock_irqrestore(&brute_stats_ptr_lock, flags);
write_lock_irqsave(&brute_stats_ptr_lock, flags);
- *stats = brute_new_stats();
+ *stats = brute_new_stats(network_to_local, is_setid);
WARN(!*stats, "Cannot allocate statistical data\n");
write_unlock_irqrestore(&brute_stats_ptr_lock, flags);
}
@@ -653,12 +787,103 @@ static void brute_manage_exec_attack(struct brute_stats *stats, u64 now,
print_exec_attack_running(exec_stats);
}
+/**
+ * brute_priv_have_changed() - Test if the privileges have changed.
+ * @stats: Statistics that hold the saved credentials.
+ *
+ * The privileges have changed if the credentials of the current task are
+ * different from the credentials saved in the statistics structure.
+ *
+ * The statistics that hold the saved credentials cannot be NULL.
+ *
+ * Context: Must be called with interrupts disabled and brute_stats_ptr_lock
+ * and brute_stats::lock held.
+ * Return: True if the privileges have changed. False otherwise.
+ */
+static bool brute_priv_have_changed(struct brute_stats *stats)
+{
+ const struct cred *cred = current_cred();
+ bool priv_have_changed;
+
+ priv_have_changed = !uid_eq(stats->saved_cred.uid, cred->uid) ||
+ !gid_eq(stats->saved_cred.gid, cred->gid) ||
+ !uid_eq(stats->saved_cred.suid, cred->suid) ||
+ !gid_eq(stats->saved_cred.sgid, cred->sgid) ||
+ !uid_eq(stats->saved_cred.euid, cred->euid) ||
+ !gid_eq(stats->saved_cred.egid, cred->egid) ||
+ !uid_eq(stats->saved_cred.fsuid, cred->fsuid) ||
+ !gid_eq(stats->saved_cred.fsgid, cred->fsgid);
+
+ return priv_have_changed;
+}
+
+/**
+ * brute_threat_model_supported() - Test if the threat model is supported.
+ * @siginfo: Contains the signal information.
+ * @stats: Statistical data shared by all the fork hierarchy processes.
+ *
+ * To avoid false positives during the attack detection it is necessary to
+ * narrow the possible cases. Only the following scenarios are taken into
+ * account:
+ *
+ * 1.- Launching (fork()/exec()) a setuid/setgid process repeatedly until a
+ * desirable memory layout is got (e.g. Stack Clash).
+ * 2.- Connecting to an exec()ing network daemon (e.g. xinetd) repeatedly until
+ * a desirable memory layout is got (e.g. what CTFs do for simple network
+ * service).
+ * 3.- Launching processes without exec() (e.g. Android Zygote) and exposing
+ * state to attack a sibling.
+ * 4.- Connecting to a fork()ing network daemon (e.g. apache) repeatedly until
+ * the previously shared memory layout of all the other children is exposed
+ * (e.g. kind of related to HeartBleed).
+ *
+ * In each case, a privilege boundary has been crossed:
+ *
+ * Case 1: setuid/setgid process
+ * Case 2: network to local
+ * Case 3: privilege changes
+ * Case 4: network to local
+ *
+ * Also, only the signals delivered by the kernel are taken into account with
+ * the exception of the SIGABRT signal since the latter is used by glibc for
+ * stack canary, malloc, etc failures, which may indicate that a mitigation has
+ * been triggered.
+ *
+ * The signal information and the statistical data shared by all the fork
+ * hierarchy processes cannot be NULL.
+ *
+ * It's mandatory to disable interrupts before acquiring the brute_stats::lock
+ * since the task_free hook can be called from an IRQ context during the
+ * execution of the task_fatal_signal hook.
+ *
+ * Context: Must be called with interrupts disabled and brute_stats_ptr_lock
+ * held.
+ * Return: True if the threat model is supported. False otherwise.
+ */
+static bool brute_threat_model_supported(const kernel_siginfo_t *siginfo,
+ struct brute_stats *stats)
+{
+ bool bounds_crossed;
+
+ if (siginfo->si_signo == SIGKILL && siginfo->si_code != SIGABRT)
+ return false;
+
+ spin_lock(&stats->lock);
+ bounds_crossed = stats->bounds_crossed;
+ bounds_crossed = bounds_crossed || brute_priv_have_changed(stats);
+ stats->bounds_crossed = bounds_crossed;
+ spin_unlock(&stats->lock);
+
+ return bounds_crossed;
+}
+
/**
* brute_task_fatal_signal() - Target for the task_fatal_signal hook.
* @siginfo: Contains the signal information.
*
- * To detect a brute force attack is necessary to update the fork and exec
- * statistics in every fatal crash and act based on these data.
+ * To detect a brute force attack it is necessary, as a first step, to test in
+ * every fatal crash if the threat model is supported. If so, update the fork
+ * and exec statistics and act based on these data.
*
* It's mandatory to disable interrupts before acquiring brute_stats_ptr_lock
* and brute_stats::lock since the task_free hook can be called from an IRQ@@ -675,18 +900,59 @@ static void brute_task_fatal_signal(const kernel_siginfo_t *siginfo)
read_lock(&tasklist_lock);
read_lock_irqsave(&brute_stats_ptr_lock, flags);
- if (WARN(!*stats, "No statistical data\n")) {
- read_unlock_irqrestore(&brute_stats_ptr_lock, flags);
- read_unlock(&tasklist_lock);
- return;
- }
+ if (WARN(!*stats, "No statistical data\n"))
+ goto unlock;
+
+ if (!brute_threat_model_supported(siginfo, *stats))
+ goto unlock;
last_fork_crash = brute_manage_fork_attack(*stats, now);
brute_manage_exec_attack(*stats, now, last_fork_crash);
+unlock:
read_unlock_irqrestore(&brute_stats_ptr_lock, flags);
read_unlock(&tasklist_lock);
}
+/**
+ * brute_network() - Target for the socket_sock_rcv_skb hook.
+ * @sk: Contains the sock (not socket) associated with the incoming sk_buff.
+ * @skb: Contains the incoming network data.
+ *
+ * A previous step to detect that a network to local boundary has been crossed
+ * is to detect if there is network activity. To do this, it is only necessary
+ * to check if there are data packets received from a network device other than
+ * loopback.
+ *
+ * It's mandatory to disable interrupts before acquiring brute_stats_ptr_lock
+ * and brute_stats::lock since the task_free hook can be called from an IRQ
+ * context during the execution of the socket_sock_rcv_skb hook.
+ *
+ * Return: -EFAULT if the current task doesn't have statistical data. Zero
+ * otherwise.
+ */
+static int brute_network(struct sock *sk, struct sk_buff *skb)
+{
+ struct brute_stats **stats;
+ unsigned long flags;
+
+ if (!skb->dev || (skb->dev->flags & IFF_LOOPBACK))
+ return 0;
+
+ stats = brute_stats_ptr(current);
+ read_lock_irqsave(&brute_stats_ptr_lock, flags);
+
+ if (!*stats) {
+ read_unlock_irqrestore(&brute_stats_ptr_lock, flags);
+ return -EFAULT;
+ }
+
+ spin_lock(&(*stats)->lock);
+ (*stats)->network = true;
+ spin_unlock(&(*stats)->lock);
+ read_unlock_irqrestore(&brute_stats_ptr_lock, flags);
+ return 0;
+}
+
/*
* brute_hooks - Targets for the LSM's hooks.
*/@@ -695,6 +961,7 @@ static struct security_hook_list brute_hooks[] __lsm_ro_after_init = {
LSM_HOOK_INIT(bprm_committing_creds, brute_task_execve),
LSM_HOOK_INIT(task_free, brute_task_free),
LSM_HOOK_INIT(task_fatal_signal, brute_task_fatal_signal),
+ LSM_HOOK_INIT(socket_sock_rcv_skb, brute_network),
};
/**
--