blob: c00dc7b0827c85e8ae65177411cb95e6307e82f9 [file] [log] [blame] [view]
# Shell scripts & noexec mounts
*** note
**Warning: This document is old & has moved. Please update any links:**<br>
https://chromium.googlesource.com/chromiumos/docs/+/HEAD/security/noexec_shell_scripts.md
***
Chrome OS has added logic to the shells we ship (e.g. [dash] & [bash]) to detect
when code is being run from `noexec` partitions.
This can cause trouble for code that previously worked, or continues to work on
systems other than Chrome OS.
Here we'll dive into the technical details and how to address common problems.
[TOC]
## The Journey
### What are noexec mounts?
When creating mounts in Linux, you can add `noexec` to the options in order to
get a path where the kernel will reject attempts to execute code.
Instead, you'll get `EACCES` (Permission denied) errors whenever you try.
For example:
```sh
# /bin/hostname
localhost
# cp /bin/hostname /var/
# /var/hostname
-bash: /var/hostname: Permission denied
# mount /var -o remount,exec
# /var/hostname
localhost
```
### Why do we want noexec mounts?
noexec is extremely useful for security as it means we can strongly separate
read-only code from writable data.
This makes it harder for attackers as they won't be able to download arbitrary
programs from the network and execute them directly, or to copy the programs to
a writable location and then trick a privileged service into executing them.
Both of those are common techniques when attacking a system & building up an
[exploit chain].
In Chrome OS, programs need to be able save state somewhere (e.g. log files,
user preferences, network settings, etc...) so they persist across reboots.
If we were entirely read-only all the time, then users would have to reenter
all their settings everytime their system booted.
Thus we strive to have only one place where code may be executed: the read-only
rootfs that is cryptographically verified at all times.
All other paths on the system are mounted with `noexec` settings.
Now if an attacker downloads programs (even as root!) to writable locations like
`/var` or `/tmp` or `/home`, any attempts to run that code will be rejected.
### What about interpreted code?
So far we've talked about the scenario where the kernel executes the code.
In other words, it has an executable binary format (a.k.a. binfmt) handler that
is responsible for parsing & executing things directly.
This applies to [ELF] files, but what about interpreted code (a.k.a. scripts)?
While the kernel will process the [shebang] in scripts and enforce the noexec
setting correctly for them, that's where it stops.
So executing scripts directly fail (which is great!):
```sh
# printf '#!/bin/sh\necho hi\n' > /var/test.sh
# chmod a+rx /var/test.sh
# /var/test.sh
-bash: /var/test.sh: Permission denied
```
But executing scripts indirectly still works (which is bad!):
```sh
# sh /var/test.sh
hi
```
This scenario plays out for all programs that accept dynamic code at runtime.
So not only shell scripts, but also awk, Python, Perl, etc...
Which means in Chrome OS, we're somewhat back where we started: attackers are
able to download arbitrary shell scripts to a writable location and then run the
right language interpreter against it.
### What protection do interpreters have to offer?
The behavior in all shells today is as we describe above -- they will gladly
execute any scripts given to them regardless of where they live.
This isn't really a bug for them, it's simply not a use case they care about.
### How does Chrome OS handle this in general?
We start off by removing as many interpreters as possible from the OS.
By default, we block attempts to install packages like Python and Perl.
For many tools (e.g. [sed] and [mawk]), we enable their sandbox mode at build
time which allows us to keep them on the system and execute arbitrary scripts,
but only as pipeline tools: there is no support in the language for running
arbitrary programs or opening files on the system for reading/writing.
Basically all they can do is read stdin, transform the stream, and then write it
back out to stdout.
We recognize that disallowing shell scripts entirely is a significant hurdle to
overall development velocity.
We also have a bit of significant legacy code in the system which is still
written in shell code.
Thus we have policies like "shell scripts are OK for small/trivial uses, but if
it grows too large, it needs to be rewritten in a proper compiled language".
### How does Chrome OS handle shell scripts specifically?
Now we get to the heart of the matter :).
Chrome OS patches [dash] and [bash] to detect the origin of the code at runtime
before it will actually execute it.
If it detects the shell script lives on a noexec partition, it will abort!
So now we get closer parity to the rest of the system.
If we revisit one of our earlier examples that passed:
```sh
# sh /var/test.sh
sh: 0: Refusing to exec /var/test.sh
```
### But what about {XXX} edge case?
We're well aware that the current implementation is not foolproof.
It will correctly detect & catch attempts at direct execution, but it doesn't
detect indirect runtime evaluation.
For example, this still works:
```sh
# sh -c "$(cat /var/test.sh)"
hi
```
However, our goal with these changes isn't necessarily to be bulletproof
(although we will expand and catch more scenarios when feasible), but to catch
a lot of common and accidental mistakes.
So the fact that we don't detect 100% of every bad usage is not a good argument
for never detecting any misuse.
Keep in mind that not all (maybe not even most?) developers are experts when it
comes to writing shell code and possible implications of, what appears to be,
fairly harmless usage in any other system.
For example, developers are used to writing things like:
```sh
#!/bin/sh
# A boot script that helps initialize the device.
# Load the state from our last run.
. "/var/lib/foo/previous-settings"
# Change runtime behavior based on our program's specific settings.
echo "${A_PREVIOUS_SETTING}"
...do more...
# Save our state for next boot.
cat <<EOF >"/var/lib/foo/previous-settings"
A_PREVIOUS_SETTING="${A_PREVIOUS_SETTING}"
SOMETHING="${SOMETHING}"
EOF
```
Can you see anything wrong with this?
The answer is that this opens the system to persistent exploitation, and often a
persistent root exploit if the shell script is a service run during boot.
If an attacker managed to write a plain text file to that path, then that code
would fully execute as complete shell script!
Nothing in the `.` command (which is the same `source`) says that the file may
only contain variables.
It could just as easily be `exec /bin/sh /var/bad-script.sh` which means the
rest of our boot script would never execute!
Before you wonder, yes, developers have attempted to write code exactly like
this in Chrome OS and ship it to all our users.
The author simply had no idea that the shell code could be so powerful and
dangerous.
Thankfully, these get caught during code review, but that isn't guaranteed.
## How do I fix my code?
Now that we've covered the background, let's get into common scenarios that
developers will likely run into.
*** aside
It might be a bit heavy handed, but if you're running into problems with running
shell code from a noexec partition, then chances are good that your shell script
is already at the point where you shouldn't be using shell.
Please strongly consider switching languages, especially due to the fact that it
is rarely feasible to unittest shell scripts.
All code in Chrome OS should have proper unittest coverage so we can confidently
ship a stable & reliable system to our millions of users.
***
### How to save/restore settings?
For people who want to save & restore a set of "simple" variables (usually ones
with values like "0" or "1" or "true" or "false" or similar), there's a few
different possibilities.
#### Use separate files.
Instead of writing a key/value store to a shell script, use the filesystem as
your key/value store.
After all, filesystems are "just" databases!
This works when you have a small/limited number of settings.
So instead of a file saved at `/var/lib/foo/settings` like:
```
VAR=1
FOO=yes
```
Split them up into separate files:
* `/var/lib/foo/settings/VAR` will have the content `1`
* `/var/lib/foo/settings/FOO` will have the content `yes`
You can then read/write them as needed:
```sh
# Load the value and ignore errors if it doesn't exist.
VAR="$(cat /var/lib/foo/settings/VAR 2>/dev/null || :)"
# Save the value later on.
printf '%s' "${VAR}" >"/var/lib/foo/settings/VAR"
```
### How to run code in dev mode?
Dev mode is where users take their device and put it into a mode where they can
get full access to their device (i.e. unlock it).
In this case, it's common for developers to write their own personal scripts to
noexec paths and then try to directly run them.
This will no longer work.
However, in dev mode, Chrome OS already guarantees that `/usr/local` will be
created for users to do whatever they want.
This includes mounting it as executable.
So copy all your shell scripts there and run them directly without problems.
We also add `/usr/local/bin` to the shell's default `$PATH`, so you can put your
custom scripts there and execute them without having to use a full path.
```sh
# printf '#!/bin/sh\necho hi\n' > /usr/local/bin/test.sh
# chmod a+rx /usr/local/bin/test.sh
# which test.sh
/usr/local/bin/test.sh
# test.sh
hi
```
### How to run code in dev or test images?
The answer is the same as dev mode -- use `/usr/local` for all arbitrary code.
Historically we would would remount `/home` and `/tmp` as executable in test
images, but that must no longer be relied upon.
It creates a test system that does not match the behavior of the code that we
ship to all our users!
### How to run crouton?
[crouton] is affected in the same way as any other script. The [crouton README]
has been updated to detail the new recommended steps.
[dash]: https://en.wikipedia.org/wiki/Debian_Almquist_shell
[bash]: https://www.gnu.org/software/bash/
[ELF]: https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
[exploit chain]: https://static1.squarespace.com/static/5419be5de4b062d1159bbe31/t/546b91d6e4b0e010426d60c8/1416335830344/Examining+the+Exploit-Chain.pdf
[mawk]: https://invisible-island.net/mawk/
[sed]: https://www.gnu.org/software/sed/
[shebang]: https://en.wikipedia.org/wiki/Shebang_(Unix)
[crouton]: https://github.com/dnschneid/crouton
[crouton readme]: https://github.com/dnschneid/crouton/blob/HEAD/README.md