From patchwork Sun Feb 21 15:49:12 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385959 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-13.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 2E8D9C433DB for ; Sun, 21 Feb 2021 16:25:37 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id E6EC064E5F for ; Sun, 21 Feb 2021 16:25:36 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S229970AbhBUQZV (ORCPT ); Sun, 21 Feb 2021 11:25:21 -0500 Received: from mout.gmx.net ([212.227.17.21]:38469 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229817AbhBUQZU (ORCPT ); Sun, 21 Feb 2021 11:25:20 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613924601; bh=Mig8mLw6txPp64eklWsPmtDq+qJ7Ux3BOxxRc/jYndU=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=HplHLhejOO3vcTUhTyHaFtyxF3k8LaTeI5sM3tlux0xQfT70pqiwVrWT1U/joJbA6 XSofeh0uUMWrVIUeMKDGsSka9kzMJyjV7bl0lK+C0+MUanz2ogJn9/Z0aY+zFatYuc yQ4shj+QhmaBFf4u6hqaFBWTOdlZ5hFdBaqMO504= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx105 [212.227.17.174]) with ESMTPSA (Nemesis) id 1MVN6t-1lNb3u0kqN-00SOz1; Sun, 21 Feb 2021 17:23:21 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 1/8] security: Add LSM hook at the point where a task gets a fatal signal Date: Sun, 21 Feb 2021 16:49:12 +0100 Message-Id: <20210221154919.68050-2-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:Mk8QK57R80ANwQs60TNWIOOGi7oZIl0vZSBtlU2wG9EQTvfcyUN Ypqj7CRTNjXwl032tG5aHovn1oS8TsU0YzyLyyONXYyaIUVd8SoNxQNL30wpSF8+sszPqiK eo7FPfJnUSFxCg0ve5ZOyEUCDtTUuTRLa3lraiBV/83zihO9+ry56SLBvWngiAoEcghfZ9Y d10x7SVYQccjY8XDHAIsg== X-UI-Out-Filterresults: notjunk:1; V03:K0:PLP7KhJEMYw=:I0hZ3CUHTLIKbo8z3JwUiy czCfT7CKWPXnhyNrztr0MmnxVl5HQtibEObi6Q5zNhTragbUB6I933/g5zLCxqvkfbwBMRjLR c6g02r2bBHgxgBCiBETUj4Shg5F5YGgH/bUj3y6NnHtWV0QGtzmB2j4nnWLMXf5aA6Gck6bFN o6GGOdzD7RqpjX+ZrDPScV5LxGmEF/j31eIkLInSHjBrDHHMtMVwaBNcMpksZv4FOdK4LwQfW eOLC1IItJx2baHWC67Efw9s/OeSP1KpmhV/vXwDbu8liwDshc//1y+c3Yltn9dVezpjemwoaN JFTDzlNGclEmRyw1m8r37DNGH3+3AvZRaS1gTteUeiWLi8fuTSxit+Z1TnjxO9Vx2tY4h4JOi oRpQr7EP7y6PkQxnJrIaSHVQT6x9NLtvjkBNzEOIGBXaElKsh3M7Q9ghsXQKj8b9zi5cW3aa/ B+XTchNaFqVgXT0yobrXHHcORuk3W+ex7FQJIxThtCbhTm9reecY2oIv2Ai3MDUbGY4R8PqSy RyMOoLBRmQTJ7M/egc3W2Wm4Bs/Tl3HvNrTaBE4SPVaaeM7TaFQIklppyLwm89dXrP3Jt1UI9 LhWCs3COYHO5U9ILUplhqJeGfBrnAzgJtW0zlR7JoX5StEIxnnwLpqilYwlVW9eG+vZpdsHbM iEUplBp/jHXjYY5N8qaZpvmfGJ2KAKdeCPCpY4acZ0MeCRlAGQZ4m8U24NLIV2qRBpidG+hdW qjBKMCl6rwIad1h+iWWxXRnfKUgLZszBNcCH1TplBKNz+w69TZwPKN5TclHQqFLmCnOjNfAJU 0QXdf4gOBgrFKtbxA/adjkVQ3xiMFHjUG/Gk/UO6F3NZ3p3i8uhLsPiIBmcBLLCJvxTtjb9cM UWuwG9B96Hk7sU6UD/xA== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org Add a security hook that allows a LSM to be notified when a task gets a fatal signal. This patch is a previous step on the way to compute the task crash period by the "brute" LSM (linux security module to detect and mitigate fork brute force attack against vulnerable userspace processes). Signed-off-by: John Wood --- include/linux/lsm_hook_defs.h | 1 + include/linux/lsm_hooks.h | 4 ++++ include/linux/security.h | 4 ++++ kernel/signal.c | 1 + security/security.c | 5 +++++ 5 files changed, 15 insertions(+) -- 2.25.1 diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h index 7aaa753b8608..fa9bfa9676f3 100644 --- a/include/linux/lsm_hook_defs.h +++ b/include/linux/lsm_hook_defs.h @@ -215,6 +215,7 @@ LSM_HOOK(int, -ENOSYS, task_prctl, int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5) LSM_HOOK(void, LSM_RET_VOID, task_to_inode, struct task_struct *p, struct inode *inode) +LSM_HOOK(void, LSM_RET_VOID, task_fatal_signal, const kernel_siginfo_t *siginfo) LSM_HOOK(int, 0, ipc_permission, struct kern_ipc_perm *ipcp, short flag) LSM_HOOK(void, LSM_RET_VOID, ipc_getsecid, struct kern_ipc_perm *ipcp, u32 *secid) diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h index a19adef1f088..1ec253e557a4 100644 --- a/include/linux/lsm_hooks.h +++ b/include/linux/lsm_hooks.h @@ -774,6 +774,10 @@ * security attributes, e.g. for /proc/pid inodes. * @p contains the task_struct for the task. * @inode contains the inode structure for the inode. + * @task_fatal_signal: + * This hook allows security modules to be notified when a task gets a + * fatal signal. + * @siginfo contains the signal information. * * Security hooks for Netlink messaging. * diff --git a/include/linux/security.h b/include/linux/security.h index c35ea0ffccd9..0663db6fca7e 100644 --- a/include/linux/security.h +++ b/include/linux/security.h @@ -419,6 +419,7 @@ int security_task_kill(struct task_struct *p, struct kernel_siginfo *info, int security_task_prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5); void security_task_to_inode(struct task_struct *p, struct inode *inode); +void security_task_fatal_signal(const kernel_siginfo_t *siginfo); int security_ipc_permission(struct kern_ipc_perm *ipcp, short flag); void security_ipc_getsecid(struct kern_ipc_perm *ipcp, u32 *secid); int security_msg_msg_alloc(struct msg_msg *msg); @@ -1141,6 +1142,9 @@ static inline int security_task_prctl(int option, unsigned long arg2, static inline void security_task_to_inode(struct task_struct *p, struct inode *inode) { } +static inline void security_task_fatal_signal(const kernel_siginfo_t *siginfo) +{ } + static inline int security_ipc_permission(struct kern_ipc_perm *ipcp, short flag) { diff --git a/kernel/signal.c b/kernel/signal.c index 5ad8566534e7..893c07a77c76 100644 --- a/kernel/signal.c +++ b/kernel/signal.c @@ -2750,6 +2750,7 @@ bool get_signal(struct ksignal *ksig) /* * Anything else is fatal, maybe with a core dump. */ + security_task_fatal_signal(&ksig->info); current->flags |= PF_SIGNALED; if (sig_kernel_coredump(signr)) { diff --git a/security/security.c b/security/security.c index 7b09cfbae94f..96731d0428f9 100644 --- a/security/security.c +++ b/security/security.c @@ -1827,6 +1827,11 @@ void security_task_to_inode(struct task_struct *p, struct inode *inode) call_void_hook(task_to_inode, p, inode); } +void security_task_fatal_signal(const kernel_siginfo_t *siginfo) +{ + call_void_hook(task_fatal_signal, siginfo); +} + int security_ipc_permission(struct kern_ipc_perm *ipcp, short flag) { return call_int_hook(ipc_permission, 0, ipcp, flag); From patchwork Sun Feb 21 15:49:13 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385632 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-13.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id F36E1C433DB for ; Sun, 21 Feb 2021 16:58:59 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id C47C964EF5 for ; Sun, 21 Feb 2021 16:58:59 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S230157AbhBUQ6n (ORCPT ); Sun, 21 Feb 2021 11:58:43 -0500 Received: from mout.gmx.net ([212.227.17.20]:33471 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S230154AbhBUQ6k (ORCPT ); Sun, 21 Feb 2021 11:58:40 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613926604; bh=b0A70STc6q0zG4y40zx/OPGBNbJzabH34MJgs6/ezX0=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=dB1drp1D17iibtPZaFdAchtoC0GBTWPW9x44fMMH/dwqebEWuObl/u1vZBKKmBtdp IEJ+bgsIdNcJepp5LQkNHW4gbBQm4TqNfSrSNXhLRZz9eZEWFJWLpbo2RRUfLiIn7+ QdcJ8mkv6MaBWd+9EMVjrdVKTJInIr53xebfN9Rg= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx105 [212.227.17.174]) with ESMTPSA (Nemesis) id 1MWASY-1lK16E0g7W-00XdcO; Sun, 21 Feb 2021 17:56:44 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 2/8] security/brute: Define a LSM and manage statistical data Date: Sun, 21 Feb 2021 16:49:13 +0100 Message-Id: <20210221154919.68050-3-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:8GiMSI1InG8Trx6G+IAU1G9zTRZMHhG/VvMFF2f2mhnMDs+EZ1g H/ILVdS3J1mpBUPfuhEv9wKnpp3xFK6vQADYKI2B4m7wTV7kq12YPuxw2ZovzDQ+H8bdC91 dWrMWAhecamqUIVUfefhERKcuYzCdFk0ueSPLTMhvF9sxTKE7ZTUzVPff7LFbjiPV8LzhgD tr5ZmZPHvi9fesKX2ZcnQ== X-UI-Out-Filterresults: notjunk:1; V03:K0:GvPZmhT5EPA=:HATTQwbtzaISXPoR6iF+aV RSaHn0VWFIFnKsClaIWXM7uFa+QNeX2B37DYg9PcFbM1D9ULSj5jNv/B1aA9FBdBnU77oKZO1 r+4ZbrC2mhgtj0NPfqUUJmYFUZUrvXyTpI1wAfqVxbcIq3n6/Gxye6bIpVSTPRQbRBOMzXQbW Nct2O+nhqWRcR8gIGbszsoFgJzQu3HEFCODGPtBpWc2lJ4+rAZQWUrHp7fvDANHoRyxAZznTi U38XSy5H3oWilL7ADR/Fe3ZSMa7AZH26QiRug8k4kWjDlsayVhrBLC9w2aH+Mp8SU5PhBcbQ4 ckoIZlKLRbLF+VkzrTX90ZXLsy+IJq5d7Nj1sGno5caL96XTXaSKvx7gx/zCzNvhHZrupuVBC lSyopuLmRXgawQYLAh1g/NVQwhs0kGoy/9eP0fOfRgD458X/uQyb7Mt0y/4fxMKgVeuLM5cT/ 1S/62yNwMhspfCjdUavbK9oBbhaxZf9k/nNpLvDI7YE3TlMyT5bxUJwyqqlvGoH9w7mYFFn3x x8K13PLk+UoySQ+/aWsfm9XmoQkIal8pKWD3DjRqTx0M0hKVSazK83r8b85ZvBM7ATcHV12Td v0ftla5cZbSiK9Tu/QS6gVevO7Jr8l5heg1BVc2gZi8rmvb676wi3toAUQSfU+6xcL+1raX3i tgYHvKjCbjnizL7SJSuRR2blGsqMVlmrF4eebjCQ2hqz/+SZWeL7GzJWTSRGgjXesJtItZNpj 1qSqo97F/Xs4reGynD2Q8w9Cbl86fRFeytAiKt5xIvjFvI/cYgXgENT5mUS/DXYb+wqWLW6fk lzjSjnNZJQftSl87yr713c7IE7gQWyj7NpeS7fEWrgdTRTvaBsr1576VujwbWjvyhW0WnkDrA Q3TswEdPmfy05OY3yQ6Q== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org Add a new Kconfig file to define a menu entry under "Security options" to enable the "Fork brute force attack detection and mitigation" feature. For a correct management of a fork brute force attack it is necessary that all the tasks hold statistical data. The same statistical data needs to be shared between all the tasks that hold the same memory contents or in other words, between all the tasks that have been forked without any execve call. So, define a statistical data structure to hold all the necessary information shared by all the fork hierarchy processes. This info is basically the number of crashes, the last crash timestamp and the crash period's moving average. When a forked task calls the execve system call, the memory contents are set with new values. So, in this scenario the parent's statistical data no need to be shared. Instead, a new statistical data structure must be allocated to start a new hierarchy. The statistical data that is shared between all the fork hierarchy processes needs to be freed when this hierarchy disappears. So, based in all the previous information define a LSM with three hooks to manage all the commented cases. These hooks are "task_alloc" to do the fork management, "bprm_committing_creds" to do the execve management and "task_free" to release the resources. Also, add to the task_struct's security blob the pointer to the statistical data. This way, all the tasks will have access to this information. Signed-off-by: John Wood --- security/Kconfig | 11 +- security/Makefile | 4 + security/brute/Kconfig | 12 ++ security/brute/Makefile | 2 + security/brute/brute.c | 252 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 security/brute/Kconfig create mode 100644 security/brute/Makefile create mode 100644 security/brute/brute.c -- 2.25.1 diff --git a/security/Kconfig b/security/Kconfig index 7561f6f99f1d..204bb311b1f1 100644 --- a/security/Kconfig +++ b/security/Kconfig @@ -240,6 +240,7 @@ source "security/safesetid/Kconfig" source "security/lockdown/Kconfig" source "security/integrity/Kconfig" +source "security/brute/Kconfig" choice prompt "First legacy 'major LSM' to be initialized" @@ -277,11 +278,11 @@ endchoice config LSM string "Ordered list of enabled LSMs" - default "lockdown,yama,loadpin,safesetid,integrity,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK - default "lockdown,yama,loadpin,safesetid,integrity,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR - default "lockdown,yama,loadpin,safesetid,integrity,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO - default "lockdown,yama,loadpin,safesetid,integrity,bpf" if DEFAULT_SECURITY_DAC - default "lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,bpf" + default "brute,lockdown,yama,loadpin,safesetid,integrity,smack,selinux,tomoyo,apparmor,bpf" if DEFAULT_SECURITY_SMACK + default "brute,lockdown,yama,loadpin,safesetid,integrity,apparmor,selinux,smack,tomoyo,bpf" if DEFAULT_SECURITY_APPARMOR + default "brute,lockdown,yama,loadpin,safesetid,integrity,tomoyo,bpf" if DEFAULT_SECURITY_TOMOYO + default "brute,lockdown,yama,loadpin,safesetid,integrity,bpf" if DEFAULT_SECURITY_DAC + default "brute,lockdown,yama,loadpin,safesetid,integrity,selinux,smack,tomoyo,apparmor,bpf" help A comma-separated list of LSMs, in initialization order. Any LSMs left off this list will be ignored. This can be diff --git a/security/Makefile b/security/Makefile index 3baf435de541..1236864876da 100644 --- a/security/Makefile +++ b/security/Makefile @@ -36,3 +36,7 @@ obj-$(CONFIG_BPF_LSM) += bpf/ # Object integrity file lists subdir-$(CONFIG_INTEGRITY) += integrity obj-$(CONFIG_INTEGRITY) += integrity/ + +# Object brute file lists +subdir-$(CONFIG_SECURITY_FORK_BRUTE) += brute +obj-$(CONFIG_SECURITY_FORK_BRUTE) += brute/ diff --git a/security/brute/Kconfig b/security/brute/Kconfig new file mode 100644 index 000000000000..1bd2df1e2dec --- /dev/null +++ b/security/brute/Kconfig @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0 +config SECURITY_FORK_BRUTE + bool "Fork brute force attack detection and mitigation" + depends on SECURITY + help + This is an LSM that stops any fork brute force attack against + vulnerable userspace processes. The detection method is based on + the application crash period and as a mitigation procedure all the + offending tasks are killed. Like capabilities, this security module + stacks with other LSMs. + + If you are unsure how to answer this question, answer N. diff --git a/security/brute/Makefile b/security/brute/Makefile new file mode 100644 index 000000000000..d3f233a132a9 --- /dev/null +++ b/security/brute/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0 +obj-$(CONFIG_SECURITY_FORK_BRUTE) += brute.o diff --git a/security/brute/brute.c b/security/brute/brute.c new file mode 100644 index 000000000000..70f812bb7763 --- /dev/null +++ b/security/brute/brute.c @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-2.0 + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * struct brute_stats - Fork brute force attack statistics. + * @lock: Lock to protect the brute_stats structure. + * @refc: Reference counter. + * @faults: Number of crashes. + * @jiffies: Last crash timestamp. + * @period: Crash period's moving average. + * + * This structure holds the statistical data shared by all the fork hierarchy + * processes. + */ +struct brute_stats { + spinlock_t lock; + refcount_t refc; + unsigned char faults; + u64 jiffies; + u64 period; +}; + +/** + * brute_blob_sizes - LSM blob sizes. + * + * To share statistical data among all the fork hierarchy processes, define a + * pointer to the brute_stats structure as a part of the task_struct's security + * blob. + */ +static struct lsm_blob_sizes brute_blob_sizes __lsm_ro_after_init = { + .lbs_task = sizeof(struct brute_stats *), +}; + +/** + * brute_stats_ptr() - Get the pointer to the brute_stats structure. + * @task: Task that holds the statistical data. + * + * Return: A pointer to a pointer to the brute_stats structure. + */ +static inline struct brute_stats **brute_stats_ptr(struct task_struct *task) +{ + return task->security + brute_blob_sizes.lbs_task; +} + +/** + * brute_new_stats() - Allocate a new statistics structure. + * + * 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. + * + * 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) +{ + struct brute_stats *stats; + + stats = kmalloc(sizeof(struct brute_stats), GFP_KERNEL); + if (!stats) + return NULL; + + spin_lock_init(&stats->lock); + refcount_set(&stats->refc, 1); + stats->faults = 0; + stats->jiffies = get_jiffies_64(); + stats->period = 0; + + return stats; +} + +/** + * brute_share_stats() - Share the statistical data between processes. + * @src: Source of statistics to be shared. + * @dst: Destination of statistics to be shared. + * + * Copy the src's pointer to the statistical data structure to the dst's pointer + * to the same structure. Since there is a new process that shares the same + * data, increase the reference counter. The src's pointer 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_alloc hook. + */ +static void brute_share_stats(struct brute_stats *src, + struct brute_stats **dst) +{ + unsigned long flags; + + spin_lock_irqsave(&src->lock, flags); + refcount_inc(&src->refc); + *dst = src; + spin_unlock_irqrestore(&src->lock, flags); +} + +/** + * brute_task_alloc() - Target for the task_alloc hook. + * @task: Task being allocated. + * @clone_flags: Contains the flags indicating what should be shared. + * + * For a correct management of a fork brute force attack it is necessary that + * all the tasks hold statistical data. The same statistical data needs to be + * shared between all the tasks that hold the same memory contents or in other + * words, between all the tasks that have been forked without any execve call. + * + * To ensure this, if the current task doesn't have statistical data when forks, + * it is mandatory to allocate a new statistics structure and share it between + * this task and the new one being allocated. Otherwise, share the statistics + * that the current task already has. + * + * Return: -ENOMEM if the allocation of the new statistics structure fails. Zero + * otherwise. + */ +static int brute_task_alloc(struct task_struct *task, unsigned long clone_flags) +{ + struct brute_stats **stats, **p_stats; + + stats = brute_stats_ptr(task); + p_stats = brute_stats_ptr(current); + + if (likely(*p_stats)) { + brute_share_stats(*p_stats, stats); + return 0; + } + + *stats = brute_new_stats(); + if (!*stats) + return -ENOMEM; + + brute_share_stats(*stats, p_stats); + return 0; +} + +/** + * brute_task_execve() - Target for the bprm_committing_creds hook. + * @bprm: Points to the linux_binprm structure. + * + * When a forked task calls the execve system call, the memory contents are set + * with new values. So, in this scenario the parent's statistical data no need + * to be shared. Instead, a new statistical data structure must be allocated to + * start a new hierarchy. This condition is detected when the statistics + * reference counter holds a value greater than or equal to two (a fork always + * sets the statistics reference counter to a minimum of two since the parent + * and the child task are sharing the same data). + * + * However, if the execve function is called immediately after another execve + * call, althought the memory contents are reset, there is no need to allocate + * a new statistical data structure. This is possible because at this moment + * 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. + * + * 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 bprm_committing_creds hook. + */ +static void brute_task_execve(struct linux_binprm *bprm) +{ + struct brute_stats **stats; + unsigned long flags; + + stats = brute_stats_ptr(current); + if (WARN(!*stats, "No statistical data\n")) + return; + + spin_lock_irqsave(&(*stats)->lock, flags); + + if (!refcount_dec_not_one(&(*stats)->refc)) { + /* execve call after an execve call */ + (*stats)->faults = 0; + (*stats)->jiffies = get_jiffies_64(); + (*stats)->period = 0; + spin_unlock_irqrestore(&(*stats)->lock, flags); + return; + } + + /* execve call after a fork call */ + spin_unlock_irqrestore(&(*stats)->lock, flags); + *stats = brute_new_stats(); + WARN(!*stats, "Cannot allocate statistical data\n"); +} + +/** + * brute_task_free() - Target for the task_free hook. + * @task: Task about to be freed. + * + * The statistical data that is shared between all the fork hierarchy processes + * needs to be freed when this hierarchy disappears. + */ +static void brute_task_free(struct task_struct *task) +{ + struct brute_stats **stats; + bool refc_is_zero; + + stats = brute_stats_ptr(task); + if (WARN(!*stats, "No statistical data\n")) + return; + + spin_lock(&(*stats)->lock); + refc_is_zero = refcount_dec_and_test(&(*stats)->refc); + spin_unlock(&(*stats)->lock); + + if (refc_is_zero) { + kfree(*stats); + *stats = NULL; + } +} + +/** + * brute_hooks - Targets for the LSM's hooks. + */ +static struct security_hook_list brute_hooks[] __lsm_ro_after_init = { + LSM_HOOK_INIT(task_alloc, brute_task_alloc), + LSM_HOOK_INIT(bprm_committing_creds, brute_task_execve), + LSM_HOOK_INIT(task_free, brute_task_free), +}; + +/** + * brute_init() - Initialize the brute LSM. + * + * Return: Always returns zero. + */ +static int __init brute_init(void) +{ + pr_info("Brute initialized\n"); + security_add_hooks(brute_hooks, ARRAY_SIZE(brute_hooks), + KBUILD_MODNAME); + return 0; +} + +DEFINE_LSM(brute) = { + .name = KBUILD_MODNAME, + .init = brute_init, + .blobs = &brute_blob_sizes, +}; From patchwork Sun Feb 21 15:49:14 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385958 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-13.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 355B0C433E0 for ; Sun, 21 Feb 2021 17:32:34 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id D437C64EE0 for ; Sun, 21 Feb 2021 17:32:33 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S229844AbhBURcR (ORCPT ); Sun, 21 Feb 2021 12:32:17 -0500 Received: from mout.gmx.net ([212.227.15.15]:46877 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229685AbhBURcQ (ORCPT ); Sun, 21 Feb 2021 12:32:16 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613928610; bh=oMhbco5kDracN9d79YkZS3hB+HSUG8UB8H1ZKqjx3EY=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=TACU1FLoT7qAk/Iy/fkgL7ZJCsj4uq25/0D+XSfYFqUV6qBhCAlhv4isIdvh/e9La pnw81s4opgcwF247xLgU1FpeIIqswHoTCKYJZRqMuP6KYMyqVA01EquLROog0jOaUu 1nkrd1KGAD6rxh94y8tyO/YqnP6XG/+UI/mMn7q0= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx004 [212.227.17.184]) with ESMTPSA (Nemesis) id 1MrQIv-1laaaT1Rx9-00oTCp; Sun, 21 Feb 2021 18:30:10 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 3/8] securtiy/brute: Detect a brute force attack Date: Sun, 21 Feb 2021 16:49:14 +0100 Message-Id: <20210221154919.68050-4-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:YacEwmqAAU+6XBmIARBdWnQSC5vckZu8rO4UvdszEfoj4RqMftr uIUYt4PVrkgtCHiYpSqHkFNc5uK087UCzr/2Vybe9CFv7TIGdFHA8YUVgltP6yySRdgtBhm f1fPHHvcCYD4ltWFqpp+EXsojyYT1Jh17nK0Wn23Zihzo2DUwyW+ArnK9tIHr2DKjB8n4rl n7bDpnaT8eskVKrg/J/zw== X-UI-Out-Filterresults: notjunk:1; V03:K0:bbViJQ5ItSs=:BvT2YLCkibvnAAYaH0/wLb b+NhZRh2SooR0FifkEo0Hu9Hh2liLtvwoMA3PmtwAu5ztLtrosx/3b7tkTVKhDqC2cAHjOx2p CYSkGebmvVAY6jIVnD0uTnh6U07KNHKseqKg5L7Q5j/a0kz3l4Qf+hRyGaZsAb/uIcMh3w7IF FeniwCmc8iARbr2VX4eY312oSiRfCAQ8xgz/SO92Jn7CTIpfQ6/HM1PWLPRyqlX66QsQlJDkn hCtpPcKjRjFy6X4TtGRoVQe0dY8CbxJze5XoWcpz0MBjz3l6XJqnBDfQ6BR5+zmkC3PU6r7fp TmGEaZ0JlCXW9gwnQLpiHUwaFZj4ZoTyfYxR4yNNw9afsF7ukK7RA0S8/ISXKJhBokVBR+haD U+p5UOgTQDv8GImp4rwjzvskyS9KMs/YPHgW47KfUZ6S7FKX+eabMZXeuA8UWmGya7r8g9m/o //LUM9CYRBORWxzvWlQLvezZD2sT6RIgMl0n4/3uNMwXzTUMfxRW5/FzV/ho27n7hmUmGxsqs j2l4LIQsH8Eppe+X2Ny/9rioPXUCCdCE1la+fXk9uZLyYU20emIbOiewPy56Uu9BUEKaYzvU6 VaPhFOs3WInUXLab5Gd4ZjhrE/YrWwXqnmeNsnFPQPr1cBMEpG0HWLrnE1mtwxZoZSQEfdGGg J5p6D6Axlf0DGt6eZ2IxgiTLkqNN8ZP2K+ifu4hpmYKAWZ3PSANDFXgLm+bETaL/dVJvpdbMr +X2kImvNudBhAxxKnYEf1TkOcQqK6RvdT3W13K1ebsK3sk5HowAXQ9w5iyepjAycyKWK7nRTz 5I4nqxi50GDX4hJ3O6n2V92PrlL1xXZT9Z66g4n9Lp2MO1TkBWNwtKA5LfyxIZDRGt9wGRN4u 7my7mZWk7mP6dCvLPYpg== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org To detect a brute force attack it is necessary that the statistics shared by all the fork hierarchy processes be updated in every fatal crash and the most important data to update is the application crash period. To do so, use the new "task_fatal_signal" LSM hook added in a previous step. The application crash period must be a value that is not prone to change due to spurious data and follows the real crash period. So, to compute it, the exponential moving average (EMA) is used. There are two types of brute force attacks that need to be detected. The first one is an attack that happens through the fork system call and the second one is an attack that happens through the execve system call. The first type uses the statistics shared by all the fork hierarchy processes, but the second type cannot use this statistical data due to these statistics dissapear when the involved tasks finished. In this last scenario the attack info should be tracked by the statistics of a higher fork hierarchy (the hierarchy that contains the process that forks before the execve system call). Moreover, these two attack types have two variants. A slow brute force attack that is detected if the maximum number of faults per fork hierarchy is reached and a fast brute force attack that is detected if the application crash period falls below a certain threshold. Also, this patch adds locking to protect the statistics pointer hold by every process. Signed-off-by: John Wood --- security/brute/brute.c | 488 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 474 insertions(+), 14 deletions(-) -- 2.25.1 diff --git a/security/brute/brute.c b/security/brute/brute.c index 70f812bb7763..645bd6e02638 100644 --- a/security/brute/brute.c +++ b/security/brute/brute.c @@ -11,9 +11,14 @@ #include #include #include +#include #include #include +#include +#include #include +#include +#include #include #include #include @@ -37,6 +42,11 @@ struct brute_stats { u64 period; }; +/** + * brute_stats_ptr_lock - Lock to protect the brute_stats structure pointer. + */ +static DEFINE_RWLOCK(brute_stats_ptr_lock); + /** * brute_blob_sizes - LSM blob sizes. * @@ -74,7 +84,7 @@ static struct brute_stats *brute_new_stats(void) { struct brute_stats *stats; - stats = kmalloc(sizeof(struct brute_stats), GFP_KERNEL); + stats = kmalloc(sizeof(struct brute_stats), GFP_ATOMIC); if (!stats) return NULL; @@ -99,16 +109,17 @@ static struct brute_stats *brute_new_stats(void) * 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_alloc hook. + * + * Context: Must be called with interrupts disabled and brute_stats_ptr_lock + * held. */ static void brute_share_stats(struct brute_stats *src, struct brute_stats **dst) { - unsigned long flags; - - spin_lock_irqsave(&src->lock, flags); + spin_lock(&src->lock); refcount_inc(&src->refc); *dst = src; - spin_unlock_irqrestore(&src->lock, flags); + spin_unlock(&src->lock); } /** @@ -126,26 +137,36 @@ 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. * + * 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. + * * Return: -ENOMEM if the allocation of the new statistics structure fails. Zero * otherwise. */ static int brute_task_alloc(struct task_struct *task, unsigned long clone_flags) { struct brute_stats **stats, **p_stats; + unsigned long flags; stats = brute_stats_ptr(task); p_stats = brute_stats_ptr(current); + write_lock_irqsave(&brute_stats_ptr_lock, flags); if (likely(*p_stats)) { brute_share_stats(*p_stats, stats); + write_unlock_irqrestore(&brute_stats_ptr_lock, flags); return 0; } *stats = brute_new_stats(); - if (!*stats) + if (!*stats) { + write_unlock_irqrestore(&brute_stats_ptr_lock, flags); return -ENOMEM; + } brute_share_stats(*stats, p_stats); + write_unlock_irqrestore(&brute_stats_ptr_lock, flags); return 0; } @@ -167,9 +188,9 @@ 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. * - * 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 bprm_committing_creds hook. + * 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. */ static void brute_task_execve(struct linux_binprm *bprm) { @@ -177,24 +198,33 @@ static void brute_task_execve(struct linux_binprm *bprm) unsigned long flags; stats = brute_stats_ptr(current); - if (WARN(!*stats, "No statistical data\n")) + read_lock_irqsave(&brute_stats_ptr_lock, flags); + + if (WARN(!*stats, "No statistical data\n")) { + read_unlock_irqrestore(&brute_stats_ptr_lock, flags); return; + } - spin_lock_irqsave(&(*stats)->lock, flags); + spin_lock(&(*stats)->lock); if (!refcount_dec_not_one(&(*stats)->refc)) { /* execve call after an execve call */ (*stats)->faults = 0; (*stats)->jiffies = get_jiffies_64(); (*stats)->period = 0; - spin_unlock_irqrestore(&(*stats)->lock, flags); + spin_unlock(&(*stats)->lock); + read_unlock_irqrestore(&brute_stats_ptr_lock, flags); return; } /* execve call after a fork call */ - spin_unlock_irqrestore(&(*stats)->lock, flags); + spin_unlock(&(*stats)->lock); + read_unlock_irqrestore(&brute_stats_ptr_lock, flags); + + write_lock_irqsave(&brute_stats_ptr_lock, flags); *stats = brute_new_stats(); WARN(!*stats, "Cannot allocate statistical data\n"); + write_unlock_irqrestore(&brute_stats_ptr_lock, flags); } /** @@ -210,17 +240,446 @@ static void brute_task_free(struct task_struct *task) bool refc_is_zero; stats = brute_stats_ptr(task); - if (WARN(!*stats, "No statistical data\n")) + read_lock(&brute_stats_ptr_lock); + + if (WARN(!*stats, "No statistical data\n")) { + read_unlock(&brute_stats_ptr_lock); return; + } spin_lock(&(*stats)->lock); refc_is_zero = refcount_dec_and_test(&(*stats)->refc); spin_unlock(&(*stats)->lock); + read_unlock(&brute_stats_ptr_lock); if (refc_is_zero) { + write_lock(&brute_stats_ptr_lock); kfree(*stats); *stats = NULL; + write_unlock(&brute_stats_ptr_lock); + } +} + +/** + * BRUTE_EMA_WEIGHT_NUMERATOR - Weight's numerator of EMA. + */ +static const u64 BRUTE_EMA_WEIGHT_NUMERATOR = 7; + +/** + * BRUTE_EMA_WEIGHT_DENOMINATOR - Weight's denominator of EMA. + */ +static const u64 BRUTE_EMA_WEIGHT_DENOMINATOR = 10; + +/** + * brute_mul_by_ema_weight() - Multiply by EMA weight. + * @value: Value to multiply by EMA weight. + * + * Return: The result of the multiplication operation. + */ +static inline u64 brute_mul_by_ema_weight(u64 value) +{ + return mul_u64_u64_div_u64(value, BRUTE_EMA_WEIGHT_NUMERATOR, + BRUTE_EMA_WEIGHT_DENOMINATOR); +} + +/** + * BRUTE_MAX_FAULTS - Maximum number of faults. + * + * If a brute force attack is running slowly for a long time, the application + * crash period's EMA is not suitable for the detection. This type of attack + * must be detected using a maximum number of faults. + */ +static const unsigned char BRUTE_MAX_FAULTS = 200; + +/** + * brute_update_crash_period() - Update the application crash period. + * @stats: Statistics that hold the application crash period to update. + * @now: The current timestamp in jiffies. + * + * The application crash period must be a value that is not prone to change due + * to spurious data and follows the real crash period. So, to compute it, the + * exponential moving average (EMA) is used. + * + * This kind of average defines a weight (between 0 and 1) for the new value to + * add and applies the remainder of the weight to the current average value. + * This way, some spurious data will not excessively modify the average and only + * if the new values are persistent, the moving average will tend towards them. + * + * Mathematically the application crash period's EMA can be expressed as + * follows: + * + * period_ema = period * weight + period_ema * (1 - weight) + * + * If the operations are applied: + * + * period_ema = period * weight + period_ema - period_ema * weight + * + * If the operands are ordered: + * + * period_ema = period_ema - period_ema * weight + period * weight + * + * Finally, this formula can be written as follows: + * + * period_ema -= period_ema * weight; + * period_ema += period * weight; + * + * The statistics that hold the application crash period to update 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: The last crash timestamp before updating it. + */ +static u64 brute_update_crash_period(struct brute_stats *stats, u64 now) +{ + u64 current_period; + u64 last_crash_timestamp; + + spin_lock(&stats->lock); + current_period = now - stats->jiffies; + last_crash_timestamp = stats->jiffies; + stats->jiffies = now; + + stats->period -= brute_mul_by_ema_weight(stats->period); + stats->period += brute_mul_by_ema_weight(current_period); + + if (stats->faults < BRUTE_MAX_FAULTS) + stats->faults += 1; + + spin_unlock(&stats->lock); + return last_crash_timestamp; +} + +/** + * BRUTE_MIN_FAULTS - Minimum number of faults. + * + * The application crash period's EMA cannot be used until a minimum number of + * data has been applied to it. This constraint allows getting a trend when this + * moving average is used. Moreover, it avoids the scenario where an application + * fails quickly from execve system call due to reasons unrelated to a real + * attack. + */ +static const unsigned char BRUTE_MIN_FAULTS = 5; + +/** + * BRUTE_CRASH_PERIOD_THRESHOLD - Application crash period threshold. + * + * The units are expressed in milliseconds. + * + * A fast brute force attack is detected when the application crash period falls + * below this threshold. + */ +static const u64 BRUTE_CRASH_PERIOD_THRESHOLD = 30000; + +/** + * brute_attack_running() - Test if a brute force attack is happening. + * @stats: Statistical data shared by all the fork hierarchy processes. + * + * The decision if a brute force attack is running is based on the statistical + * data shared by all the fork hierarchy processes. This statistics cannot be + * NULL. + * + * There are two types of brute force attacks that can be detected using the + * statistical data. The first one is a slow brute force attack that is detected + * if the maximum number of faults per fork hierarchy is reached. The second + * type is a fast brute force attack that is detected if the application crash + * period falls below a certain threshold. + * + * Moreover, it is important to note that no attacks will be detected until a + * minimum number of faults have occurred. This allows to have a trend in the + * crash period when the EMA is used and also avoids the scenario where an + * application fails quickly from execve system call due to reasons unrelated to + * a real attack. + * + * 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 a brute force attack is happening. False otherwise. + */ +static bool brute_attack_running(struct brute_stats *stats) +{ + u64 crash_period; + + spin_lock(&stats->lock); + if (stats->faults < BRUTE_MIN_FAULTS) { + spin_unlock(&stats->lock); + return false; + } + + if (stats->faults >= BRUTE_MAX_FAULTS) { + spin_unlock(&stats->lock); + return true; + } + + crash_period = jiffies64_to_msecs(stats->period); + spin_unlock(&stats->lock); + + return crash_period < BRUTE_CRASH_PERIOD_THRESHOLD; +} + +/** + * print_fork_attack_running() - Warn about a fork brute force attack. + */ +static inline void print_fork_attack_running(void) +{ + pr_warn("Fork brute force attack detected [%s]\n", current->comm); +} + +/** + * brute_manage_fork_attack() - Manage a fork brute force attack. + * @stats: Statistical data shared by all the fork hierarchy processes. + * @now: The current timestamp in jiffies. + * + * For a correct management of a fork brute force attack it is only necessary to + * update the statistics and test if an attack is happening based on these data. + * + * 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: The last crash timestamp before updating it. + */ +static u64 brute_manage_fork_attack(struct brute_stats *stats, u64 now) +{ + u64 last_fork_crash; + + last_fork_crash = brute_update_crash_period(stats, now); + if (brute_attack_running(stats)) + print_fork_attack_running(); + + return last_fork_crash; +} + +/** + * brute_get_exec_stats() - Get the exec statistics. + * @stats: When this function is called, this parameter must point to the + * current process' statistical data. When this function returns, this + * parameter points to the parent process' statistics of the fork + * hierarchy that hold the current process' statistics. + * + * To manage a brute force attack that happens through the execve system call it + * is not possible to use the statistical data hold by this process due to these + * statistics dissapear when this task is finished. In this scenario this data + * should be tracked by the statistics of a higher fork hierarchy (the hierarchy + * that contains the process that forks before the execve system call). + * + * To find these statistics the current fork hierarchy must be traversed up + * until new statistics are found. + * + * Context: Must be called with tasklist_lock and brute_stats_ptr_lock held. + */ +static void brute_get_exec_stats(struct brute_stats **stats) +{ + const struct task_struct *task = current; + struct brute_stats **p_stats; + + do { + if (!task->real_parent) { + *stats = NULL; + return; + } + + p_stats = brute_stats_ptr(task->real_parent); + task = task->real_parent; + } while (*stats == *p_stats); + + *stats = *p_stats; +} + +/** + * brute_update_exec_crash_period() - Update the exec crash period. + * @stats: When this function is called, this parameter must point to the + * current process' statistical data. When this function returns, this + * parameter points to the updated statistics (statistics that track the + * info to manage a brute force attack that happens through the execve + * system call). + * @now: The current timestamp in jiffies. + * @last_fork_crash: The last fork crash timestamp before updating it. + * + * If this is the first update of the statistics used to manage a brute force + * attack that happens through the execve system call, its last crash timestamp + * (the timestamp that shows when the execve was called) cannot be used to + * compute the crash period's EMA. Instead, the last fork crash timestamp should + * be used (the last crash timestamp of the child fork hierarchy before updating + * the crash period). This allows that in a brute force attack that happens + * through the fork system call, the exec and fork statistics are the same. In + * this situation, the mitigation method will act only in the processes that are + * sharing the fork statistics. This way, the process that forked before the + * execve system call will not be involved in the mitigation method. In this + * scenario, the parent is not responsible of the child's behaviour. + * + * 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 tasklist_lock and + * brute_stats_ptr_lock held. + * Return: -EFAULT if there are no exec statistics. Zero otherwise. + */ +static int brute_update_exec_crash_period(struct brute_stats **stats, + u64 now, u64 last_fork_crash) +{ + brute_get_exec_stats(stats); + if (!*stats) + return -EFAULT; + + spin_lock(&(*stats)->lock); + if (!(*stats)->faults) + (*stats)->jiffies = last_fork_crash; + spin_unlock(&(*stats)->lock); + + brute_update_crash_period(*stats, now); + return 0; +} + +/** + * brute_get_crash_period() - Get the application crash period. + * @stats: Statistical data shared by all the fork hierarchy processes. + * + * 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: The application crash period. + */ +static u64 brute_get_crash_period(struct brute_stats *stats) +{ + u64 crash_period; + + spin_lock(&stats->lock); + crash_period = stats->period; + spin_unlock(&stats->lock); + + return crash_period; +} + +/** + * print_exec_attack_running() - Warn about an exec brute force attack. + * @stats: Statistical data shared by all the fork hierarchy processes. + * + * The statistical data shared by all the fork hierarchy processes cannot be + * NULL. + * + * Before showing the process name it is mandatory to find a process that holds + * a pointer to the exec statistics. + * + * Context: Must be called with tasklist_lock and brute_stats_ptr_lock held. + */ +static void print_exec_attack_running(const struct brute_stats *stats) +{ + struct task_struct *p; + struct brute_stats **p_stats; + bool found = false; + + for_each_process(p) { + p_stats = brute_stats_ptr(p); + if (*p_stats == stats) { + found = true; + break; + } } + + if (WARN(!found, "No exec process\n")) + return; + + pr_warn("Exec brute force attack detected [%s]\n", p->comm); +} + +/** + * brute_manage_exec_attack() - Manage an exec brute force attack. + * @stats: Statistical data shared by all the fork hierarchy processes. + * @now: The current timestamp in jiffies. + * @last_fork_crash: The last fork crash timestamp before updating it. + * + * For a correct management of an exec brute force attack it is only necessary + * to update the exec statistics and test if an attack is happening based on + * these data. + * + * It is important to note that if the fork and exec crash periods are the same, + * the attack test is avoided. This allows that in a brute force attack that + * happens through the fork system call, the mitigation method does not act on + * the parent process of the fork hierarchy. + * + * 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 tasklist_lock and + * brute_stats_ptr_lock held. + */ +static void brute_manage_exec_attack(struct brute_stats *stats, u64 now, + u64 last_fork_crash) +{ + int ret; + struct brute_stats *exec_stats = stats; + u64 fork_period; + u64 exec_period; + + ret = brute_update_exec_crash_period(&exec_stats, now, last_fork_crash); + if (WARN(ret, "No exec statistical data\n")) + return; + + fork_period = brute_get_crash_period(stats); + exec_period = brute_get_crash_period(exec_stats); + if (fork_period == exec_period) + return; + + if (brute_attack_running(exec_stats)) + print_exec_attack_running(exec_stats); +} + +/** + * 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. + * + * 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_fatal_signal hook. + */ +static void brute_task_fatal_signal(const kernel_siginfo_t *siginfo) +{ + struct brute_stats **stats; + unsigned long flags; + u64 last_fork_crash; + u64 now = get_jiffies_64(); + + stats = brute_stats_ptr(current); + 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; + } + + last_fork_crash = brute_manage_fork_attack(*stats, now); + brute_manage_exec_attack(*stats, now, last_fork_crash); + read_unlock_irqrestore(&brute_stats_ptr_lock, flags); + read_unlock(&tasklist_lock); } /** @@ -230,6 +689,7 @@ static struct security_hook_list brute_hooks[] __lsm_ro_after_init = { LSM_HOOK_INIT(task_alloc, brute_task_alloc), 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), }; /** From patchwork Sun Feb 21 15:49:15 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385631 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-13.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 1C0C7C433DB for ; Sun, 21 Feb 2021 18:05:50 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id C676664EE6 for ; Sun, 21 Feb 2021 18:05:49 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S230048AbhBUSFd (ORCPT ); Sun, 21 Feb 2021 13:05:33 -0500 Received: from mout.gmx.net ([212.227.17.21]:40431 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229942AbhBUSFc (ORCPT ); Sun, 21 Feb 2021 13:05:32 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613930616; bh=g4HJDy+WXxT0QISwG1+uT8doXnVuhV9SQ3naNPLG790=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=CV8zAN63O/p6OuI6JkmhLL662KUco7UuDJmV1slLxEe22AlJUy2lXJWNLZtUBI0a+ DEIwd6EwQ9klVtgCC5OmFoBWNFQMzGXW3QiU3foQ4q9Ls2zcroKQ13m7xGx1hBl9l+ NqUoWv2zmJkz/H2iJi/OF2eyCN0/WLpgDzMyHpd4= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx105 [212.227.17.174]) with ESMTPSA (Nemesis) id 1MKbkC-1lYrq810ou-00L0YC; Sun, 21 Feb 2021 19:03:36 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 4/8] security/brute: Fine tuning the attack detection Date: Sun, 21 Feb 2021 16:49:15 +0100 Message-Id: <20210221154919.68050-5-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:rkNIKAEZwyV/bGfKjZkxQz4xaYgQW1TaIzaUwxyrYEzhH6Sefn2 seT1xzz616pi5f0ymMmYLx2P3NZ9vjTJOtELxqdxYmicTxNfz7lrY675CbwAyJllCsKYbG6 XniKI6DEtR49Qo1EP8Rvblu5/1dxzqbPyY/uDpurHz1ZG4HCsv+9hCDFl94n85qvviO9UNd ZQ1mEEEBPDehF3deuxqvA== X-UI-Out-Filterresults: notjunk:1; V03:K0:9ouCnryJP/4=:rv7CZb2nDQgvl6EB5ihYev FsDPcM5e5ZJRQbvUlPJ9RkiBw4m2dyM+us85cCx8Is0XLgAZ6O6rQ+kGF0Y9+fMGydfgEnYK4 DeZUQq5QAjPWJ2ECzA0Jya6axhkm8eaXZCtzuaEJChklYbuZpKwhv+N02I8RiJ8uzLp0NK/Ip wW+uofOIpC4GPQr5/JuTAScskdZkkwJ5Euzj9CxIjwPTFfkw+9j55DBKYzVi4DHhNF82hnI7i DsozO82JLZb3yspoFfggZCelP5gfW3GMeW/iM9TpRwUSMDVU5KftAGMzkXnHdlrkzYhbZBmva IuOyIjEzZApn/06PLshG6tGsI7Sz1PaL2FRNPR/Ib+OHmhgjG2m9Zg/ouqOKFwfGhOkVw7qqh zzIbrqF+cdzUFQyjyA/6P/ZNljP11ywTyfK7pUV1SydwdafN+ADwfMs777rJh7KxGlzpd2KKG G1aY8bkynfx2rIdMWWRth8K0LfqIXSO/WkMhAe9MF2hiWRqTkh7yQJM3gozrr8yyrsn/Nh2fC 0tNwn/b77ldhTdWvh++MxO37JV/AjuBkI3km7VBpJXCNqsqjvMJLhyMBI/FgH0JkuZK36CKTA K6IjTEeeWkFqjftnJT1GMobWcolu3wKQyKRjy6y0+6ngpw3rYEV/IRgyHadivCzPVNtbrEEQZ LpHyT0eFUGSe8dTgijz+EbZMJJoiJu2TEvphtOCExdylwRnHINX65qdCwmkZ/WXYr5Wf8B0Bm t/DKxrbWZ0KIX6+XnUK7o5mCLIWt/V5XRANILb/Df9tBMbWGmOvWIZ4qvZdmtIa8Fj7FS6lq6 YAnDT8DwjEMiRmzSRWFWJO9rVitwFvU+AHPNXwv9rKSSQMCy3Nhlg6NkKsbw6ek0n9a1nPwSX FAOQTHlRfWm7qONaum8A== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org 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 So, this patch checks if any of these privilege boundaries have been crossed before to compute the application crash period. Also, in every fatal crash 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. Signed-off-by: John Wood --- security/brute/brute.c | 293 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 280 insertions(+), 13 deletions(-) -- 2.25.1 diff --git a/security/brute/brute.c b/security/brute/brute.c index 645bd6e02638..8d03ea0ecac5 100644 --- a/security/brute/brute.c +++ b/security/brute/brute.c @@ -3,15 +3,25 @@ #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt #include +#include +#include +#include +#include #include #include +#include +#include #include +#include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -19,9 +29,35 @@ #include #include #include +#include +#include #include #include +#include #include +#include + +/** + * 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); } @@ -648,12 +782,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 @@ -670,18 +895,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. */ @@ -690,6 +956,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), }; /** From patchwork Sun Feb 21 15:49:16 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385957 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-13.7 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id DF4D4C433DB for ; Sun, 21 Feb 2021 18:38:52 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 98B6A6148E for ; Sun, 21 Feb 2021 18:38:52 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S230132AbhBUSiw (ORCPT ); Sun, 21 Feb 2021 13:38:52 -0500 Received: from mout.gmx.net ([212.227.17.22]:38985 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S230088AbhBUSiv (ORCPT ); Sun, 21 Feb 2021 13:38:51 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613932620; bh=SlIggBd+WqRJccgKoSY+jsgMPGho7MOSNDrdTCHcJIE=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=JSnUXw2WWYy/DcBcBvxf/eck/peQmc+Fc1fB5o+3gPF41xLs0gwYV+iNV6rUbyUlC yEkqL6t1uAMZrY/8GxV5dIA77bU0VRqkMZDRHwtSUnC2bobeLRVNF+ArmmXGUeJvoV qMU2xhfSMe+3QMQKi5REclkK4rtajjBtRptizwZk= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx104 [212.227.17.174]) with ESMTPSA (Nemesis) id 1MQvD5-1lS0sO3gRz-00NyX8; Sun, 21 Feb 2021 19:37:00 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 5/8] security/brute: Mitigate a brute force attack Date: Sun, 21 Feb 2021 16:49:16 +0100 Message-Id: <20210221154919.68050-6-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:UPdUC4a8CRZEE5wqJf0baxsrqjeCTJQ5IRIHSh41m/k51iqL/UV TNftC3ZL3GMVW3AMkaO74Msv+MJHrIxfPoePdQ07ujLmm3w2THpFJXsyR/BShj589SZbV3f ZenPq+NKddx/+ukYG/5ZCcpnMgiIqSbMWKfLkflS1HXDfA/ITxIhF5Iwyy0/LuPoplsmH1i YHsG7FF4rVjmJauyxWI8g== X-UI-Out-Filterresults: notjunk:1; V03:K0:/COfCpmUkGI=:TDb8v4wPPri8+NAYVVpP9V 5SNAtp5VnfkbCzavNXd9GRuDpX4wVOxFBCq1w1UKQ5ca7+ZUOKAA5zVF40h9M7QL3/1VqNJIU nEaBJnZhnJl8JHRWEIUArL4DQ8O1n9F4prk2GxQJYT0iY5AVqoQSv7tmI0sb/BDarbX+2XTnD TQH94UEIRc+h3BXszdMCC4WZm+w2QV7uCvwre2I4p0D7+looHaVXilzHSJ8Qp5oCAfrRz4V4I YgVe9aCIFWhpPA8e3FVCJR2FsZqXzUgF9QwILNR30CxpJ11jM+kluTUHlFdnomdyrPilV5Eqw ok0A6jiOKwZI+g6Kzg5Bq2ciDIKzaundOx8NPNMGntX3pFw4YerbrI8F7gLbi6yUmLVu8qkn6 BOP4VoVbCsTqSHOiRHFCGf11jviWZhrXMPz41gDdshM1JggEw/+AcFyTSM25aKeJsst1+lUF1 +ThWXXbVrKBqpPyWLSFGhxRyLOMlTCVqNFnqhrkKVnKMCoOOOPGe6q3yMvlPYCrponccUrzBc H5u9RKnrawPnzgWgwgzvr/xYPtZtbNmDJ+hooiGCKvw1hhVWRMXBSRqcs2kVCBDKs/fvISSQl XID8JF0jXGCTpsvKy4kPU7XFFgM7Sv9YUcXuZmTMgdiPd5/2Khs5nPW/jSx9udLInAv0E1O6T 71n0VrEWPUvimEnpVMgXqEwf8Oac/VqB7VfayS23qGbXcBHPsnR207l70XJ17APg4RgF/owYb KyP78S3e3kkoPT563Q3nHe5VfCJ7U21//c7BEJlgUTfEUoLFibudAilByxoB1C2nQnYGIe1zf mpx3SnsqK9+3FzgwpRXk6VXY8QUACLJy3t2nMb67EG1ueFvQ14r7kbVP99AQwGwpxmet1wy7d VcLnpD9EUZpVaZdOHwOw== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org In order to mitigate a brute force attack all the offending tasks involved in the attack must be killed. In other words, it is necessary to kill all the tasks that share the fork and/or exec statistical data related to the attack. Moreover, if the attack happens through the fork system call, the processes that have the same group_leader that the current task (the task that has crashed) must be avoided since they are in the path to be killed. When the SIGKILL signal is sent to the offending tasks, the function "brute_kill_offending_tasks" will be called in a recursive way from the task_fatal_signal LSM hook due to a small crash period. So, to avoid kill again the same tasks due to a recursive call of this function, it is necessary to disable the attack detection for the involved hierarchies. To disable the attack detection, set to zero the last crash timestamp and avoid to compute the application crash period in this case. Signed-off-by: John Wood --- security/brute/brute.c | 141 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 132 insertions(+), 9 deletions(-) -- 2.25.1 diff --git a/security/brute/brute.c b/security/brute/brute.c index 8d03ea0ecac5..581aba291438 100644 --- a/security/brute/brute.c +++ b/security/brute/brute.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ struct brute_cred { * @lock: Lock to protect the brute_stats structure. * @refc: Reference counter. * @faults: Number of crashes. - * @jiffies: Last crash timestamp. + * @jiffies: Last crash timestamp. If zero, the attack detection is disabled. * @period: Crash period's moving average. * @saved_cred: Saved credentials. * @network: Network activity flag. @@ -566,6 +567,125 @@ static inline void print_fork_attack_running(void) pr_warn("Fork brute force attack detected [%s]\n", current->comm); } +/** + * brute_disabled() - Test if the brute force attack detection is disabled. + * @stats: Statistical data shared by all the fork hierarchy processes. + * + * The brute force attack detection enabling/disabling is based on the last + * crash timestamp. A zero timestamp indicates that this feature is disabled. A + * timestamp greater than zero indicates that the attack detection is enabled. + * + * 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 brute force attack detection is disabled. False + * otherwise. + */ +static bool brute_disabled(struct brute_stats *stats) +{ + bool disabled; + + spin_lock(&stats->lock); + disabled = !stats->jiffies; + spin_unlock(&stats->lock); + + return disabled; +} + +/** + * brute_disable() - Disable the brute force attack detection. + * @stats: Statistical data shared by all the fork hierarchy processes. + * + * To disable the brute force attack detection it is only necessary to set the + * last crash timestamp to zero. A zero timestamp indicates that this feature is + * disabled. A timestamp greater than zero indicates that the attack detection + * is enabled. + * + * The statistical data shared by all the fork hierarchy processes cannot be + * NULL. + * + * Context: Must be called with interrupts disabled and brute_stats_ptr_lock + * and brute_stats::lock held. + */ +static inline void brute_disable(struct brute_stats *stats) +{ + stats->jiffies = 0; +} + +/** + * enum brute_attack_type - Brute force attack type. + * @BRUTE_ATTACK_TYPE_FORK: Attack that happens through the fork system call. + * @BRUTE_ATTACK_TYPE_EXEC: Attack that happens through the execve system call. + */ +enum brute_attack_type { + BRUTE_ATTACK_TYPE_FORK, + BRUTE_ATTACK_TYPE_EXEC, +}; + +/** + * brute_kill_offending_tasks() - Kill the offending tasks. + * @attack_type: Brute force attack type. + * @stats: Statistical data shared by all the fork hierarchy processes. + * + * When a brute force attack is detected all the offending tasks involved in the + * attack must be killed. In other words, it is necessary to kill all the tasks + * that share the same statistical data. Moreover, if the attack happens through + * the fork system call, the processes that have the same group_leader that the + * current task must be avoided since they are in the path to be killed. + * + * When the SIGKILL signal is sent to the offending tasks, this function will be + * called again from the task_fatal_signal hook due to a small crash period. So, + * to avoid kill again the same tasks due to a recursive call of this function, + * it is necessary to disable the attack detection for this fork hierarchy. + * + * 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 tasklist_lock and + * brute_stats_ptr_lock held. + */ +static void brute_kill_offending_tasks(enum brute_attack_type attack_type, + struct brute_stats *stats) +{ + struct task_struct *p; + struct brute_stats **p_stats; + + spin_lock(&stats->lock); + + if (attack_type == BRUTE_ATTACK_TYPE_FORK && + refcount_read(&stats->refc) == 1) { + spin_unlock(&stats->lock); + return; + } + + brute_disable(stats); + spin_unlock(&stats->lock); + + for_each_process(p) { + if (attack_type == BRUTE_ATTACK_TYPE_FORK && + p->group_leader == current->group_leader) + continue; + + p_stats = brute_stats_ptr(p); + if (*p_stats != stats) + continue; + + do_send_sig_info(SIGKILL, SEND_SIG_PRIV, p, PIDTYPE_PID); + pr_warn_ratelimited("Offending process %d [%s] killed\n", + p->pid, p->comm); + } +} + /** * brute_manage_fork_attack() - Manage a fork brute force attack. * @stats: Statistical data shared by all the fork hierarchy processes. @@ -581,8 +701,8 @@ static inline void print_fork_attack_running(void) * 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. + * Context: Must be called with interrupts disabled and tasklist_lock and + * brute_stats_ptr_lock held. * Return: The last crash timestamp before updating it. */ static u64 brute_manage_fork_attack(struct brute_stats *stats, u64 now) @@ -590,8 +710,10 @@ static u64 brute_manage_fork_attack(struct brute_stats *stats, u64 now) u64 last_fork_crash; last_fork_crash = brute_update_crash_period(stats, now); - if (brute_attack_running(stats)) + if (brute_attack_running(stats)) { print_fork_attack_running(); + brute_kill_offending_tasks(BRUTE_ATTACK_TYPE_FORK, stats); + } return last_fork_crash; } @@ -778,8 +900,10 @@ static void brute_manage_exec_attack(struct brute_stats *stats, u64 now, if (fork_period == exec_period) return; - if (brute_attack_running(exec_stats)) + if (brute_attack_running(exec_stats)) { print_exec_attack_running(exec_stats); + brute_kill_offending_tasks(BRUTE_ATTACK_TYPE_EXEC, exec_stats); + } } /** @@ -895,10 +1019,9 @@ 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")) - goto unlock; - - if (!brute_threat_model_supported(siginfo, *stats)) + if (WARN(!*stats, "No statistical data\n") || + brute_disabled(*stats) || + !brute_threat_model_supported(siginfo, *stats)) goto unlock; last_fork_crash = brute_manage_fork_attack(*stats, now); From patchwork Sun Feb 21 15:49:17 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385629 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-12.2 required=3.0 tests=BAYES_00, DATE_IN_PAST_03_06, DKIM_SIGNED,DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 11362C433DB for ; Sun, 21 Feb 2021 19:12:19 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id CD15364E04 for ; Sun, 21 Feb 2021 19:12:18 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S230233AbhBUTMQ (ORCPT ); Sun, 21 Feb 2021 14:12:16 -0500 Received: from mout.gmx.net ([212.227.15.15]:45345 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229999AbhBUTMQ (ORCPT ); Sun, 21 Feb 2021 14:12:16 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613934624; bh=Gnyw3ntDXzq5J+pulO1CLr/hLoiws9/OqNz9CVLVugo=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=SfGqHuhRbXbRGAuifza6fX39bI7JSNC/9hfgQJ2rdbCAs8LbA6MOFwMdcbehAWJlN /fQObsJaCfp2lHEh27ae+7mCrsmHiXvX259GCHBoINTn7GqilY7sRgL6PWBfHOhieW dv35h1WCHdvygp85fqwSESOhnhe20dTrsItarfIM= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx004 [212.227.17.184]) with ESMTPSA (Nemesis) id 1MFbRs-1l3Q013fVm-00HBTD; Sun, 21 Feb 2021 20:10:24 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 6/8] selftests/brute: Add tests for the Brute LSM Date: Sun, 21 Feb 2021 16:49:17 +0100 Message-Id: <20210221154919.68050-7-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:eKkNKSkGjbtNgTlJujXkC2f8Pg87GIo90kS3LmnUYSjVMr2Wqye EMwnTO1UqT+O6wAWReyrVZkSSEWxIwb2tOH5U9jtwOm6TiPWCNi4dqUtVWUr9D2gt36CABQ 1dlJK6ZO3W95BfpxPT3ayWXrPsRARBZPqQ4XtdG9IxGfwvrDlbtjnV62/wgtsZgQKphMqiK nO/bjMzNGc+9y9Q1J/gpg== X-UI-Out-Filterresults: notjunk:1; V03:K0:8VSAE4T77jc=:dNtm2eg6T/fvgg7gEAT5ii MWBI5wgS6l5MaveTzDykka9KM7e4wdOypFFspy7Y6WzkihhVuUE731CTCYszRbGzu+/69/ndp vVzA8oeCrQadsDOFgI2jASys/wD+sR3AcMALp65sam8zm3OGKmjmCEdvFLyzhykYvXdQDaax0 r1hiXxs6A75sASESoH6d5RYM53WdEKxvQ2jU55IsZWyggsrRJlnJDkxxWvRgHS35uzTKp3dDg wYaNlpnaRsbn8UF+QkVcyBATLXhol0u3dCYc/WxTB5DqyfMJNLiizR8JGxtGP2FL9/LKUh+hF f+VLBzwuc1nZW7PV1tPClLgsKLyrqPteDfSfLNNigtZcNi89qglfdxbv8TxuCReh/HRzLtmOc sFsFUEZCcj9P4GKcASOlpW1QI/skbtJyFvyh9coX5Jbest8rO39b0MGOT62rwi8ALvnnnTk9m J9umipwBY3P0f4qrpURCvzHyhv96xSdkM1utgqLMbhl2KZf7xF/5DKGCKRo1buXi5eXsdcYQp 4hwKevacFlezlofK4vQstokv60T9PyXAke8TNKQ9OANynANV1mP4sUxAn34iwXC9WZs1HXmxA ionMMVi+1wdI6URJIg88nZyPPe/JSHzrB59ev7IzGLJza1338+VBE0UyRghxZykQ+iVztrpGI OsEdfo/44T3kca4E+8svOnzf8s/pFcn1zMz2l0sgD/7Fiq89BthvtRj6odlnXMcFWM/G/HOAI 8RzZr/aJbLDyojIDTR9ELx5ConEuJpN5kNvshwq5FDyQ7raIbi/GgBCcE487N/iZcceLeHUo+ pIyIhjlWD9eO7rFv3flsYD/ODLcgh6VE6INNvzR+vBZfSev/i4bg4WsStk/tTXNQaLGW/x08i N9q6f1Pdvje58tVZwkBw== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org Add tests to check the brute LSM functionality and cover fork/exec brute force attacks crossing the following privilege boundaries: 1.- setuid process 2.- privilege changes 3.- network to local Also, as a first step check that fork/exec brute force attacks without crossing any privilege boundariy already commented doesn't trigger the detection and mitigation stage. All the fork brute force attacks are carried out via the "exec" app to avoid the triggering of the "brute" LSM over the shell script running the tests. Signed-off-by: John Wood --- tools/testing/selftests/Makefile | 1 + tools/testing/selftests/brute/.gitignore | 2 + tools/testing/selftests/brute/Makefile | 5 + tools/testing/selftests/brute/config | 1 + tools/testing/selftests/brute/exec.c | 44 ++ tools/testing/selftests/brute/test.c | 507 +++++++++++++++++++++++ tools/testing/selftests/brute/test.sh | 226 ++++++++++ 7 files changed, 786 insertions(+) create mode 100644 tools/testing/selftests/brute/.gitignore create mode 100644 tools/testing/selftests/brute/Makefile create mode 100644 tools/testing/selftests/brute/config create mode 100644 tools/testing/selftests/brute/exec.c create mode 100644 tools/testing/selftests/brute/test.c create mode 100755 tools/testing/selftests/brute/test.sh -- 2.25.1 diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile index 8a917cb4426a..e63f040e2859 100644 --- a/tools/testing/selftests/Makefile +++ b/tools/testing/selftests/Makefile @@ -2,6 +2,7 @@ TARGETS = arm64 TARGETS += bpf TARGETS += breakpoints +TARGETS += brute TARGETS += capabilities TARGETS += cgroup TARGETS += clone3 diff --git a/tools/testing/selftests/brute/.gitignore b/tools/testing/selftests/brute/.gitignore new file mode 100644 index 000000000000..1ccc45251a1b --- /dev/null +++ b/tools/testing/selftests/brute/.gitignore @@ -0,0 +1,2 @@ +exec +test diff --git a/tools/testing/selftests/brute/Makefile b/tools/testing/selftests/brute/Makefile new file mode 100644 index 000000000000..52662d0b484c --- /dev/null +++ b/tools/testing/selftests/brute/Makefile @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: GPL-2.0 +CFLAGS += -Wall -O2 +TEST_PROGS := test.sh +TEST_GEN_FILES := exec test +include ../lib.mk diff --git a/tools/testing/selftests/brute/config b/tools/testing/selftests/brute/config new file mode 100644 index 000000000000..3587b7bf6c23 --- /dev/null +++ b/tools/testing/selftests/brute/config @@ -0,0 +1 @@ +CONFIG_SECURITY_FORK_BRUTE=y diff --git a/tools/testing/selftests/brute/exec.c b/tools/testing/selftests/brute/exec.c new file mode 100644 index 000000000000..1bbe72f6e4bd --- /dev/null +++ b/tools/testing/selftests/brute/exec.c @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include +#include +#include +#include + +static __attribute__((noreturn)) void error_failure(const char *message) +{ + perror(message); + exit(EXIT_FAILURE); +} + +#define PROG_NAME basename(argv[0]) + +int main(int argc, char **argv) +{ + pid_t pid; + int status; + + if (argc < 2) { + printf("Usage: %s \n", PROG_NAME); + exit(EXIT_FAILURE); + } + + pid = fork(); + if (pid < 0) + error_failure("fork"); + + /* Child process */ + if (!pid) { + execve(argv[1], &argv[1], NULL); + error_failure("execve"); + } + + /* Parent process */ + pid = waitpid(pid, &status, 0); + if (pid < 0) + error_failure("waitpid"); + + return EXIT_SUCCESS; +} diff --git a/tools/testing/selftests/brute/test.c b/tools/testing/selftests/brute/test.c new file mode 100644 index 000000000000..44c32f446dca --- /dev/null +++ b/tools/testing/selftests/brute/test.c @@ -0,0 +1,507 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *message = "message"; + +enum mode { + MODE_NONE, + MODE_CRASH, + MODE_SERVER_CRASH, + MODE_CLIENT, +}; + +enum crash_after { + CRASH_AFTER_NONE, + CRASH_AFTER_FORK, + CRASH_AFTER_EXEC, +}; + +enum signal_from { + SIGNAL_FROM_NONE, + SIGNAL_FROM_USER, + SIGNAL_FROM_KERNEL, +}; + +struct args { + uint32_t ip; + uint16_t port; + int counter; + long timeout; + enum mode mode; + enum crash_after crash_after; + enum signal_from signal_from; + unsigned char has_counter : 1; + unsigned char has_change_priv : 1; + unsigned char has_ip : 1; + unsigned char has_port : 1; + unsigned char has_timeout : 1; +}; + +#define OPT_STRING "hm:c:s:n:Ca:p:t:" + +static void usage(const char *prog) +{ + printf("Usage: %s \n", prog); + printf("OPTIONS:\n"); + printf(" -h: Show this help and exit. Optional.\n"); + printf(" -m (crash | server_crash | client): Mode. Required.\n"); + printf("Options for crash mode:\n"); + printf(" -c (fork | exec): Crash after. Optional.\n"); + printf(" -s (user | kernel): Signal from. Required.\n"); + printf(" -n counter: Number of crashes.\n"); + printf(" Required if the option -c is used.\n"); + printf(" Not used without the option -c.\n"); + printf(" Range from 1 to INT_MAX.\n"); + printf(" -C: Change privileges before crash. Optional.\n"); + printf("Options for server_crash mode:\n"); + printf(" -a ip: Ip v4 address to accept. Required.\n"); + printf(" -p port: Port number. Required.\n"); + printf(" Range from 1 to UINT16_MAX.\n"); + printf(" -t secs: Accept timeout. Required.\n"); + printf(" Range from 1 to LONG_MAX.\n"); + printf(" -c (fork | exec): Crash after. Required.\n"); + printf(" -s (user | kernel): Signal from. Required.\n"); + printf(" -n counter: Number of crashes. Required.\n"); + printf(" Range from 1 to INT_MAX.\n"); + printf("Options for client mode:\n"); + printf(" -a ip: Ip v4 address to connect. Required.\n"); + printf(" -p port: Port number. Required.\n"); + printf(" Range from 1 to UINT16_MAX.\n"); + printf(" -t secs: Connect timeout. Required.\n"); + printf(" Range from 1 to LONG_MAX.\n"); +} + +static __attribute__((noreturn)) void info_failure(const char *message, + const char *prog) +{ + printf("%s\n", message); + usage(prog); + exit(EXIT_FAILURE); +} + +static enum mode get_mode(const char *text, const char *prog) +{ + if (!strcmp(text, "crash")) + return MODE_CRASH; + + if (!strcmp(text, "server_crash")) + return MODE_SERVER_CRASH; + + if (!strcmp(text, "client")) + return MODE_CLIENT; + + info_failure("Invalid mode option [-m].", prog); +} + +static enum crash_after get_crash_after(const char *text, const char *prog) +{ + if (!strcmp(text, "fork")) + return CRASH_AFTER_FORK; + + if (!strcmp(text, "exec")) + return CRASH_AFTER_EXEC; + + info_failure("Invalid crash after option [-c].", prog); +} + +static enum signal_from get_signal_from(const char *text, const char *prog) +{ + if (!strcmp(text, "user")) + return SIGNAL_FROM_USER; + + if (!strcmp(text, "kernel")) + return SIGNAL_FROM_KERNEL; + + info_failure("Invalid signal from option [-s]", prog); +} + +static int get_counter(const char *text, const char *prog) +{ + int counter; + + counter = atoi(text); + if (counter > 0) + return counter; + + info_failure("Invalid counter option [-n].", prog); +} + +static __attribute__((noreturn)) void error_failure(const char *message) +{ + perror(message); + exit(EXIT_FAILURE); +} + +static uint32_t get_ip(const char *text, const char *prog) +{ + int ret; + uint32_t ip; + + ret = inet_pton(AF_INET, text, &ip); + if (!ret) + info_failure("Invalid ip option [-a].", prog); + else if (ret < 0) + error_failure("inet_pton"); + + return ip; +} + +static uint16_t get_port(const char *text, const char *prog) +{ + long port; + + port = atol(text); + if ((port > 0) && (port <= UINT16_MAX)) + return htons(port); + + info_failure("Invalid port option [-p].", prog); +} + +static long get_timeout(const char *text, const char *prog) +{ + long timeout; + + timeout = atol(text); + if (timeout > 0) + return timeout; + + info_failure("Invalid timeout option [-t].", prog); +} + +static void check_args(const struct args *args, const char *prog) +{ + if (args->mode == MODE_CRASH && args->crash_after != CRASH_AFTER_NONE && + args->signal_from != SIGNAL_FROM_NONE && args->has_counter && + !args->has_ip && !args->has_port && !args->has_timeout) + return; + + if (args->mode == MODE_CRASH && args->signal_from != SIGNAL_FROM_NONE && + args->crash_after == CRASH_AFTER_NONE && !args->has_counter && + !args->has_ip && !args->has_port && !args->has_timeout) + return; + + if (args->mode == MODE_SERVER_CRASH && args->has_ip && args->has_port && + args->has_timeout && args->crash_after != CRASH_AFTER_NONE && + args->signal_from != SIGNAL_FROM_NONE && args->has_counter && + !args->has_change_priv) + return; + + if (args->mode == MODE_CLIENT && args->has_ip && args->has_port && + args->has_timeout && args->crash_after == CRASH_AFTER_NONE && + args->signal_from == SIGNAL_FROM_NONE && !args->has_counter && + !args->has_change_priv) + return; + + info_failure("Invalid use of options.", prog); +} + +static uid_t get_non_root_uid(void) +{ + struct passwd *pwent; + uid_t uid; + + while (true) { + errno = 0; + pwent = getpwent(); + if (!pwent) { + if (errno) { + perror("getpwent"); + endpwent(); + exit(EXIT_FAILURE); + } + break; + } + + if (pwent->pw_uid) { + uid = pwent->pw_uid; + endpwent(); + return uid; + } + } + + endpwent(); + printf("A user different of root is needed.\n"); + exit(EXIT_FAILURE); +} + +static inline void do_sigsegv(void) +{ + int *p = NULL; + *p = 0; +} + +static void do_sigkill(void) +{ + int ret; + + ret = kill(getpid(), SIGKILL); + if (ret) + error_failure("kill"); +} + +static void crash(enum signal_from signal_from, bool change_priv) +{ + int ret; + + if (change_priv) { + ret = setuid(get_non_root_uid()); + if (ret) + error_failure("setuid"); + } + + if (signal_from == SIGNAL_FROM_KERNEL) + do_sigsegv(); + + do_sigkill(); +} + +static void execve_crash(char *const argv[]) +{ + execve(argv[0], argv, NULL); + error_failure("execve"); +} + +static void exec_crash_user(void) +{ + char *const argv[] = { + "./test", "-m", "crash", "-s", "user", NULL, + }; + + execve_crash(argv); +} + +static void exec_crash_user_change_priv(void) +{ + char *const argv[] = { + "./test", "-m", "crash", "-s", "user", "-C", NULL, + }; + + execve_crash(argv); +} + +static void exec_crash_kernel(void) +{ + char *const argv[] = { + "./test", "-m", "crash", "-s", "kernel", NULL, + }; + + execve_crash(argv); +} + +static void exec_crash_kernel_change_priv(void) +{ + char *const argv[] = { + "./test", "-m", "crash", "-s", "kernel", "-C", NULL, + }; + + execve_crash(argv); +} + +static void exec_crash(enum signal_from signal_from, bool change_priv) +{ + if (signal_from == SIGNAL_FROM_USER && !change_priv) + exec_crash_user(); + if (signal_from == SIGNAL_FROM_USER && change_priv) + exec_crash_user_change_priv(); + if (signal_from == SIGNAL_FROM_KERNEL && !change_priv) + exec_crash_kernel(); + if (signal_from == SIGNAL_FROM_KERNEL && change_priv) + exec_crash_kernel_change_priv(); +} + +static void do_crash(enum crash_after crash_after, enum signal_from signal_from, + int counter, bool change_priv) +{ + pid_t pid; + int status; + + if (crash_after == CRASH_AFTER_NONE) + crash(signal_from, change_priv); + + while (counter > 0) { + pid = fork(); + if (pid < 0) + error_failure("fork"); + + /* Child process */ + if (!pid) { + if (crash_after == CRASH_AFTER_FORK) + crash(signal_from, change_priv); + + exec_crash(signal_from, change_priv); + } + + /* Parent process */ + counter -= 1; + pid = waitpid(pid, &status, 0); + if (pid < 0) + error_failure("waitpid"); + } +} + +static __attribute__((noreturn)) void error_close_failure(const char *message, + int fd) +{ + perror(message); + close(fd); + exit(EXIT_FAILURE); +} + +static void do_server(uint32_t ip, uint16_t port, long accept_timeout) +{ + int sockfd; + int ret; + struct sockaddr_in address; + struct timeval timeout; + int newsockfd; + + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) + error_failure("socket"); + + address.sin_family = AF_INET; + address.sin_addr.s_addr = ip; + address.sin_port = port; + + ret = bind(sockfd, (const struct sockaddr *)&address, sizeof(address)); + if (ret) + error_close_failure("bind", sockfd); + + ret = listen(sockfd, 1); + if (ret) + error_close_failure("listen", sockfd); + + timeout.tv_sec = accept_timeout; + timeout.tv_usec = 0; + ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, + (const struct timeval *)&timeout, sizeof(timeout)); + if (ret) + error_close_failure("setsockopt", sockfd); + + newsockfd = accept(sockfd, NULL, NULL); + if (newsockfd < 0) + error_close_failure("accept", sockfd); + + close(sockfd); + close(newsockfd); +} + +static void do_client(uint32_t ip, uint16_t port, long connect_timeout) +{ + int sockfd; + int ret; + struct timeval timeout; + struct sockaddr_in address; + + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) + error_failure("socket"); + + timeout.tv_sec = connect_timeout; + timeout.tv_usec = 0; + ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, + (const struct timeval *)&timeout, sizeof(timeout)); + if (ret) + error_close_failure("setsockopt", sockfd); + + address.sin_family = AF_INET; + address.sin_addr.s_addr = ip; + address.sin_port = port; + + ret = connect(sockfd, (const struct sockaddr *)&address, + sizeof(address)); + if (ret) + error_close_failure("connect", sockfd); + + ret = write(sockfd, message, strlen(message)); + if (ret < 0) + error_close_failure("write", sockfd); + + close(sockfd); +} + +#define PROG_NAME basename(argv[0]) + +int main(int argc, char **argv) +{ + int opt; + struct args args = { + .mode = MODE_NONE, + .crash_after = CRASH_AFTER_NONE, + .signal_from = SIGNAL_FROM_NONE, + .has_counter = false, + .has_change_priv = false, + .has_ip = false, + .has_port = false, + .has_timeout = false, + }; + + while ((opt = getopt(argc, argv, OPT_STRING)) != -1) { + switch (opt) { + case 'h': + usage(PROG_NAME); + return EXIT_SUCCESS; + case 'm': + args.mode = get_mode(optarg, PROG_NAME); + break; + case 'c': + args.crash_after = get_crash_after(optarg, PROG_NAME); + break; + case 's': + args.signal_from = get_signal_from(optarg, PROG_NAME); + break; + case 'n': + args.counter = get_counter(optarg, PROG_NAME); + args.has_counter = true; + break; + case 'C': + args.has_change_priv = true; + break; + case 'a': + args.ip = get_ip(optarg, PROG_NAME); + args.has_ip = true; + break; + case 'p': + args.port = get_port(optarg, PROG_NAME); + args.has_port = true; + break; + case 't': + args.timeout = get_timeout(optarg, PROG_NAME); + args.has_timeout = true; + break; + default: + usage(PROG_NAME); + return EXIT_FAILURE; + } + } + + check_args(&args, PROG_NAME); + + if (args.mode == MODE_CRASH) { + do_crash(args.crash_after, args.signal_from, args.counter, + args.has_change_priv); + } else if (args.mode == MODE_SERVER_CRASH) { + do_server(args.ip, args.port, args.timeout); + do_crash(args.crash_after, args.signal_from, args.counter, + false); + } else if (args.mode == MODE_CLIENT) { + do_client(args.ip, args.port, args.timeout); + } + + return EXIT_SUCCESS; +} diff --git a/tools/testing/selftests/brute/test.sh b/tools/testing/selftests/brute/test.sh new file mode 100755 index 000000000000..f53f26ae5b96 --- /dev/null +++ b/tools/testing/selftests/brute/test.sh @@ -0,0 +1,226 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 + +TCID="test.sh" + +KSFT_PASS=0 +KSFT_FAIL=1 +KSFT_SKIP=4 + +errno=$KSFT_PASS + +check_root() +{ + local uid=$(id -u) + if [ $uid -ne 0 ]; then + echo $TCID: must be run as root >&2 + exit $KSFT_SKIP + fi +} + +count_fork_matches() +{ + dmesg | grep "brute: Fork brute force attack detected" | wc -l +} + +assert_equal() +{ + local val1=$1 + local val2=$2 + + if [ $val1 -eq $val2 ]; then + echo "$TCID: $message [PASS]" + else + echo "$TCID: $message [FAIL]" + errno=$KSFT_FAIL + fi +} + +test_fork_user() +{ + COUNTER=20 + + old_count=$(count_fork_matches) + ./exec test -m crash -c fork -s user -n $COUNTER + new_count=$(count_fork_matches) + + message="Fork attack (user signals, no bounds crossed)" + assert_equal $old_count $new_count +} + +test_fork_kernel() +{ + old_count=$(count_fork_matches) + ./exec test -m crash -c fork -s kernel -n $COUNTER + new_count=$(count_fork_matches) + + message="Fork attack (kernel signals, no bounds crossed)" + assert_equal $old_count $new_count +} + +count_exec_matches() +{ + dmesg | grep "brute: Exec brute force attack detected" | wc -l +} + +test_exec_user() +{ + old_count=$(count_exec_matches) + ./test -m crash -c exec -s user -n $COUNTER + new_count=$(count_exec_matches) + + message="Exec attack (user signals, no bounds crossed)" + assert_equal $old_count $new_count +} + +test_exec_kernel() +{ + old_count=$(count_exec_matches) + ./test -m crash -c exec -s kernel -n $COUNTER + new_count=$(count_exec_matches) + + message="Exec attack (kernel signals, no bounds crossed)" + assert_equal $old_count $new_count +} + +assert_not_equal() +{ + local val1=$1 + local val2=$2 + + if [ $val1 -ne $val2 ]; then + echo $TCID: $message [PASS] + else + echo $TCID: $message [FAIL] + errno=$KSFT_FAIL + fi +} + +test_fork_kernel_setuid() +{ + old_count=$(count_fork_matches) + chmod u+s test + ./exec test -m crash -c fork -s kernel -n $COUNTER + chmod u-s test + new_count=$(count_fork_matches) + + message="Fork attack (kernel signals, setuid binary)" + assert_not_equal $old_count $new_count +} + +test_exec_kernel_setuid() +{ + old_count=$(count_exec_matches) + chmod u+s test + ./test -m crash -c exec -s kernel -n $COUNTER + chmod u-s test + new_count=$(count_exec_matches) + + message="Exec attack (kernel signals, setuid binary)" + assert_not_equal $old_count $new_count +} + +test_fork_kernel_change_priv() +{ + old_count=$(count_fork_matches) + ./exec test -m crash -c fork -s kernel -n $COUNTER -C + new_count=$(count_fork_matches) + + message="Fork attack (kernel signals, change privileges)" + assert_not_equal $old_count $new_count +} + +test_exec_kernel_change_priv() +{ + old_count=$(count_exec_matches) + ./test -m crash -c exec -s kernel -n $COUNTER -C + new_count=$(count_exec_matches) + + message="Exec attack (kernel signals, change privileges)" + assert_not_equal $old_count $new_count +} + +network_ns_setup() +{ + local vnet_name=$1 + local veth_name=$2 + local ip_src=$3 + local ip_dst=$4 + + ip netns add $vnet_name + ip link set $veth_name netns $vnet_name + ip -n $vnet_name addr add $ip_src/24 dev $veth_name + ip -n $vnet_name link set $veth_name up + ip -n $vnet_name route add $ip_dst/24 dev $veth_name +} + +network_setup() +{ + VETH0_NAME=veth0 + VNET0_NAME=vnet0 + VNET0_IP=10.0.1.0 + VETH1_NAME=veth1 + VNET1_NAME=vnet1 + VNET1_IP=10.0.2.0 + + ip link add $VETH0_NAME type veth peer name $VETH1_NAME + network_ns_setup $VNET0_NAME $VETH0_NAME $VNET0_IP $VNET1_IP + network_ns_setup $VNET1_NAME $VETH1_NAME $VNET1_IP $VNET0_IP +} + +test_fork_kernel_network_to_local() +{ + INADDR_ANY=0.0.0.0 + PORT=65535 + TIMEOUT=5 + + old_count=$(count_fork_matches) + ip netns exec $VNET0_NAME ./exec test -m server_crash -a $INADDR_ANY \ + -p $PORT -t $TIMEOUT -c fork -s kernel -n $COUNTER & + sleep 1 + ip netns exec $VNET1_NAME ./test -m client -a $VNET0_IP -p $PORT \ + -t $TIMEOUT + sleep 1 + new_count=$(count_fork_matches) + + message="Fork attack (kernel signals, network to local)" + assert_not_equal $old_count $new_count +} + +test_exec_kernel_network_to_local() +{ + old_count=$(count_exec_matches) + ip netns exec $VNET0_NAME ./test -m server_crash -a $INADDR_ANY \ + -p $PORT -t $TIMEOUT -c exec -s kernel -n $COUNTER & + sleep 1 + ip netns exec $VNET1_NAME ./test -m client -a $VNET0_IP -p $PORT \ + -t $TIMEOUT + sleep 1 + new_count=$(count_exec_matches) + + message="Exec attack (kernel signals, network to local)" + assert_not_equal $old_count $new_count +} + +network_cleanup() +{ + ip netns del $VNET0_NAME >/dev/null 2>&1 + ip netns del $VNET1_NAME >/dev/null 2>&1 + ip link delete $VETH0_NAME >/dev/null 2>&1 + ip link delete $VETH1_NAME >/dev/null 2>&1 +} + +check_root +test_fork_user +test_fork_kernel +test_exec_user +test_exec_kernel +test_fork_kernel_setuid +test_exec_kernel_setuid +test_fork_kernel_change_priv +test_exec_kernel_change_priv +network_setup +test_fork_kernel_network_to_local +test_exec_kernel_network_to_local +network_cleanup +exit $errno From patchwork Sun Feb 21 15:49:18 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385955 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-12.2 required=3.0 tests=BAYES_00, DATE_IN_PAST_03_06, DKIM_SIGNED,DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 48C18C433E0 for ; Sun, 21 Feb 2021 19:46:05 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 0864764E5C for ; Sun, 21 Feb 2021 19:46:05 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S230426AbhBUTpm (ORCPT ); Sun, 21 Feb 2021 14:45:42 -0500 Received: from mout.gmx.net ([212.227.17.22]:45125 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S230231AbhBUTpi (ORCPT ); Sun, 21 Feb 2021 14:45:38 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613936629; bh=BUrFkJRdMorIGyPHZ/sn3asqSQHApToumYQiv4PUyts=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=MpjfSnW4Pdc6QJtPINJLEsVea0SRtIGpuviSdtdc3miwxV3/yp4JCmQA9C7G6NhUw o27m6SvIUyJED65B/tI5yeW4OFb4Iz+SHqdcBC5fuTcGHvfPrOc+27h0EIeCQIMTIo otPxDGcdo30AI3WvbY7vzClqsvYMVzv6sGKleW0M= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx104 [212.227.17.174]) with ESMTPSA (Nemesis) id 1Mf078-1lkDBn0ZfW-00gYb0; Sun, 21 Feb 2021 20:43:49 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 7/8] Documentation: Add documentation for the Brute LSM Date: Sun, 21 Feb 2021 16:49:18 +0100 Message-Id: <20210221154919.68050-8-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:44uOoH+ZPMBDkREQ24fRDZfnWoDYdq7Pmn8oC2iWxTUnJyLPgaL qzIAyklS1rNWnmf4P9ixGM9SFMU6LZhaE2eGjyKnD+DO3fYqBUbKhGfQxRvn27+XrYhDuWD PuQNTPXJKaiJ2Tpcha+Wq7JfYhyEDCT1g2hMPjRKBibnqrOz6ijZqd9xzC1y68wFvzEMk/B Yen2bDH9LqJyGqLOuJUlw== X-UI-Out-Filterresults: notjunk:1; V03:K0:ocJtB21G5YE=:ymNwAd/gaQEDs1s7C1Hm1o NWLbzvCS7tq3pMLp2JKCjqVNcVZO7XNeYy0T/iA8dXAqisXVWkJlybr9pk3iIFbSndDo35yWA gAf6LnnPEtN2pPnN5CAvIipa48xOV/5nh/Us8fbMDiDLyIt6E6EeaaYshpFtAAqtAFezFUX5m KRXfjnOWeGX5DkAH+YhEm63oNA9IPj+wMs6IxaZImnhd+4XYoZfMYljp8gLdQpkXleWvF/2Le CLojitpjbSv49ZVB3+U+iu/yYsWQXtJ8/2k6nZ9eCEOyUzNslaYe6roWkQBVzuPAMawvnEFxK xAhZ9HZ4uPB8atvukoWa3nvRC07ewseF2a+7gC9gTi/Re888zzMCrZPcw9/Yi1Z9AkmGlEpCA fJSFLhH64dXAB+Tz2XEa3yE2vYEY9yZw0QSCi/tXl3fGQA9eQtjwVqxVBpRqvHsKojTUDiVO8 E7JdDv5TnkUTg0F9ktqB0YaSPsl7zD3AHrx4cgp2cqruItbl8a35wvkL5WJi+Sk4YqblSjxVw e5vNzb4vdY4wtFSC4yKkligjJelDWAZ5k3fBTqAhXET5oXvV1xwRCBYk0ge62DznaaMsMuM0K QFAFD37W7gb8E94i9VlWzSQpsg5jlg/4M8eSTK+a92Rj1sfxTEo77IdClEQw9cLV7AflFwnNr TzttWJEFHGqKWqW5BSzIYMclZhmeOLHMzqCgjv5ecDR6uyBw3KWLlo5Xk7JPc4jXTQP5jXilK 96Ar0C6qIUqPurjVPkG3KHeICXeFdJ3PN1RXuUQdxDjTe4JkFzgIkhoB++r9ySdyi5lNnUxhO YUMStKPRUUH/E076UPkBL03Sz76uUA2miTRZnPMh7m6oVhr+x0MfAdxSu3QPh+dlqPvG14oFi 9+SaQK9UeJw+9x58nX7g== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org Add some info detailing what is the Brute LSM, its motivation, weak points of existing implementations, proposed solutions, enabling, disabling and self-tests. Signed-off-by: John Wood --- Documentation/admin-guide/LSM/Brute.rst | 224 ++++++++++++++++++++++++ Documentation/admin-guide/LSM/index.rst | 1 + security/brute/Kconfig | 3 +- 3 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 Documentation/admin-guide/LSM/Brute.rst -- 2.25.1 diff --git a/Documentation/admin-guide/LSM/Brute.rst b/Documentation/admin-guide/LSM/Brute.rst new file mode 100644 index 000000000000..485966a610bb --- /dev/null +++ b/Documentation/admin-guide/LSM/Brute.rst @@ -0,0 +1,224 @@ +.. SPDX-License-Identifier: GPL-2.0 +=========================================================== +Brute: Fork brute force attack detection and mitigation LSM +=========================================================== + +Attacks against vulnerable userspace applications with the purpose to break ASLR +or bypass canaries traditionally use some level of brute force with the help of +the fork system call. This is possible since when creating a new process using +fork its memory contents are the same as those of the parent process (the +process that called the fork system call). So, the attacker can test the memory +infinite times to find the correct memory values or the correct memory addresses +without worrying about crashing the application. + +Based on the above scenario it would be nice to have this detected and +mitigated, and this is the goal of this implementation. Specifically the +following attacks are expected to be detected: + +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 + +So, what really needs to be detected are fork/exec brute force attacks that +cross any of the commented bounds. + + +Other implementations +===================== + +The public version of grsecurity, as a summary, is based on the idea of delaying +the fork system call if a child died due to some fatal signal (SIGSEGV, SIGBUS, +SIGKILL or SIGILL). This has some issues: + +Bad practices +------------- + +Adding delays to the kernel is, in general, a bad idea. + +Scenarios not detected (false negatives) +---------------------------------------- + +This protection acts only when the fork system call is called after a child has +crashed. So, it would still be possible for an attacker to fork a big amount of +children (in the order of thousands), then probe all of them, and finally wait +the protection time before repeating the steps. + +Moreover, this method is based on the idea that the protection doesn't act if +the parent crashes. So, it would still be possible for an attacker to fork a +process and probe itself. Then, fork the child process and probe itself again. +This way, these steps can be repeated infinite times without any mitigation. + +Scenarios detected (false positives) +------------------------------------ + +Scenarios where an application rarely fails for reasons unrelated to a real +attack. + + +This implementation +=================== + +The main idea behind this implementation is to improve the existing ones +focusing on the weak points annotated before. Basically, the adopted solution is +to detect a fast crash rate instead of only one simple crash and to detect both +the crash of parent and child processes. Also, fine tune the detection focusing +on privilege boundary crossing. And finally, as a mitigation method, kill all +the offending tasks involved in the attack instead of using delays. + +To achieve this goal, and going into more details, this implementation is based +on the use of some statistical data shared across all the processes that can +have the same memory contents. Or in other words, a statistical data shared +between all the fork hierarchy processes after an execve system call. + +The purpose of these statistics is, basically, collect all the necessary info +to compute the application crash period in order to detect an attack. This crash +period is the time between the execve system call and the first fault or the +time between two consecutive faults, but this has a drawback. If an application +crashes twice in a short period of time for some reason unrelated to a real +attack, a false positive will be triggered. To avoid this scenario the +exponential moving average (EMA) is used. This way, the application crash period +will be a value that is not prone to change due to spurious data and follows the +real crash period. + +To detect a brute force attack it is necessary that the statistics shared by all +the fork hierarchy processes be updated in every fatal crash and the most +important data to update is the application crash period. + +There are two types of brute force attacks that need to be detected. The first +one is an attack that happens through the fork system call and the second one is +an attack that happens through the execve system call. The first type uses the +statistics shared by all the fork hierarchy processes, but the second type +cannot use this statistical data due to these statistics dissapear when the +involved tasks finished. In this last scenario the attack info should be tracked +by the statistics of a higher fork hierarchy (the hierarchy that contains the +process that forks before the execve system call). + +Moreover, these two attack types have two variants. A slow brute force attack +that is detected if a maximum number of faults per fork hierarchy is reached and +a fast brute force attack that is detected if the application crash period falls +below a certain threshold. + +Exponential moving average (EMA) +-------------------------------- + +This kind of average defines a weight (between 0 and 1) for the new value to add +and applies the remainder of the weight to the current average value. This way, +some spurious data will not excessively modify the average and only if the new +values are persistent, the moving average will tend towards them. + +Mathematically the application crash period's EMA can be expressed as follows: + +period_ema = period * weight + period_ema * (1 - weight) + +Related to the attack detection, the EMA must guarantee that not many crashes +are needed. To demonstrate this, the scenario where an application has been +running without any crashes for a month will be used. + +The period's EMA can be written now as: + +period_ema[i] = period[i] * weight + period_ema[i - 1] * (1 - weight) + +If the new crash periods have insignificant values related to the first crash +period (a month in this case), the formula can be rewritten as: + +period_ema[i] = period_ema[i - 1] * (1 - weight) + +And by extension: + +period_ema[i - 1] = period_ema[i - 2] * (1 - weight) +period_ema[i - 2] = period_ema[i - 3] * (1 - weight) +period_ema[i - 3] = period_ema[i - 4] * (1 - weight) + +So, if the substitution is made: + +period_ema[i] = period_ema[i - 1] * (1 - weight) +period_ema[i] = period_ema[i - 2] * pow((1 - weight) , 2) +period_ema[i] = period_ema[i - 3] * pow((1 - weight) , 3) +period_ema[i] = period_ema[i - 4] * pow((1 - weight) , 4) + +And in a more generic form: + +period_ema[i] = period_ema[i - n] * pow((1 - weight) , n) + +Where n represents the number of iterations to obtain an EMA value. Or in other +words, the number of crashes to detect an attack. + +So, if we isolate the number of crashes: + +period_ema[i] / period_ema[i - n] = pow((1 - weight), n) +log(period_ema[i] / period_ema[i - n]) = log(pow((1 - weight), n)) +log(period_ema[i] / period_ema[i - n]) = n * log(1 - weight) +n = log(period_ema[i] / period_ema[i - n]) / log(1 - weight) + +Then, in the commented scenario (an application has been running without any +crashes for a month), the approximate number of crashes to detect an attack +(using the implementation values for the weight and the crash period threshold) +is: + +weight = 7 / 10 +crash_period_threshold = 30 seconds + +n = log(crash_period_threshold / seconds_per_month) / log(1 - weight) +n = log(30 / (30 * 24 * 3600)) / log(1 - 0.7) +n = 9.44 + +So, with 10 crashes for this scenario an attack will be detected. If these steps +are repeated for different scenarios and the results are collected: + +1 month without any crashes ----> 9.44 crashes to detect an attack +1 year without any crashes -----> 11.50 crashes to detect an attack +10 years without any crashes ---> 13.42 crashes to detect an attack + +However, this computation has a drawback. The first data added to the EMA not +obtains a real average showing a trend. So the solution is simple, the EMA needs +a minimum number of data to be able to be interpreted. This way, the case where +a few first faults are fast enough followed by no crashes is avoided. + +Per system enabling/disabling +----------------------------- + +This feature can be enabled at build time using the CONFIG_SECURITY_FORK_BRUTE +option or using the visual config application under the following menu: + +Security options ---> Fork brute force attack detection and mitigation + +Also, at boot time, this feature can be disable too, by changing the "lsm=" boot +parameter. + +Kernel selftests +---------------- + +To validate all the expectations about this implementation, there is a set of +selftests. This tests cover fork/exec brute force attacks crossing the following +privilege boundaries: + +1.- setuid process +2.- privilege changes +3.- network to local + +Also, there are some tests to check that fork/exec brute force attacks without +crossing any privilege boundariy already commented doesn't trigger the detection +and mitigation stage. + +To build the tests: +make -C tools/testing/selftests/ TARGETS=brute + +To run the tests: +make -C tools/testing/selftests TARGETS=brute run_tests + +To package the tests: +make -C tools/testing/selftests TARGETS=brute gen_tar diff --git a/Documentation/admin-guide/LSM/index.rst b/Documentation/admin-guide/LSM/index.rst index a6ba95fbaa9f..1f68982bb330 100644 --- a/Documentation/admin-guide/LSM/index.rst +++ b/Documentation/admin-guide/LSM/index.rst @@ -41,6 +41,7 @@ subdirectories. :maxdepth: 1 apparmor + Brute LoadPin SELinux Smack diff --git a/security/brute/Kconfig b/security/brute/Kconfig index 1bd2df1e2dec..334d7e88d27f 100644 --- a/security/brute/Kconfig +++ b/security/brute/Kconfig @@ -7,6 +7,7 @@ config SECURITY_FORK_BRUTE vulnerable userspace processes. The detection method is based on the application crash period and as a mitigation procedure all the offending tasks are killed. Like capabilities, this security module - stacks with other LSMs. + stacks with other LSMs. Further information can be found in + Documentation/admin-guide/LSM/Brute.rst. If you are unsure how to answer this question, answer N. From patchwork Sun Feb 21 15:49:19 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: John Wood X-Patchwork-Id: 385628 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-12.2 required=3.0 tests=BAYES_00, DATE_IN_PAST_03_06, DKIM_SIGNED,DKIM_VALID,FREEMAIL_FORGED_FROMDOMAIN,FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_GIT autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id DFDB5C433DB for ; Sun, 21 Feb 2021 20:19:21 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id A42BE64E5C for ; Sun, 21 Feb 2021 20:19:21 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S230296AbhBUUTG (ORCPT ); Sun, 21 Feb 2021 15:19:06 -0500 Received: from mout.gmx.net ([212.227.15.19]:48833 "EHLO mout.gmx.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S230174AbhBUUTF (ORCPT ); Sun, 21 Feb 2021 15:19:05 -0500 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=gmx.net; s=badeba3b8450; t=1613938631; bh=cFGcwB+oIMoQD2nm971NYoY7QQ0QXu1e9XPHJAzwb7k=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:In-Reply-To:References; b=GOZwDesgWdfw4DZi7UV0SSNxWXqWTaptbzoRgQq/dOd+ZSmTmVKCJj5Zm9l0x7Uks ZFDoacsmwJQMbMZImrM3XVv7ODwsjhmvAQaWact2YeTIQG8k+NmIC1CAXjYP8YOpYA xzTn2vTksJsdIuiDFDno8FDV3C94wNqyAUagcA44= X-UI-Sender-Class: 01bb95c1-4bf8-414a-932a-4f6e2808ef9c Received: from localhost.localdomain ([83.52.229.153]) by mail.gmx.net (mrgmx005 [212.227.17.184]) with ESMTPSA (Nemesis) id 1MplXz-1ldI6c2LhF-00qEN2; Sun, 21 Feb 2021 21:17:11 +0100 From: John Wood To: Kees Cook , Jann Horn , Randy Dunlap , Jonathan Corbet , James Morris , Shuah Khan Cc: John Wood , "Serge E. Hallyn" , Greg Kroah-Hartman , linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, linux-security-module@vger.kernel.org, linux-kselftest@vger.kernel.org Subject: [PATCH v3 8/8] MAINTAINERS: Add a new entry for the Brute LSM Date: Sun, 21 Feb 2021 16:49:19 +0100 Message-Id: <20210221154919.68050-9-john.wood@gmx.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20210221154919.68050-1-john.wood@gmx.com> References: <20210221154919.68050-1-john.wood@gmx.com> MIME-Version: 1.0 X-Provags-ID: V03:K1:A9lV3V9nsLMCf5CmMu6k/qVFGT1BkfgTJJSixdrK1IpFF6pUB7/ peNPmEQle3ecEnl7Bk6MLydWlFKHNCF7Fzm8lkP9Vv5TR7vuedJis7NBbrz6WXPwAMNmtnC zdnbYQ9+zGzQlaSIt4JlxwSK4lg/pdpElRrY/c203DLGnQFE16MLhZdE1jJmWGAWY6TqxYD ZrIGTZhAjXr/UJ9yfMS3w== X-UI-Out-Filterresults: notjunk:1; V03:K0:MM733bPvwtU=:tsjxnsOkeC5P2GMTGa4ajS XPH1WOi3gOxUnh1Zk8XgsE/gheE3qzBqu0wpZdjzHwOJoUaHitd6jn6B711VuRlq1t0Xsn4Kq gZReMFfirnCNVrL+UVzx9SmD7Vrfa6fuJtPIk3VOe8kPu+1Ac7PKZ/h46FvNEJEJPmTVeOFVE 9nlqNuPKlH4Z/31jN8j5BbUvJIphjMr3KQ7fPIQP2znm8K90lBUR5/AAPhC07jrkgxaUKO/2a SH3WtcrX+J7xPY7mgPGCCNwz4Lp4o2zXhv2Zx9LAxZpnS+xFBhQYaHTg07LrQAK7B7QPMtkc3 Pxanl08Z700pqccI4ALdspjqtUrpMQMUtFLdKZ9p1+ZGDqwk14jQVGqafsQp50GnqKm/yFoaT TH8ST0/kDC831b6BvHkitx/iqhmDQvVljGqlXWqSCF/+Z+nZSfgWZa/taJB6Ot0fFicNvuJ7s gE8wtxOm/n1anWlGPhqmeBSeiudrcIF8kT2H/Q4PbMXus3Wqzwxe7cCC0HCJYCMlCpaaC32dC RAkgqEGi39VdLlsvLpc5bk/BMzyks7B0+IP4VPO1ewId0lxytyAqNxx/Y1hOA5dkiPO7of2Fg iproAeTELRRF+4dqU2JL/FSKlAuSDcGIT6jXN6X8IQFf30Dsv07dYYOJxf9N49EpsVZOVxeLL dx621j4LVw52JkTVToYVOVSd0fXvx4JQgaU/AYdv9/in3rTz9GUfu6pnYb7p7aAPcy2Q+DICM CZQJCaDuFg5vvxlecRE/mOkApyK3r3ZuMF5b53xforWIxLPTW+3JVXTGtfHxyuYyH17XZ+M5y TWO54IvBjlu1APw3zoqqDHC3we1y5f33bhLhp7TrWRgUVKL2Q0JYTTXvf7XCtb5cdZhN0oIJ7 PBo417v0KOadm/NriYTg== Precedence: bulk List-ID: X-Mailing-List: linux-kselftest@vger.kernel.org In order to maintain the code for the Brute LSM add a new entry to the maintainers list. Signed-off-by: John Wood --- MAINTAINERS | 7 +++++++ 1 file changed, 7 insertions(+) -- 2.25.1 diff --git a/MAINTAINERS b/MAINTAINERS index bfc1b86e3e73..a88327198474 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3760,6 +3760,13 @@ L: netdev@vger.kernel.org S: Supported F: drivers/net/ethernet/brocade/bna/ +BRUTE SECURITY MODULE +M: John Wood +S: Maintained +F: Documentation/admin-guide/LSM/Brute.rst +F: security/brute/ +F: tools/testing/selftests/brute/ + BSG (block layer generic sg v4 driver) M: FUJITA Tomonori L: linux-scsi@vger.kernel.org