A good software engineer must be able to create secure software. This is a fundamental, yet often unknown, skill for a good developer or software engineer. The product must meet two different types of requirements:

  • Functional requirements: the software must do what it is designed for according to the specifications.
  • Non-functional requirements: the software must be usable, safe, and secure, all things not strictly related to the software’s functionality, but to its quality.

Creating inherently secure applications is a fundamental, yet often unknown, skill for a good developer or software engineer. Creating secure software is a really hard process, and the proof of that is the number of known software vulnerabilities. If a software does not meet the security specifications, it is vulnerable. A vulnerability is a way to leverage a vulnerability to violate the CIA (Confidentiality, Integrity, Availability) of the software. A vulnerability is not an exploit, but it is a way to exploit the software.

Life of a vulnerability

Every software has vulnerabilities, and they are introduced during the development process. The time between the introduction of a vulnerability and the complete patching of it, is called “window of exposure”. During this period of time, we can distinguish two possible surface of attack:

  1. Zero-day attacks: the time between the release in the wild of an exploit and its public disclosure, is the most dangerous period of time for a software; during this period, the software is vulnerable and the company is not aware or do not know the existence of the vulnerability.
  2. Follow-on attacks: the time between the public disclosure of an exploit and the complete patching of the vulnerability, is the period of time in which the software is vulnerable, but the company is aware of the existence of the vulnerability and is working to fix it. This period must be as short as possible.

From the birth of software development, bugs and vulnerability were things always present in any software. A study conducted by the NIST’s National Vulnerability Database shows that the number of known software vulnerabilities has always been high, and it is still high today. We can see three periods:

  • The early days of non-disclosure
  • The years of full disclosure
  • The years of anti-disclosure

In the years of non-disclousure, the software companies did not want to disclose the vulnerabilities of their software, and they did not want to admit that their software had bugs. Many geeks and hackers started to disclose the vulnerabilities of the software they found, trying to make the software companies aware of the bugs in their software.

In the years of full disclosure, the software companies started to admit that their software had bugs, and they started to fix them. The hackers and geeks continued to disclose the vulnerabilities of the software they found, but they started to do it in a more controlled way. They started to disclose the vulnerabilities only to the software companies, and disclose them publicaly only after the software companies had fixed them. Many companies also started to call hackers and geeks, inviting them to parties and events, in order to find out the vulnerabilities of their software. Everything without paying them.

Nowadays, in the era of anti-disclousure, the software companies do not want to disclose the vulnerabilities of their software, so the hackers and geeks started pretending to be paid for their job. They started to sell the vulnerabilities they found to the highest bidder, even selling them to the black market. On the other hands, many companies started to pay the hackers to find the vulnerabilities of their software, creating contests and bug bounties.

Vulnerability and exploit

Definitions

  • A vulnerability is a security flaw in a software that can be exploited by an attacker to violate the CIA (Confidentiality, Integrity, Availability) of the software.
  • An exploit is a way to leverage a vulnerability to violate the CIA of the software.

Take as example the ownership of a file in a UNIX-like system. Every file has an owner and permissions associated with it. Who creates a file is the owner of it. In Unix systems, with the command ls -la <file> we can see the owner of the file, the group, the permissions associated with it, the size and the date of the last modification of the file. Every user can see the owner of a file, but only the owner of the file can change the permissions associated with the file.

[bar@localhost]$ ls -la executable
-rwxr-xr-x 1 foo group 41836 2012-10-14 19:19 executable

When we run an executable, the process has a real UID (RUID), which is the real owner of the process. The RUID could differ from the owner of the file. It’s possible to check the owner of a process with the command ps -a -x -o user,pid,cmd.

[bar@localhost]$ ./executable   # Darwin Kernel 13.1.0
[bar@localhost]$ ps -a -x -o user,pid,cmd
USER PID    COMMAND
bar  18299  ./executable

The effective UID (EUID) is the UID used to check the permissions associated with the process. Normally, the RUID is equal to the EUID. The saved set-user-ID (SUID) can be used to change the EUID at runtime. With the command chmod u+s executable, we can set the SUID of the executable to the owner of the file. Now, the executable’s SUID is “foo”.

[root@localhost]# chmod u+s executable
[root@localhost]# ls -la executable
-rwsr-xr-x 1 foo group 41836 2012-10-14 19:19 executable

When we run the executable with a different user, the RUID is different from the EUID. The RUID is the real owner of the process, while the EUID is the owner of the file.

[bar@localhost]$ ./executable
[bar@localhost]$ ps -a -x -o user,pid,cmd
USER PID    COMMAND
foo   18299  ./executable

SUMMARY

User IDDescriptionUsageKey Differences
RUIDReal User IDIdentifies the actual user who owns the process.Represents the original user ID of the process owner.
Typically used for access control decisions.Stays constant throughout the process unless explicitly changed.
SUIDSaved Set-User-IDStores the previous EUID value when the EUID is changed temporarily.Used to allow a process to temporarily assume different privileges and then revert back to its original EUID.
Helps in managing permissions when using setuid programs.Allows for privilege escalation and safe reversion to the original state.
EUIDEffective User IDDetermines the permissions the process currently has.Used by the operating system to check process permissions when accessing resources.
Can be different from RUID if the process has elevated privileges (e.g., through setuid).Can be temporarily changed to SUID and back, enabling processes to perform tasks with elevated privileges.

