Analysis of Glibc privilege escalation vulnerability “Looney Tunables” (CVE-2023–4911)

TutorialBoy
9 min readFeb 21, 2024

Recently, the Threat Research Unit of Qualys Company disclosed a Glibc vulnerability. The Glibc library has a buffer overflow vulnerability when processing environment variables, which can lead to local privilege escalation. This vulnerability affects various Linux distributions, including Fedora, Ubuntu, Debian, etc.

Vulnerability Analysis

According to the disclosed information, the vulnerability exists in the processing of environment variables by the ld. so dynamic linker. Use the dd command to view the loader of the system program. For example ldd /bin/ls, you can see that the actual loader is /lib64/ld-linux-x86–64.so.2.

$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffe2935d000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f088ec45000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f088ea1d000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f088e986000)
/lib64/ld-linux-x86-64.so.2 (0x00007f088eca8000)

The vulnerability exists in the loader’s parse_tunablesfunction, which is tunables_initcalled by the tunables_initfunction that handles GLIBC_TUNABLESenvironment variables, allowing developers to dynamically adjust the behaviour of the runtime library.

void __tunables_init (char **envp)
{
char *envname = NULL;
char *envval = NULL;
size_t len = 0;
char **prev_envp = envp;
maybe_enable_malloc_check ();
 while ((envp = get_next_env (envp, &envname, &len, &envval,
&prev_envp)) != NULL) #获取环境变量
{
\#if TUNABLES_FRONTEND == TUNABLES_FRONTEND_valstring
if (tunable_is_name (GLIBC_TUNABLES, envname))
{
char *new_env = tunables_strdup (envname);
if (new_env != NULL)
parse_tunables (new_env + len + 1, envval); #漏洞程序
/* Put in the updated envval. */
*prev_envp = new_env;
continue;
}

The function in the code get_next_envextracts environment variable information one by one from the saved environment variables. tunable_is_name (GLIBC_TUNABLES, envname)The function is responsible for finding the environment variable of “GLIBC_TUNABLES”. After finding the variable, it saves it to tunables_strdupthe space requested by the function and saves the return buffer address to new_envthe pointer. Since the malloc program has not been initialized, tunables_strdupcalling to __minimal_mallocallocate an address minimal_malloc()actually calls mmap() to obtain memory.

static char *
tunables_strdup (const char *in)
{
size_t i = 0;
while (in[i++] != '\0');
char *out = __minimal_malloc (i + 1);
/* For most of the tunables code, we ignore user errors. However,
this is a system error - and running out of memory at program
startup should be reported, so we do. */
if (out == NULL)
_dl_fatal_printf ("failed to allocate memory to process tunables\n");
while (i-- > 0)
out[i] = in[i];
return out;
}
\#endif

Then call parse_tunablesthe method to process new_envthe data, and the code will be analyzed in detail below. Take the “tunable1=tunable2=AAA” parameter as an example. Entering the first while(true), first, find the parameter after the first “=”, and then point p to the value of the first parameter “tunable2=AAA”.

while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
len++;
...
p += len + 1;
len=0;

Then, the second loop retrieval starts. At this time, the second equal sign input in the wrong format is not retrieved, and the end of the parameter is directly located. At this time, the length of len is the length of “tunable2=AAA”.

while (p[len] != ':' && p[len] != '\0')
len++;

Then in the for loop, all the data after tunable1 is copied to tunestr. At this time, the buffer is already full.

for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
{
tunable_t *cur = &tunable_list[i];
if (tunable_is_name (cur->name, name))
{
/* If we are in a secure context (AT_SECURE) then ignore the
tunable unless it is explicitly marked as secure. Tunable
values take precedence over their envvar aliases. We write
the tunables that are not SXID_ERASE back to TUNESTR, thus
dropping all SXID_ERASE tunables and any invalid or
unrecognized tunables. */
if (__libc_enable_secure)
{
if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
{
if (off > 0)
tunestr[off++] = ':';
const char *n = cur->name;
while (*n != '\0')
tunestr[off++] = *n++;
tunestr[off++] = '=';
for (size_t j = 0; j < len; j++)
tunestr[off++] = value[j];
}
if (cur->security_level != TUNABLE_SECLEVEL_NONE)
break;
}
value[len] = '\0';
tunable_initialize (cur, value);
break;
}
}

If the last judgment is made p[len]!=’\0', p points to the next parameter. But as seen from the above, at this time p[len]==’\0', it enters the second loop, and p points to the value of the second parameter “tunable2=AAA”. Repeating the above copy process will cause a buffer overflow, and the overflow byte will be “AAA”.

if (p[len] != '\0')
p += len + 1;

Privilege Elevation

The following describes how to hijack the program’s environment variables, modify the glibc dynamic link library path, and make it load the modified libc.so.6file to achieve the purpose of elevating privileges.

First, let’s look at the process of applying for space in this part of the program. According to debugging, tunables_initthe first-time GLIBC_TUNABLES the environment variable is obtained during initialization, it will be called minimal_mallocto apply for memory. The location of the requested memory 0x7f8b545cd2e0is located /usr/local/lib/ld-linux-x86–64.so.2in the buffer, and ld-linux-x86–64.so.2the distance from the end of the program space is 0xd20.

pwndbg> b __GI___tunables_init
pwndbg> b *0x7f8b545aad5d #通过计算得到
Breakpoint 4 at 0x7f8b545aad5d: file dl-tunables.c, line 52.
pwndbg> c
Continuing.
Thread 3.1 "test" hit Breakpoint 4, 0x00007f8b545aad5d in tunables_strdup (in=<optimized out>) at dl-tunables.c:52
52 char *out = __minimal_malloc (i + 1);
pwndbg> ni
pwndbg> i r
rax 0x7f8b545cd2e0
pwndbg> vmmap
0x7f8b54595000 0x7f8b54597000 r--p 2000 0 /usr/local/lib/ld-linux-x86-64.so.2
0x7f8b54597000 0x7f8b545be000 r-xp 27000 2000 /usr/local/lib/ld-linux-x86-64.so.2
0x7f8b545be000 0x7f8b545c9000 r--p b000 29000 /usr/local/lib/ld-linux-x86-64.so.2
0x7f8b545ca000 0x7f8b545ce000 rw-p 4000 34000 /usr/local/lib/ld-linux-x86-64.so.2
0x7ffca3e3e000 0x7ffca4440000 rw-p 602000 0 [stack]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
pwndbg> hex(0x7f8b545ce000-0x7f8b545cd2e0)
0x000d20

The first time you apply for space will be allocated to 0xd20this part of the space, but if you apply for 0xd00a space of size after. The program calls the minimal_mallocthe function again to apply for space, and mmap()the program will be called to apply for available space from the kernel. Here, taking the application 0x200size space as an example, you can see that the kernel allocates 0x2000space of size. After debugging, it can be seen that the minimal_mallocthe space requested for subsequent use will also be allocated from this space, which makes it possible to exploit this vulnerability.

pwndbg> c
Continuing.
Thread 3.1 "test" hit Breakpoint 4, 0x00007f8b545aad5d in tunables_strdup (in=<optimized out>) at dl-tunables.c:52
52 char *out = __minimal_malloc (i + 1);
pwndbg> ni
0x00007f8b545aad62 52 char *out = __minimal_malloc (i + 1);
pwndbg> i r
rax 0x7f8b5458d000
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x403000 0x405000 rw-p 2000 4000 /home/kpy/test
0x7f8b5458d000 0x7f8b5458f000 rw-p 2000 0 [anon_7f8b5458d]

After tunables_initthe initialization is completed, a buffer will be applied in the function to store the dl-object.c the structure. Since the calloc function of glibc has not been initialized at this time, the function is still called to apply for space at this time.__dl_new_objectstruct link_mapminimal_malloc

new = (struct link_map *) calloc (sizeof (*new) + audit_space
\+ sizeof (struct link_map *)
\+ sizeof (*newname) + libname_len, 1);

According to the debugging information, the space requested at this time is located GLIBC_TUNABLES behind the environment variable, which means that the overflow can just cover struct link_mapthe contents of the structure.

pwndbg> c
Continuing.
Thread 3.1 "test" hit Breakpoint 4, 0x00007f8b545aad5d in tunables_strdup (in=<optimized out>) at dl-tunables.c:52
52 char *out = __minimal_malloc (i + 1);
pwndbg> ni
0x00007f8b545aad62 52 char *out = __minimal_malloc (i + 1);
pwndbg> i r
rax 0x7f8b5458d210 140236392288784
pwndbg> vmmap
0x403000 0x405000 rw-p 2000 4000 /home/kpy/test
0x7f8b5458d000 0x7f8b5458f000 rw-p 2000 0 [anon_7f8b5458d]

Next, consider which member variables of the structure need to be overridden. According to link_mapthe structure information, a very interesting member variable is found link_map->l_info[DT_RPATH], which is a (16B) Elf64_Dynpointer to a small structure.

pwndbg> p *((struct link_map *) $rax)
$1 = {
l_addr = 4774451407232463713,
l_name = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>,
l_ld = 0x4242424242424242,
l_next = 0x4242424242424242,
l_prev = 0x4242424242424242,
l_real = 0x4242424242424242,
l_ns = 4774451407313060418,
l_libname = 0x4242424242424242,
l_info = {0x4242424242424242 <repeats 49 times>, 0x696c673a42424242, 0x6f6c6c616d2e6362, 0x74736166786d2e63, 0x3d, 0x0 <repeats 24 times>},
l_phdr = 0x7ffcfffff010,
l_entry = 0,
l_phnum = 0,
l_ldnum = 0,
l_searchlist = {
r_list = 0x0,
r_nlist = 0
}
l_local_scope = {0x0, 0x2e6362696c673a00},
l_file_id = {
dev = 7867334929274397037,
ino = 67570361263736
},
...
l_relro_addr = 0,
l_relro_size = 0,
l_serial = 0
}

Controlling this pointer variable can control the dynamic link library path of the user program. The specific code is in _dl_init_paths(elf/dl-load.c)the function, and this part of the code will be executed when the dynamic linker loads the shared library. The code first checks to see DT_RPATHif the member variable exists, and if so, reads the RPATH information from the section and parses it into a set of directory paths, which is stored in l->l_rpath_dirs.dirs. If RPATH is empty, it is set l->l_rpath_dirs.dirs = (void*)-1, indicating that the path lookup failed.

if (l->l_info[DT_RPATH])
{
/* Allocate room for the search path and fill in information
from RPATH. */
decompose_rpath (&l->l_rpath_dirs,
(const void *) (D_PTR (l, l_info[DT_STRTAB])
\+ l->l_info[DT_RPATH]->d_un.d_val),
l, "RPATH");
/* During rtld init the memory is allocated by the stub
malloc, prevent any attempt to free it by the normal
malloc. */
l->l_rpath_dirs.malloced = 0;
}
else
l->l_rpath_dirs.dirs = (void *) -1;
}

When the above code calls decompose_rpath, the code l->l_rpath_dirsallocates and initializes memory for, where l->l_info[DT_STRTAB]and l->l_info[DT_RPATH]->d_un.d_valpoint to the table and offset respectively DT_STRTAB.

DT_STRTABThe table address is at the actual program su 0xFF0. By adding the offset to this address, the dynamic link library path called by the program can be obtained.

Generally, in the suid program, DT_STRTABthere will be similar characters as shown in the figure below near the table. Take the quotation mark character “ as an example. That is, if you l->l_info[DT_RPATH]->d_un.d_valset to -0x14, you can calculate the path of the directory as the quotation mark character “ If the settings are modified libc.so.6, the attacked program can call the wrong dynamic link library and obtain root privileges.

In actual development, how to l_info[DT_RPATH]set to 0x14 the address pointing to? As mentioned above, the initial environment variable is stored on the stack, so l_info[DT_RPATH]the address is overwritten with the stack address here. However, usually, programs with SUID permissions have PIE protection turned on, and there are no stable and available addresses in the stack. But since the vulnerability can be triggered repeatedly, Stack Spray is used. On Linux, the stack is randomized in 16GB regions and environment variable strings can occupy up to 6MB. If we fill in an environment variable with a size of 6M, 16GB / 6MB = 2730it is very likely to enumerate 0x14 the address pointing to after a maximum of attempts. After more than 2,000 attempts, the privilege escalation was successful.

Patch Analysis

The following is Ubuntu’s repair code for this vulnerability. You can see that a judgment statement is added at the end of the code if (p[len] == ‘\0’). If p[len]==\0, the break is executed, the loop is jumped out, and copying will not continue, preventing buffer overflow.

Patch link: https://ubuntu.com/security/notices/USN-6409-1.

+-static void
++__attribute__ ((noinline)) static void
\+ parse_tunables (char *tunestr, char *valstring)
\+ {
\+ if (tunestr == NULL || *tunestr == '\0')
+@@ -187,11 +187,7 @@ parse_tunables (char *tunestr, char *val
\+ /* If we reach the end of the string before getting a valid name-value\
\+ pair, bail out. */
\+ if (p[len] == '\0')
+- {
+- if (__libc_enable_secure)
+- tunestr[off] = '\0';
+- return;
+- }
++ break;
\+
\+ /* We did not find a valid name-value pair before encountering the
\+ colon. */
+@@ -251,9 +247,16 @@ parse_tunables (char *tunestr, char *val
\+ }
\+ }
\+
+- if (p[len] != '\0')
+- p += len + 1;
++ /* We reached the end while processing the tunable string. */
++ if (p[len] == '\0')
++ break;
++
++ p += len + 1;
\+ }
++
++ /* Terminate tunestr before we leave. */
++ if (__libc_enable_secure)
++ tunestr[off] = '\0';
\+ }
\+ #endif

Repair Suggestions

In the Ubuntu system, you can run the following commands to upgrade and improve system security.

# apt-get update
# apt-get upgrade libc6

References

Originally published at https://tutorialboy24.blogspot.com

--

--

TutorialBoy

Our mission is to get you into information security. We'll introduce you to penetration testing and Red Teaming. We cover network testing, Active Directory.