2. Definitions, least privilege, confused deputies
5 min read
Security mechanisms must be designed for ‘adversial’ situations where someone is actively trying to break them.
Common security properties
Fault-tolerance: protecting processes (or other resources) from accidental error (incompetence)
Security: protecting processes or other resources from adversarial (malicious) behaviour
Confidentiality: information is not disclosed to unintended parties
Integrity: information is not modifiable by unintended parties (‘tamper-proof’) or at least not without giveaway (‘tamper-evident’)
Availability: malicious actors can’t deprive others of the service (e.g. DoS attack, forkbomb)
‘Modification’ includes unintended creation, e.g. fake / spurious data.
These properties are only ever provided up to certain assumptions (‘attack model’) and budget (‘attacker resources’). They may be traded for performance or convenience.
The policies we can formulate in terms of access control lists or file-descriptor-like capabilities are about confidentiality and integrity.
To protect availability, i.e. prevent ‘DoS attacks’, we need to add resource limits:
set a max no. of processes, allocated memory, etc per user
limit rate of process creation per user
In this scenario, we make the assumption (attack model) that it is hard for an attacker to get more than one account.
Human Bottleneck
Enforcing policies is relatively the easy part compared to:
getting policies right (human problem)
mitigating when things go wrong (‘Swiss cheese’ layers)
bootstrapping the ‘user’ notion: authentication
Complexity brings expressiveness but also potential for human error:
$ ls /path/to/file% -rw-r--r-- 1 fred staff 14507 May 1 2019 /path/to/file# This is complex, the file has:# - rwx-style permissions# - directory traversal rules# - per-filesystem options# - (on modern Unix) near-arbitrary access control list
Layers of defence (‘Swiss cheese’ model)
Security is never absolute, it is relative to attack model and attackers’ budget. Methods are often complex and can be performed incorrectly. We have to think about what can go wrong, how much is at stake, and how we can limit the damage of any one incident.
Effective computer security therefore relies on layers of defence.
At different layers, we use distinct techniques, including but not limited to:
defensive programming
static analysis
use of languages with dynamic safety properties
strong user-system and system-user authentication
authentication of installed software
tight access control policy (incl. least-privilege operation)
network security, e.g. firewalls
audit trail (logging)
user training
staff (de-?)training
'least privilege' in the OS
When you run a program on your system, it typically gets all user privileges, can access any file as yourself and run any other program. This is convenient (and this is fine if the program is correct and trustworthy), i.e. by definition it won’t do anything I don’t want.
In terms of system processes, such as httpd (running a web server), it may be convenient to run as root but this is potentially opening a huge security hole because web servers are big and complex and not entirely trustworthy. Complex software can often be tricked or exploited into doing unintended things, this is called privilege escalation.
Web servers don’t need permissio to do everything, the least privilege they need is to have access to their own user that only contains the web server. So we then revise our access control policy to say that the ‘web server user’ can only do what is necessary to run a web server.
If the web server is ever compromised, it can only do whatever the web server user can do and not everything on the system.
Problems splitting up privilege
To divide up privileges finely, you need to create distinct ‘users’, set up privileges individuals for them, and if you get something wrong: programs will fail, likely mid-execution.
Sometimes programs may need to combine multiple authorities which adds additional complications, for example the program:
$ submit 1 ~/my-project-1
Needs permission to access the current user’s directory, and the submission handler’s permission to write into the submissions directory, mark information, etc.
The Unix ‘80% solution’ is ‘setuid’ and ‘setgid’ programs.
Special permission bits in Unix: set user ID and set group ID
Unix includes the following permission bits:
first s: when run (by anyone), uid is owning uid
second s: when run (by anyone), gid is owning gid
final x: anyone can run
A program’s owner can make it ‘setuid’ meaning ‘run with my privileges, not invoking user’s’. The seteuid() system call allows programs to ‘drop’ the owner’s privileges and switch to the invoking user’s. ‘setgid’ is the same concept for groups.
Case Study: Flexible means 'complex' => liable to be wrong.
Some programs are simple enough to audit, but not all:
-rwsr-xr-x 1 root root 27468 Jan 18 2022 /usr/bin/pexec
This exploit is non-trivial and requires working with several incidental ‘found’ components of the program. The key vulnerability is a buffer overflow.
This is an example of a confused deputy, where buggy code is acting under two or more authorities and an input tricks it into doing something under the wrong one. In this case, running logging code as the root user not the invoking user.
Preventing attacks like this:
Avoiding ‘setuid root’ in the first place.
Fine-grained explicit use of authority, only use ‘root’ness for the operation that needs it, such as running the requested child program, and not for things like printing text to log files.
Use bounds checking.
How can we ‘unconfuse the deputy’?
Eliminate the ambient authority: no notion of ‘current UID’, only special privilege granted to program is ‘can create a new process as a different user’ (can eliminate ‘user’ altogether), all operations must explicitly designate what authority they act under.
More ubiquitously use handles. To create a process, open a file, etc, pkexec did not have to choose a privileged handle. Its ‘root authority’ was ‘ambient’ in its user ID.
For example, pkexec could be granted a privileged handle to make its ‘new process’ system call with. But there is no such way to do this in a traditional Unix-like OS.