This document is available in: English Castellano Deutsch Francais Nederlands Russian |
by Frédéric Raynal, Christophe Blaess, Christophe Grenier About the author: Christophe Blaess is an independent aeronautics engineer. He is a Linux fan and does much of his work on this system. He is in charge of the coordination for the man pages translation published by the Linux Documentation Project. Christophe Grenier is a 5th year student at the ESIEA, where he works as a sysadmin too. He has a passion for computer security. Frédéric Raynal uses Linux, certified without (software or other) patents. Apart from that, you must see Dancer in the Dark : besides Björk who is great, this movie can't leave you unmoved (I can't say more without unveiling the end, both tragic and splendid). Content: |
Abstract:
This article is the first one in a series about the main types of security holes in applications. We'll show the ways to avoid them by changing your development habits a little.
It doesn't take more than two weeks before a major application which is part of most Linux distributions presents a security hole allowing, for instance, a local user to become root. Despite the great quality of most of this software, ensuring the security of a program is a hard job : it must not allow a bad guy to benefit illegally from system resources. The availability of application source code is a good thing, much appreciated by programmers, but the smallest defects in software become visible to everyone. Furthermore, the detection of such defects comes at random and the people finding them do not always have good intentions.
From the sysadmin side, daily work consists of reading the
lists concerning security problems and immediately updating the
involved packages. For a programmer it can be a good lesson
to try out such security problems since
avoiding security holes from the
beginning is the preferred method of fixing them. We'll try to define some "classic"
dangerous behaviors and provide solutions to reduce the
risks. We won't talk about network
security problems since they often stem from configuration mistakes
(dangerous cgi-bin scripts, ...) or from system bugs allowing DOS
(Denial Of Service) type attacks to prevent a machine from
listening to its own clients. These problems concern the sysadmin
or the kernel developers. But the application programmer must also protect
her code as soon as she takes into account external data. Some versions of
pine
, acroread
, netscape
,
access
,...
have allowed elevated access or information leaks
under some conditions.
As a matter of fact
secure programming is everyone's concern.
This set of articles shows methods which can be used to damage a Unix system. We could only have mentioned them or said a few words about them, but we prefer complete explanations to make people understand the risks. Thus, when debugging a program or developing your own, you'll be able to avoid or correct these mistakes. For each discussed hole, we will take the same approach. We'll start detailing the way it works. Next, we will show how to avoid it. For every example we will use security holes still present in wide spread software.
This first article talks about the basics needed for understanding
security
holes, that is the notion of privileges and the
Set-UID or Set-GID bit. Next, we analyse the
holes based on the system()
function, since they are
easier to understand.
We will often use small C programs to illustrate what we are
talking about. However, the approaches mentioned in these articles
are applicable to other programming languages : perl, java, shell
scripts... Some security holes depend on a language, but this is
not true for all of them as we will see it with
system()
.
On a Unix system, users are not equals, neither are
applications. The access to the file system nodes - and accordingly
the machine peripherals - relies on a strict identity control. Some
users are allowed to do sensitive operations to maintain the system
in good condition. A number called UID (User Identifier)
allows the identification. To make things easier, a user name
corresponds to this number, the association is done in
the /etc/passwd
file.
The UID of 0, with default name of root, can access
everything in the system. He can create, modify, remove every
system node, but he can as well manage the physical configuration
of the machine, mounting partitions, activating network interfaces
and changing their configuration (IP address), or using system
calls such as mlock()
to act on physical memory, or
sched_setscheduler()
to change the order mechanism. In
a future article we will study the Posix.1e features which allows
limiting the privileges of an application executed as
root, but for now, let's assume the super-user can do
everything on a machine.
The attacks we will mention are internal ones, that is an authorized user on a machine tries to gain privileges he doesn't have. On the other hand, the network attacks are external ones, coming from people trying to connect to a machine they are not allowed on.
To use privileges reserved for another user without being able to log in under her identity, one must at least have the opportunity to talk to an application running under the victim's UID. When an application - a process - runs under Linux, it has a well defined identity. First, a program has an attribute called RUID (Real UID) corresponding to the user ID who launched it. This data is managed by the kernel and usually can not change. A second attribute completes this information : the EUID field (Effective UID) corresponding to the identity the kernel takes into account when managing the access rights (opening files, reserved system-calls).
To get the privileges of another user means everything will be done under the UID of that user, and not under the proper UID. Of course, a cracker tries to get the root ID, but many other user accounts are of interest, either because they give access to system information (news, mail, lp...) or because they allow reading private data (mail, personal files, etc) or they can be used to hide illegal activities such as attacks on other sites.
To run an application with the privileges of an Effective UID
different from its Real UID (the user who launched it) the
executable file must have a specific bit turned on called Set-UID. This bit
is found in the file permission attribute (like user's execute,
read, write bits, group members or others) and has the octal value
of 4000. The Set-UID bit is represented with an s
when
displaying the rights with the ls
command :
The command ">> ls -l /bin/su -rwsr-xr-x 1 root root 14124 Aug 18 1999 /bin/su >>
find / -type f -perm +4000
" displays a
list of the system applications having their Set-UID bit set to 1.
When the kernel runs an application with the Set-UID bit on,
it uses the program owner's identity as EUID for the process. On the other
hand, the RUID doesn't change and corresponds to the user who
launched the program. For instance, every user can have access to
the /bin/su
command, but it runs
under its owner's identity (root) with
every privilege on the system. Needless to say one must be very
careful when writing a program with this attribute.
Each process also has an Effective group ID, EGID, and a real
identifier RGID. The Set-GID bit (2000 in octal) in the access
rights of an executable file, asks the kernel to use the owner's
group of the file as EGID and not the GID of the user who
launched the program. A curious combination sometimes appears with
the Set-GID set to 1 but without the group execute bit. As a matter
of fact, it's a convention having nothing to do with privileges
related to applications, but indicating the file can be blocked
with the function fcntl(fd, F_SETLK, lock)
.
Usually an application doesn't use the Set-GID bit, but it does
happen sometimes. Some games, for instance, use it to save the best
scores into a system directory.
There are various types of attacks against a system. Today we'll study the mechanisms to execute an external command from within and application. This is usually a shell running under the identity of the owner of the application. A second type of attack relies on buffer overflow giving the attacker the ability to run personal code instructions. Last, the third main type of attack is based on race condition - a lapse of time between two instructions in which a system component is changed (usually a file) while the application believes it remains the same.
The two first types of attacks often try to execute a shell with
the application owner's privileges, while the third one is targeted
instead at getting write access to protected system files.
Read access is sometimes considered a system security weakness
(personal files, emails, password file /etc/shadow
,
and pseudo kernel configuration files in /proc
).
The targets of security attacks are mostly the programs having a
Set-UID (or Set-GID) bit on. However, this also effects every
application running under a different ID than the one of its user.
The system daemons represent a big part of these programs. A daemon
is an application usually started at boot time, running in the
background without any control terminal, and doing privileged work
for any user. For instance, the lpd
daemon allows
every user to send documents to the printer, sendmail
receives and redirects electronic mail, or apmd
asks
the Bios for the battery status of a laptop. Some daemons
are in charge of communication with external users through the
network (Ftp, Http, Telnet... services). A server called
inetd
manages the connections of many of these services.
We can then conclude that a program can be attacked as soon as it talks - even briefly - to a user different from the one who started it. While developing this type of application you must be careful to keep in mind the risks presented by the functions we will study here.
When an application runs with an EUID different from its RUID, it's to provide the user with privileges he needs but doesn't have (file access, reserved system calls...). However these privileges are only needed for a very short time, for instance when opening a file, otherwise the application is able to run with its user's privileges. It's possible to temporarily change an application EUID with the system-call :
int seteuid (uid_t uid);A process can always change its EUID value giving it the one of its RUID. In that case, the old UID is kept in a saved field called SUID (Saved UID) different from SID (Session ID) used for control terminal management. It's always possible to get the SUID back to use it as EUID. Of course, a program having a null EUID (root) can change at will both its EUID and RUID (it's the way
/bin/su
works).
To reduce the risks of attacks, it's suggested to change the EUID and use the RUID of the users instead. When a portion of code needs privileges corresponding to those of the file's owner, it's possible to put the Saved UID into the EUID. Here is an example :
uid_t e_uid_initial; uid_t r_uid; int main (int argc, char * argv []) { /* Saves the different UIDs */ e_uid_initial = geteuid (); r_uid = getuid (); /* limits access rights to the ones of the * user launching the program */ seteuid (r_uid); ... privileged_function (); ... } void privileged_function (void) { /* Gets initial privileges back */ seteuid (e_uid_initial); ... /* Portion needing privileges */ ... /* Back to the rights of the runner */ seteuid (r_uid); }
This method is much more secure than the unfortunately all to common one consisting of using the initial EUID and then temporarily reducing the privileges just before doing a "risky" operation. However this privilege reduction is useless against buffer-overflow attacks. As we'll see in a next article, these attacks intend to ask the application to execute personal instructions and can contain the system-calls needed to make the privilege level higher. Nevertheless, this approach protects from external commands and from most race conditions.
An application often needs to call an external system service. A
well known example concerns the mail
command to manage
an electronic mail (running report, alarm, statistics, etc) without
requiring a complex dialog with the mail system. The easiest
solution is to use the library function :
int system (const char * command)
This function is rather dangerous : it calls the shell to
execute the command given as an argument. The shell behavior depends
on the choice of the user. A typical example comes from the
PATH
environment variable. Let's look at an
application calling the mail
function. For instance,
the following program sends its source code to the user who
launched it :
Let's say this program is Set-UID root :/* system1.c */ #include <stdio.h> #include <stdlib.h> int main (void) { if (system ("mail $USER < system1.c") != 0) perror ("system"); return (0); }
To execute this program, the system runs a shell (with>> cc system1.c -o system1 >> su Password: [root] chown root.root system1 [root] chmod +s system1 [root] exit >> ls -l system1 -rwsrwsr-x 1 root root 11831 Oct 16 17:25 system1 >>
/bin/sh
) and with the -c
option, it tells
it the instruction to invoke. Then the shell goes through the
directory hierarchy according to the PATH
environment
variable to find an executable called mail
. To compromise the program, the
user only has to change this variable's content before running the
application. For example :
looks for the>> export PATH=. >> ./system1
mail
command only within the current
directory. One need merely create an executable file (for
instance, a script running a new shell) and name it
mail
and the program will then be executed with the main
application owner's EUID! Here, our script runs
/bin/sh
. However, since it's executed with a
redirected standard input (like the initial mail
command), we must get it back in the terminal. We then create the
script :
Here is the result :#! /bin/sh # "mail" script running a shell # getting its standard input back. /bin/sh < /dev/tty
>> export PATH="." >> ./system1 bash# /usr/bin/whoami root bash#
Of course, the first solution consists in giving the full path
of the program, for instance /bin/mail
. Then a new
problem appears : the application relies on the system
installation. If /bin/mail
is usually available on
every system, where is GhostScript, for instance? (is it in
/usr/bin
, /usr/share/bin
,
/usr/local/bin
?). On the other hand, another type of
attack becomes possible with some old shells : the use of the
environment variable IFS
. The shell uses it to parse
the words in the command line. This variable holds the separators.
The defaults are the space, the tab and the return. If the user
adds the slash /
, the command "/bin/mail
"
is understood by the shell as "bin mail
". An
executable file called bin
in the current directory can
be executed just by setting PATH
, as we have seen before, and allows
to run this program with the application EUID.
Under Linux, the IFS
environment variable is not a
problem anymore since bash and pdksh both complete it with the default characters
on startup. But keeping application portability in
mind you must be aware that some systems might be less secure
regarding this variable.
Some other environment variables may cause unexpected problems.
For instance, the mail
application allows the user to
run a command while composing a message using an escape sequence
"~!
". If the user writes the string
"~!command
" at the beginning of the line, the
command is run. The program /usr/bin/suidperl
used to
make perl scripts work with a Set-UID bit calls
/bin/mail
to send a message to root
when it detects a problem.
Since /bin/mail
is
Set-UID root,
the call to
/bin/mail
is done with root's privileges
and contains the name of the faulty file.
A user can then create a file whose name contains a carriage
return followed by a ~!command
sequence and another
carriage return. If a perl script calling suidperl
fails on a low-level problem related to this file, a message is
sent under the root identity, containing the escape
sequence from the mail
application, and the command
in the file name is executed with root's privileges.
This problem shouldn't exist since the mail
program
is not supposed to accept escape sequences when run automatically
(not from a terminal). Unfortunately, an undocumented feature of
this application (probably left from debugging), allows the escape
sequences as soon as the environment variable
interactive
is set. The result? A security hole easily
exploitable (and widely exploited) in an application supposed to
improve system security. The blame is shared. First,
/bin/mail
holds an undocumented option especially
dangerous since it allows code execution only checking the
data sent, what should be a priori suspicious for
a mail utility. Second, even if the /usr/bin/suidperl
developers were not aware of the interactive
variable,
they shouldn't have left the execution environment as it was when
calling an external command, especially when writing this program
Set-UID root.
As a matter of fact, Linux ignores the Set-UID and Set-GID bit
when executing scripts (read
/usr/src/linux/fs/binfmt_script.c
and
/usr/src/linux/fs/exec.c
). But some tricks allow you to bypass
this rule, like Perl does with its own scripts using
/usr/bin/suidperl
to take these bit into account.
It isn't always easy to find a replacement for the
system()
function. The first variant is to use
system-calls such as execl()
or execle()
.
However, it'll be quite different since the external program is no
longer called as a subroutine, instead the invoked command replaces
the current process. You must fork the process and parse
the command line arguments. Thus the program :
becomes :if (system ("/bin/lpr -Plisting stats.txt") != 0) { perror ("Printing"); return (-1); }
Obviously, the code gets heavier! In some situations, it becomes quite complex, for instance, when you must redirect the application standard input such as in :pid_t pid; int status; if ((pid = fork()) < 0) { perror("fork"); return (-1); } if (pid == 0) { /* child process */ execl ("/bin/lpr", "lpr", "-Plisting", "stats.txt", NULL); perror ("execl"); exit (-1); } /* father process */ waitpid (pid, & status, 0); if ((! WIFEXITED (status)) || (WEXITSTATUS (status) != 0)) { perror ("Printing"); return (-1); }
That is, the redirection defined bysystem ("mail root < stat.txt");
<
is done from
the shell. You can do the same, using a complicated sequence
such as fork()
, open()
,
dup2()
, execl()
, etc. In that case, an
acceptable solution would be using the system()
function, but configuring the whole environment.
Under Linux, the environment variables are stored in the form of
a pointer to a table of characters : char ** environ
. This
table ends with NULL. The strings are of the form
"NAME=value
".
We start removing the environment using the Gnu extension :
or forcing the pointerint clearenv (void);
to take the NULL value. Next the important environment variables are initialized, using controlled values, with the functions :extern char ** environ;
before calling theint setenv (const char * name, const char * value, int remove) int putenv(const char *string)
system()
function. For
example :
If needed, you can save the content of some useful variables before removing the environment (clearenv (); setenv ("PATH", "/bin:/usr/bin:/usr/local/bin", 1); setenv ("IFS", " \t\n", 1); system ("mail root < /tmp/msg.txt");
HOME
,
LANG
, TERM
, TZ
,etc.). The
content, the form, the size of these variables must be strictly
checked. It is important that you remove the whole
environment before redefining the needed variables. The
suidperl
security hole wouldn't have appeared if the
environment were properly removed.
Analogues, protecting a machine on a network first implies denying every connection. Next, a sysadmin activates the required or useful services . In the same way, when programming a Set-UID application the environment must be cleared and then filled with required variables.
Verifying a parameter format is done by comparing the expected value to the allowed formats. If the comparison succeeds the parameter is validated. Otherwise, it is rejected. If you run the test using a list of invalid format values, the risk of leaving a malformed value increases and that can be a disaster for the system.
We must understand what is dangerous with system()
is
also
dangerous for some derived functions such as
popen()
, or with system-calls such as
execlp()
or execvp()
taking into account
the PATH
variable.
To improve a programs usability, it's easy to leave the user the
ability to configure most of the software behavior using macros,
for instance. To manage variables or generic patterns as the shell
does, there is a powerful function called wordexp()
.
You must be very careful with it, since sending a string like
$(command)
allows executing the mentioned
external command. Giving it the string
"$(/bin/sh)
" creates a Set-UID shell. To avoid
this, wordexp()
has an attribute called
WRDE_NOCMD
that deactivates the interpretation of the $( )
sequence .
When invoking external commands you must be careful to not
call a utility providing an escape mechanism to a shell
(like the vi :!command
sequence).
It's difficult to list them all, some applications are
obvious (text editors, file managers...) others are harder to
detect (as we have seen with /bin/mail
) or have
dangerous debugging modes.
This article illustrates various aspects :
The next article will talk about memory, its organization, and function calls before reaching the buffer overflows. We also will see how to build a shellcode.
|
Webpages maintained by the LinuxFocus Editor team
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL LinuxFocus.org Click here to report a fault or send a comment to LinuxFocus |
Translation information:
|
2001-02-23, generated by lfparser version 2.9