CVE-2018-1000001 glibc realpath缓冲区溢出漏洞分析
漏洞原理
首先要理解realpath函数的含义。realpath展开所有符号链接并解析path指定的以null结尾的路径名中的/./和/../以生成规范化的绝对路径名。生成的路径名以null结尾存储在由resolve_path指向的缓冲区中。生成的路径名将没有符号链接,/./或/../。比如/home/root目录下有一个1.txt,那么在/home/root/11/22目录下调用realpath("../../1.txt",resolved_path)得到的resolved_path就是/home/root/1.txt。
1 |
|
realpath的实现细节如下(glibc-2.24\stdlib\canonicalize.c)。首先判断path是相对路径还是绝对路径,如果第一个字符是”/”说明是绝对路径,否则是相对路径。是相对路径的话调用getcwd获得当前目录的绝对路径,后面结合path中的信息生成绝对路径名(就像上面举的例子)。在程序员的眼中,不管path是相对路径还是绝对路径,最后rpath第一个字符都是”/”,一定是绝对路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
接下来就是for循环以”/”为分割分别对path的每个部分进行处理,end - start == 1 && start[0] == '.'就是”./”的情况,表示当前目录,所以不需要处理;end - start == 2 && start[0] == '.' && start[1] == '.'就是”../”的情况,表示上级目录,rpath和dest分别是getcwd获得的绝对路径的开头和结尾,那么这个时候就把dest向前挪直到遇到”/”,前面例子中/home/root/11/22向前挪两次为/home/root/。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
如果不是这两种情况,那么会拷贝path的这个部分到dest,前面例子中把1.txt拷贝到/home/root/后面就得到/home/root/1.txt了。
1 2 |
|
还会检查此时rpath是不是一个符号链接,如果是的话要展开。
1 2 3 4 5 6 7 8 |
|
本来代码是没有任何问题的,但是Linux内核修订了getcwd,当目录不可达时,会在返回的字符串前面加上(unreachable)。glibc没有进行相应的改动,仍然假设getcwd将返回绝对地址,所以在realpath中仅仅依靠name[0] != '/'就断定参数是一个相对路径,而忽略了以”(“开头的不可达路径。这样在for循环中处理”../”这种情况时向前挪dest,因为前面没有”/”了,所以一直--dest造成后面拷贝时缓冲区溢出。
漏洞利用
POC
作者给了一个DOS的POC,测试的环境是Debian Stretch amd64&libc6 2.24-11+deb9u1&util-linux-2.29.2-1,我刚好有这么一个环境,测试的效果如下(等一会儿就关机了)。
我们看看这个DOS的POC原理是什么。触发漏洞使用的是util-linux中的umount,原因主要有两点:umount会调用realpath来解析路径,而且能被所有用户使用;umount具有SUID权限,具有这种权限的文件会在执行时使调用者暂时获得该文件拥有者的权限,可以用来提权。umount的realpath的操作发生在堆上,所以需要创造可重现的堆布局。在POC中是通过移除可能造成干扰的环境变量,仅保留locale做到的。locale在glibc或者其它需要本地化的程序和库中被用来解析文本(如时间、日期等),它会在umount参数解析之前进行初始化,所以会影响到堆的结构和位于realpath缓冲区前面的那些低地址的内容。在标准系统中,libc提供了/usr/lib/locale/C.UTF-8,它通过环境变量LC_ALL=C.UTF-8进行加载。在POC中向(unreachable)/tmp/from_archive/C/LC_MESSAGES/util-linux.mo文件写入了一些内容,将命令行中的文本base64解码再解压缩即可得到其内容。
po是portable object的缩写,mo是machine object的缩写。po文件是面向翻译人员提取于源代码的一种资源文件。当软件升级的时候,通过使用gettext软件包处理po文件,可以在一定程度上使翻译成果得以继承,减轻翻译人员的负担。mo文件是面向计算机由po文件通过gettext软件包编译而成的二进制文件。程序通过读取mo文件使自身的界面转换成用户使用的语言。在这个网站可以直接下载windows系统上编译好的gettext,用bin目录中的msgunfmt.exe在命令行中将mo文件转成po文件。
1 |
|
msgid表示的是代码中原本的文本,msgstr表示翻译的结果,也就是说遇到"%s: not mounted"会被替换成"AA%6$lnlnAAAAAAAAAAA",调试的时候会看到。下面就在gdb中开始调试来更好理解POC的含义,首先还是确认glibc和util-linux的版本。
系统自带的umount是没有符号的,所以重新下载并编译。
1 2 3 4 5 |
|
注意在gdb中通过set env和set arg设置环境变量和参数。
因为(unreachable)没有”/”,所以--dest一直向前越过(unreachable)直到C.utf8/LC_CTYPE这里覆写”/”之后的LC_CTYPE。
第一次__mempcpy的结果:
第二次__mempcpy的结果:
第三次__mempcpy的结果:
realpath返回(unreachable)/x:
之后umount_one->mk_exit_code->warnx(_("%s: not mounted"), tgt):
在调用warnx之前先调用了gettext,原本的”%s: not mounted”被替换成"AA%6$lnlnAAAAAAAAAAA":
使用fprintf的%n格式化字符串,即可对一些内存地址进行写操作。由于fprintf所使用的堆栈布局是固定的,所以可以忽略ASLR的影响。于是我们就可以利用该特性覆盖掉libmnt_context结构体中的restricted字段。
1 2 3 4 5 6 7 8 |
|
在安装文件系统时,挂载点目录的原始内容会被隐藏起来并且不可用直到被卸载。但是,挂载点目录的所有者和权限没有被隐藏,其中restricted标志用于限制对挂载文件系统的访问。如果将该值覆盖,umount会误以为挂载是从root开始的。于是可以通过卸载root文件系统实现DoS。
EXP
理解了这个POC的原理之后再看看提权的EXP,其实思路是一样的。在代码中除了一些辅助函数之外主要分成了两个部分,第一部分prepareNamespacedProcess做了一些准备工作,第二部分attemptEscalation触发漏洞提权。
正常编译运行能够完美提权:
一旦在gdb中运行就卡在这里了(原因见文章第一条评论):
prepareNamespacedProcess中主要做了下面这些事情。
1.通过设置setgroups为deny限制在新namespace里面调用setgroups来设置groups;通过设置uid_map和gid_map让子进程设置好挂载点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
2.创建了下面这些目录和文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
3.创建../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A到/proc/5600/cwd/(unreachable)/tmp/down的符号链接用来触发溢出。
1 2 3 4 5 6 |
|
4.创建/proc/5600/cwd/DATEMSK并向其中写入shebang(即“#!”)加上漏洞利用程序的路径,getdate会使用环境变量DATEMSK定义的路径名中的格式,会在下一个部分中用到它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
5.创建/proc/5600/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/C.UTF-8/LC_MESSAGES/util-linux.mo并向其中写入需要的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
转成po文件之后查看内容如下。
6.创建/proc/5600/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES/util-linux.mo。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
attemptEscalation中主要做了下面这些事情。
1.在fork的子进程中设置大量AANGUAGE = X.X环境变量喷射栈,umount调用realpath发生溢出时加载了前面设置的mo文件,格式化字符串把栈dump到stderr,修改restricted标志位并把AANGUAGE = X.X改成LANGUAGE = X.X。
2.父进程分为下面几个阶段:
第零阶段寻找溢出后的8个A定位数据的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
第一阶段读取栈上的数据绕过ASLR,找到libc基地址后将返回地址改成getdate和execl的地址,然后把这些信息写入/proc/5600/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES/util-linux.mo。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
|
转成po文件之后查看内容如下。
第二阶段因为LANGUAGE改变了所以会重新读取util-linux.mo,等待管道进入非阻塞模式再进入下一个阶段。
1 2 3 4 5 6 7 8 9 10 |
|
第三阶段读取umount输出(代码里面的注释写的是“result from mount”,应该是笔误把umount写成mount了),进行一些清理工作,等待进入ROP链。
1 2 3 4 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
前面prepareNamespacedProcess中在DATEMSK定义的路径名中写入shebang(即“#!”)加上漏洞利用程序的路径,操作系统会调用漏洞利用程序作为解释器。漏洞利用程序将自己的文件所有权和模式更改为root SUID二进制文件并退出。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
umount进程退出之后在主进程中做一些清理工作就可以直接调用shell了,成功提权!
参考资料
2.glibc Realpath缓冲区下溢漏洞(CVE–2018–1000001)分析