views:

238

answers:

2

Can anyone point to some code that deals with the security of files access via a path specified (in part) by an environment variable, specifically for Unix and its variants, but Windows solutions are also of interest?

This is a big long question - I'm not sure how well it fits the SO paradigm.

Consider this scenario:

Background:

  • Software package PQR can be installed in a location chosen by users.
  • The environment variable $PQRHOME is used to identify the install directory.
  • By default, all programs and files under $PQRHOME belong to a special group, pqrgrp.
  • Similarly, all programs and files under $PQRHOME either belong to a special user, pqrusr, or to user root (and those are SUID root programs).
  • A few programs are SUID pqrusr; a few more programs are SGID pqrgrp.
  • Most directories are owned by pqrusr and belong to pqrgrp; some can belong to other groups, and the members of those groups acquire extra privileges with the software.
  • Many of the privileged executables must be run by people who are not members of pqrgrp; the programs have to validate that the user is permitted to run it by arcane rules that do not directly concern this question.
  • After startup, some of the privileged programs have to retain their elevated privileges because they are long-running daemons that may act on behalf of many users over their lifetime.
  • The programs are not authorized to change directory to $PQRHOME for a variety of arcane reasons.

Current checking:

  • The programs currently check that $PQRHOME and key directories under it are 'safe' (owned by pqrusr, belong to pqrgrp, do not have public write access).
  • Thereafter, programs access files under $PQRHOME via the full value of environment variable.
  • In particular, the G11N and L10N is achieved by accessing files in 'safe' directories, and reading format strings for printf() etc out of the files in those directories, using the full pathname derived from $PQRHOME plus a known sub-structure (for example, $PQRHOME/g11n/en_us/messages.l10n).

Assume that the 'as installed' value of $PQRHOME is /opt/pqr.

Known attack:

  • Attacker sets PQRHOME=/home/attacker/pqr.
  • This is actually a symlink to /opt/pqr, so when one of the PQR programs, call it pqr-victim, checks the directory, it has correct permissions.
  • Immediately after the security checking is completed successfully, the attacker changes the symlink so that it points to /home/attacker/bogus-pqr, which is clearly under the attacker's control.
  • Dire things happen when the pqr-victim now accesses a file under the supposedly safe directory.

Given that PQR currently behaves as described, and is a large package (multiple millions of lines of code, developed over more than a decade to a variety of coding standards, which were frequently ignored, anyway), what techniques would you use to remediate the problem?

Known options include:

  1. Change all formatting calls to use function that checks actual arguments against the format strings, with an extra argument indicating the actual types passed to the function. (This is tricky, and potentially error prone because of the sheer number of format operations to be changed - but if the checking function is itself sound, works well.)
  2. Establish the direct path to PQRHOME and validate it for security (details below), refusing to start if it is not secure, and thereafter using the direct path and not the value of $PQRHOME (when they differ). (This requires all file operations that use $PQRHOME to use not the value from getenv() but the mapped path. For example, this would require the software to establish that /home/attacker/pqr is a symlink to /opt/pqr, that the path to /opt/pqr is secure, and thereafter, whenever a file is referenced as $PQRHOME/some/thing, the name used would be /opt/pqr/some/thing and not /home/attacker/pqr/some/thing. This is a large code base - not trivial to fix.)
  3. Ensure that all directories on $PQRHOME, even tracking through symlinks, are secure (details below, again), and the software refuses to start if anything is insecure.
  4. Hard-code the path to the software install location. (This won't work PQR; it makes testing hell, if nothing else. For users, it means they can have but one version installed, and upgrades etc require parallel running. This does not work for PQR.)

Proposed criteria for secure paths:

  • For each directory, the owner must be trusted. (Rationale: the owner can change permissions at any time, so the owner must be trusted not to make changes at random that break the security of the software.)
  • For each directory, the group must either not have write privileges (so members of the group cannot modify the directory contents) or the group must be trusted. (Rationale: if the group members can modify the directory, then they can break the security of the software, so either they must be unable to change it, or they must be trusted not to changed it.)
  • For each directory, 'others' must have no write privilege on the directory.
  • By default, the users root, bin, sys, and pqrusr can be trusted (where bin and sys exist).
  • By default, the group with GID=0 (variously known as root, wheel or system), bin, sys, and pqrgrp can be trusted. Additionally, the group that owns the root directory (which is called admin on MacOS X) can be trusted.

The POSIX function realpath() provides a mapping service that will map /home/attacker/pqr to /opt/pqr; it does not do the security checking, but that need only be done on the resolved path.

So, with all that as background, is there any known software which goes through vaguely related gyrations to ensure its security? Is this being overly paranoid? (If so, why - and are you really sure?)

Edited:

Thanks for the various comments.

@S.Lott: The attack (outlined in the question) means that at least one setuid root program can be made to use a format string of the (unprivileged) user's choosing, and can at least crash the program and therefore most probably can acquire a root shell. It requires local shell access, fortunately; it is not a remote attack. It requires a non-negligible amount of knowledge to get there, but I consider it unwise to assume that the expertise is not 'out there'.

So, what I'm describing is a 'format string vulnerability' and the known attack path involves faking the program out so that although it thinks it is accessing secure message files, it actually goes and uses the message files (which contain format strings) that are under the control of the user, not under the control of the software.

+1  A: 

Option 2 works, if you write a new value for $PQRHOME after resolving its real path and check its security. That way very little of your code needs changing thereafter.

As far as keeping the setuid privileges, it would help if you can do some sort of privilege separation, so that any operations involving input from the real user runs under the real uid. The privileged process and the real-uid process then talk using a socketpair or something like it.

Chris Jester-Young
Thanks for the advice. I agree that the less that runs privileged the better; I'm working on getting less stuff to run with elevated privileges, but it is hard.
Jonathan Leffler
+1  A: 

Well, it sounds paranoid, but if it is or not depends on which system(s) your application is running on and which damage can an attacker do.

So, if your userbase is possibly hostile and if the damage is possibly very high, I'd go for the option 4, but modified as follows to remove its drawbacks.

Let me quote two relevant things:

1)

