DEV Community

Cover image for Analysis of Glibc privilege escalation vulnerability "Looney Tunables" (CVE-2023-4911)
TutorialBoy
TutorialBoy

Posted on

Analysis of Glibc privilege escalation vulnerability "Looney Tunables" (CVE-2023-4911)

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)
Enter fullscreen mode Exit fullscreen mode

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;
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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++;
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

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
}

Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.

Image description

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.

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)