Key Points

  1. Real User ID (RUID):
    • Reflects the actual user who started the process.
    • Is used mainly for accounting purposes.
    • Generally remains unchanged unless explicitly altered by a privileged process.
  2. Saved Set-User-ID (SUID):
    • Maintains the old EUID when it is changed.
    • Enables a process to revert back to its previous EUID.
    • Facilitates safe privilege management in setuid programs.
  3. Effective User ID (EUID):
    • Determines the permissions for the process during its execution.
    • Can be altered to grant temporary privileges different from the RUID.
    • Controls access to files and system resources based on the current EUID.

Consider now a piece of code that prints the RUID and the EUID of a process created by a user (not root)

// executable.c
 
#include <unistd.h>
#include <stdio.h>
 
int main(int argc, const char *argv[]) {
    printf("RUID %d EUID %d", getuid(), geteuid());
    return 0;
}

and compile it, change user to root, set the SUID get back and check the permissions associated with the file.

[foo@localhost]$ gcc -o executable -c executable.c
[foo@localhost]$ sudo su -              # become root
 
[root@localhost]# chown root            # change the owner
[root@localhost]# chmod +s executable   # set the SUID root bit
[root@localhost]# exit                  # get back to foo
 
[foo@localhost]$ ls -la executable      # check the flags
 -rwsr-xr-x 1 root group 41836 2012-10-14 19:19 executable

When running it, we can see that the RUID is 501 and the EUID is 0.

[foo@localhost]$ ./executable
RUID 501 Effective 0        # 501 is foo's UID - 0 is root's UID
 
[foo@localhost]$ sudo -u root ./executable
RUID 0 Effective 0          # 0 is root's UID

Now, let’s add a privileged instruction to the code, that reads the content of a file that can be read only by root, like /etc/secret.

// ...
fp = fopen("/etc/secret", "r");     // /etc/secret can be read only by root
 
while (!feof(fp)) {
    fgets(line, 1024, fp);
    puts(line);
}
 
fclose(fp);

Programs are “SUID root” to allow them to execute privileged instructions. The content of the file is printed, even if the file can be read only by root. This is a serious issue, because an unprivileged user can read the content of a file that can be read only by privileged users. The EUID should be changed back once the privileged instructions are done.

#include <unistd.h>
#include <stdio.h>
 
int main(int argc, const char *argv[]) {
    // execute as EUID -------------------------
    FILE * fp;
    char line[1024];
 
    printf("Real %d Effective %d", getuid(), geteuid());
 
    fp = fopen("/etc/secret", "r");
    fgets(line, 1024, fp);
    fclose(fp);
 
    setuid(501);    // execute as unprivileged user
    printf("Real %d Effective %d", getuid(), geteuid());
    puts(line);
 
    return 0;
}

Once we read the file, we release the privileges. The subsequent instructions are executed with user’s privileges.

General idea

Taking in cosideration the pseudocode of a vulnerable program

// EUID: RUID -> SUID
read(config)
r = parse(config)
IF r = OK do_things() ELSE error("...")

The read(config) function prints the content of the file in the error message. This allows an unprivileged user to print the content of, e.g., the /etc/shadow file, which can be normally read only by privileged users. To fix this issue, the EUID should be changed back once the privileged instructions are done.

// EUID: SUID -> RUID
read(config)  //low privs
// EUID: RUID -> SUID
r = parse(config)
IF r = OK do_things() ELSE error("...")

By acquiring the privileges as late as possible, and releasing them as soon as possible, the developer decreases the attack surface (but there may still be other vulnerabilities in the do_things() part of code). This is still a vulnerable program, because any bug in the parse(config) function would happen in a privileged portion of the code, therefore potentially allowing the attacker to perform actions. So we have to change the EUID not only after the read(config) function, but also when we want to perform do_things().

// EUID: SUID -> RUID
read(config)  //low privs
r = parse(config)
 
IF r = OK
    // EUID: RUID -> SUID
    do_things()
    // EUID: SUID -> RUID
ELSE error("...")

Examples

VulneabilityExploit
Developer acquired the privileges before read(config)Invocation of the program with /etc/shadow as the first argument
Developer acquired the privileges before parse(config)Invocation of the program on a specifically crafted file to exploit a vulnerability inside the configuration file

Principles of secure design

There are some key issues in secure design that a developer must take into consideration when developing a software:

  • Reduce privileged parts to a minimum: the less privileged parts a software has, the less vulnerabilities it has.
  • KISS (Keep It Simple, Stupid): the simpler a software is, the less vulnerabilities it has because it is easier to check and fix.
  • Discard privileges definitively (i.e. SUIDRUID) as soon as possible: the sooner a software releases the privileges, the less vulnerabilities it has.
  • Open design: the software must not rely on obscurity for security but must be open and transparent to everyone, to be checked and fixed by everyone.
  • Concurrency and race conditions are tricky to manage
  • Fail-safe and default deny: the software must be designed to fail-safe and to deny by default, to avoid vulnerabilities. This means that the software must be designed to deny everything by default, and to allow only what is strictly necessary.
  • Avoid the use of shared resources (e.g. mktemp) and unknown, untrusted libraries to avoid possible vulnerabilities.
  • Filter the input and the output to avoid possible exploits.
  • Do not write any own cryptographic primitives, password, and secret management code: use trusted code that has been audited already.
  • Use trustworthy RNGs (Random Number Generators) such as /dev/[u]random to avoid possible vulnerabilities.
  • Use secure coding practices to avoid possible vulnerabilities.