The programs currently check that $PQRHOME and key directories under it are 'safe' (owned by pqrusr, belong to pqrgrp, do not have public write access).

2)

Thereafter, programs access files under $PQRHOME via the full value of environment variable.

You don't need to actually hard-code the full path, you can hard-code just the relative path from the "program" you mentioned in 1) to the path mentioned in 2) where the files are.

Issue to control:

a) you must be sure that there isn't anything "attacker-accessible" (e.g. in term of symlinks) in between the executable's path and the files' path

b) you must be sure that the executable check its own path in a reliable way, but this should not be a problem in all the Unix'es I know (but I don't know all 'em and I don't know windows at all).


EDITED after the 3rd comment:

If your OS support /proc, the syslink /proc/${pid}/exe is the best way to solve b)


EDITED after sleeping on it:

Is the installation a "safe" process? If so, you might create (at installation time) a wrapper script. This script should be executable but not writable (and possibly neither readable). It would set the $PQRHOME env var to the "safe" value and then call your actual program (it might eventually do other useful things too). Since in UNIX the env vars of a running process cannot be changed by anything else but the running process, you are safe (of course the env vars can be changed by the parent before the process starts). I do not know if this approach works in Windows, though.

Davide
Thanks for the input. The built-up pathnames are all of the form $PQRHOME/sub/subsub/file; the problem is that $PQRHOME is used as supplied by the user - leading to the attack - rather than in a sanitized and scrutinized form.
Jonathan Leffler
Your issue (a) is partly manageable (though it is hard to track down all the places where files are opened - we needed to get FD_CLOEXEC flags set on opened files, and tracking those down is still a work in progress.
Jonathan Leffler
Your issue (b) is actually remarkably hard. I'm curious to know what you would suggest for that? Remember, the invoking program can arbitrarily set argv[0] in a way that is wholly unrelated to the name of the executable that is specified for execv() to execute.
Jonathan Leffler
@Jonathan: Like my answer said, you can use setenv("PQRHOME", sanitised_path, 1) if your OS supports it. Do that at the start of every program if necessary. As for getting program path, I know in Linux you can resolve /proc/self/exe, but I don't know how this translates to other Unixes.
Chris Jester-Young
@Davide: This is on Unix (Windows has no setuid). However, your wrapper must be a binary (even if a very light one written in 5 lines of C), not a script, especially if it intends to be setuid. (setuid scripts == evil!)
Chris Jester-Young
@Chris: you are right about the wrapper, but the question explicitly mentioned Windows as something "of interest".
Davide
@Chris and Davide: Yes, Windows is of interest - but only to the extent that a service which runs for a long time could be confused into accessing the wrong materials because of tricks analogous to dinking with symlinks.
Jonathan Leffler
@Chris and Davide (continued): Fortunately, Windows has more complex rules and no real symlinks (as I understand it) and that actually makes it considerably easier.
Jonathan Leffler
@Chris: Using putenv() or setenv() to reset the env var is a good idea. There are levels of complexity in the way the code handles env vars, using functions to insulate people from basic getenv (and horrible antique code) so I'll have to check whether the env vars would be re-read correctly.
Jonathan Leffler