| From 77f549b03ecde43e926482092ecad6cd6f3cda7b Mon Sep 17 00:00:00 2001 |
| From: Mike Frysinger <vapier@chromium.org> |
| Date: Sat, 12 Dec 2015 15:18:25 -0500 |
| Subject: [PATCH] do not source/exec scripts on noexec mount points |
| |
| Today, if you have a script that lives on a noexec mount point, the |
| kernel will reject attempts to run it directly: |
| $ printf '#!/bin/sh\necho hi\n' > /dev/shm/test.sh |
| $ chmod a+rx /dev/shm/test.sh |
| $ /dev/shm/test.sh |
| bash: /dev/shm/test.sh: Permission denied |
| |
| But bash itself has no problem running this file: |
| $ bash /dev/shm/test.sh |
| hi |
| Or with letting other scripts run this file: |
| $ bash -c '. /dev/shm/test.sh' |
| hi |
| Or with reading the script from stdin: |
| $ bash </dev/shm/test.sh |
| hi |
| Or indirect loading: |
| $ ln -s test.sh /dev/shm/.profile |
| $ HOME=/dev/shm bash -l |
| hi |
| |
| This detracts from the security of the overall system. People writing |
| scripts sometimes want to save/restore state (like variables) and will |
| restore the content from a noexec point using the aforementioned source |
| command without realizing that it executes code too. Of course their |
| code is wrong, but it would be nice if the system would catch & reject |
| it explicitly to stave of inadvertent usage. |
| |
| This is not a perfect solution as it can still be worked around by |
| inlining the code itself: |
| $ bash -c "$(cat /dev/shm/test.sh)" |
| hi |
| Or forcing interactive mode: |
| $ bash -i </dev/shm/test.sh |
| hi |
| Or piping it: |
| $ cat /dev/shm/test.sh | bash |
| hi |
| |
| But this makes things a bit harder for malicious attackers (depending |
| how exactly they've managed to escalate), and it also helps developers |
| avoid getting it wrong in the first place. |
| |
| There are some compile-time knobs provided: |
| * SHELL_IGNORE_NOEXEC: If defined, allow scripts on noexec mounts. |
| * SHELL_NOEXEC_CRASH_REPORTS: If defined, generate crash reports when |
| noexec scripts are attempted. |
| * SHELL_NOEXEC_REPORT_ONLY: If defined, don't halt script execution, |
| only emit warnings to stderr. |
| |
| URL: https://crbug.com/569168 |
| URL: https://chromium.googlesource.com/chromiumos/docs/+/master/security/noexec_shell_scripts.md |
| --- |
| builtins/evalfile.c | 7 +++++ |
| config.h.in | 3 +++ |
| configure | 2 +- |
| configure.ac | 2 +- |
| shell.c | 64 +++++++++++++++++++++++++++++++++++++++++++++ |
| shell.h | 6 +++++ |
| 6 files changed, 82 insertions(+), 2 deletions(-) |
| |
| diff --git a/builtins/evalfile.c b/builtins/evalfile.c |
| index 316b794..10ed7a6 100644 |
| --- a/builtins/evalfile.c |
| +++ b/builtins/evalfile.c |
| @@ -32,6 +32,10 @@ |
| #include <signal.h> |
| #include <errno.h> |
| |
| +#if defined (HAVE_SYS_STATVFS_H) |
| +# include <sys/statvfs.h> |
| +#endif |
| + |
| #include "../bashansi.h" |
| #include "../bashintl.h" |
| |
| @@ -161,6 +165,9 @@ file_error_and_exit: |
| return ((flags & FEVAL_BUILTIN) ? EXECUTION_FAILURE : -1); |
| } |
| |
| + if (interactive_shell == 0) |
| + check_noexec (fd, filename); |
| + |
| if (S_ISREG (finfo.st_mode) && file_size <= SSIZE_MAX) |
| { |
| string = (char *)xmalloc (1 + file_size); |
| diff --git a/config.h.in b/config.h.in |
| index a5ad9e7..84f9ab5 100644 |
| --- a/config.h.in |
| +++ b/config.h.in |
| @@ -1045,6 +1045,9 @@ |
| /* Define if you have the <sys/stat.h> header file. */ |
| #undef HAVE_SYS_STAT_H |
| |
| +/* Define if you have <sys/statvfs.h>. */ |
| +#undef HAVE_SYS_STATVFS_H |
| + |
| /* Define if you have the <sys/stream.h> header file. */ |
| #undef HAVE_SYS_STREAM_H |
| |
| diff --git a/configure b/configure |
| index dc57ff9..9a7845e 100755 |
| --- a/configure |
| +++ b/configure |
| @@ -4599,7 +4599,7 @@ fi |
| |
| # On IRIX 5.3, sys/types and inttypes.h are conflicting. |
| for ac_header in sys/types.h sys/stat.h stdlib.h string.h memory.h strings.h \ |
| - inttypes.h stdint.h unistd.h |
| + inttypes.h stdint.h unistd.h sys/statvfs.h |
| do : |
| as_ac_Header=`$as_echo "ac_cv_header_$ac_header" | $as_tr_sh` |
| ac_fn_c_check_header_compile "$LINENO" "$ac_header" "$as_ac_Header" "$ac_includes_default |
| diff --git a/configure.ac b/configure.ac |
| index ce4e9b6..3b13cd9 100644 |
| --- a/configure.ac |
| +++ b/configure.ac |
| @@ -702,7 +702,7 @@ AC_CHECK_HEADERS(unistd.h stdlib.h stdarg.h varargs.h limits.h string.h \ |
| stdbool.h stddef.h stdint.h netdb.h pwd.h grp.h strings.h \ |
| regex.h syslog.h ulimit.h) |
| AC_CHECK_HEADERS(sys/pte.h sys/stream.h sys/select.h sys/file.h sys/ioctl.h \ |
| - sys/param.h sys/socket.h sys/stat.h \ |
| + sys/param.h sys/socket.h sys/stat.h sys/statvfs.h \ |
| sys/time.h sys/times.h sys/types.h sys/wait.h) |
| AC_CHECK_HEADERS(netinet/in.h arpa/inet.h) |
| |
| diff --git a/shell.c b/shell.c |
| index 45b77f9..6eaf165 100644 |
| --- a/shell.c |
| +++ b/shell.c |
| @@ -46,6 +46,10 @@ |
| # include <unistd.h> |
| #endif |
| |
| +#if defined (HAVE_SYS_STATVFS_H) |
| +# include <sys/statvfs.h> |
| +#endif |
| + |
| #include "bashintl.h" |
| |
| #define NEED_SH_SETLINEBUF_DECL /* used in externs.h */ |
| @@ -740,6 +744,7 @@ main (argc, argv, env) |
| { |
| /* In this mode, bash is reading a script from stdin, which is a |
| pipe or redirected file. */ |
| + check_noexec (0, "stdin"); |
| #if defined (BUFFERED_INPUT) |
| default_buffered_input = fileno (stdin); /* == 0 */ |
| #else |
| @@ -1467,6 +1472,63 @@ start_debugger () |
| #endif |
| } |
| |
| +#ifndef SHELL_IGNORE_NOEXEC |
| + |
| +/* |
| + * We'll fork a child who will then crash. This will signal to the system |
| + * that we ran into a problem without actually halting the script. This is |
| + * useful for tracking down users on releases w/out breaking them. |
| + */ |
| +static void |
| +maybe_generate_crash_report (void) |
| +{ |
| +# ifdef SHELL_NOEXEC_CRASH_REPORTS |
| + if (fork () == 0) |
| + abort (); |
| +# endif |
| +} |
| + |
| +/* |
| + * See if the fd is coming from a noexec partition. |
| + * If so, fall over and complain. |
| + */ |
| +void |
| +check_noexec (int fd, const char *source) |
| +{ |
| +#if defined (HAVE_SYS_STATVFS_H) && defined (ST_NOEXEC) |
| + /* Make sure the file isn't on a noexec mount point. */ |
| + struct statvfs stvfs; |
| + |
| + if (fstatvfs (fd, &stvfs) == -1) |
| + { |
| + maybe_generate_crash_report (); |
| + |
| + sys_error ("Can't fstatvfs %s", source); |
| +# ifdef SHELL_NOEXEC_REPORT_ONLY |
| + /* Clear the flag to avoid the code path below. */ |
| + stvfs.f_flag = 0; |
| +# else |
| + exit_shell (EX_NOTFOUND); |
| +# endif |
| + } |
| + |
| + if (stvfs.f_flag & ST_NOEXEC) |
| + { |
| + const char docs[] = "https://chromium.googlesource.com/chromiumos/docs/+/master/security/noexec_shell_scripts.md"; |
| + maybe_generate_crash_report (); |
| + |
| +# ifdef SHELL_NOEXEC_REPORT_ONLY |
| + internal_warning ("%s: warning: script from noexec mount; see %s", source, docs); |
| +# else |
| + internal_error ("Refusing to exec %s from noexec mount; see %s", source, docs); |
| + exit_shell (EX_NOEXEC); |
| +# endif |
| + } |
| +#endif |
| +} |
| + |
| +#endif |
| + |
| static int |
| open_shell_script (script_name) |
| char *script_name; |
| @@ -1604,6 +1666,8 @@ open_shell_script (script_name) |
| SET_CLOSE_ON_EXEC (fileno (default_input)); |
| #endif /* !BUFFERED_INPUT */ |
| |
| + check_noexec (fd, filename); |
| + |
| /* Just about the only way for this code to be executed is if something |
| like `bash -i /dev/stdin' is executed. */ |
| if (interactive_shell && fd_is_tty) |
| diff --git a/shell.h b/shell.h |
| index ce08879..e8b8ad0 100644 |
| --- a/shell.h |
| +++ b/shell.h |
| @@ -193,3 +193,9 @@ extern void restore_parser_state __P((sh_parser_state_t *)); |
| |
| extern sh_input_line_state_t *save_input_line_state __P((sh_input_line_state_t *)); |
| extern void restore_input_line_state __P((sh_input_line_state_t *)); |
| + |
| +#ifndef SHELL_IGNORE_NOEXEC |
| +extern void check_noexec __P((int, const char *)); |
| +#else |
| +static inline void check_noexec (int fd, const char *source) {} |
| +#endif |
| -- |
| 2.29.2 |
| |