首页
社区
课程
招聘
[原创]CVE-2018-1000001 glibc realpath缓冲区溢出漏洞分析
发表于: 2018-6-20 16:26 12055

[原创]CVE-2018-1000001 glibc realpath缓冲区溢出漏洞分析

2018-6-20 16:26
12055

今年年初的一个漏洞,当时没有看懂就回家过年了,这几天决定把这个漏洞弄懂。表述不当的地方,还望批评指正。

realpath的实现细节如下(glibc-2.24\stdlib\canonicalize.c)。首先判断path是相对路径还是绝对路径,如果第一个字符是”/”说明是绝对路径,否则是相对路径。是相对路径的话调用getcwd获得当前目录的绝对路径,后面结合path中的信息生成绝对路径名(就像上面举的例子)。在程序员的眼中,不管path是相对路径还是绝对路径,最后rpath第一个字符都是”/”,一定是绝对路径。

如果不是这两种情况,那么会拷贝path的这个部分到dest,前面例子中把1.txt拷贝到/home/root/后面就得到/home/root/1.txt了。

还会检查此时rpath是不是一个符号链接,如果是的话要展开。

本来代码是没有任何问题的,但是Linux内核修订了getcwd,当目录不可达时,会在返回的字符串前面加上(unreachable)。glibc没有进行相应的改动,仍然假设getcwd将返回绝对地址,所以在realpath中仅仅依靠name[0] != '/'就断定参数是一个相对路径,而忽略了以”(“开头的不可达路径。这样在for循环中处理”../”这种情况时向前挪dest,因为前面没有”/”了,所以一直--dest造成后面拷贝时缓冲区溢出。

作者给了一个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文件。

msgid表示的是代码中原本的文本,msgstr表示翻译的结果,也就是说遇到"%s: not mounted"会被替换成"AA%6$lnlnAAAAAAAAAAA",调试的时候会看到。下面就在gdb中开始调试来更好理解POC的含义,首先还是确认glibc和util-linux的版本。


系统自带的umount是没有符号的,所以重新下载并编译。

注意在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字段。

在安装文件系统时,挂载点目录的原始内容会被隐藏起来并且不可用直到被卸载。但是,挂载点目录的所有者和权限没有被隐藏,其中restricted标志用于限制对挂载文件系统的访问。如果将该值覆盖,umount会误以为挂载是从root开始的。于是可以通过卸载root文件系统实现DoS。




理解了这个POC的原理之后再看看提权的EXP,其实思路是一样的。在代码中除了一些辅助函数之外主要分成了两个部分,第一部分prepareNamespacedProcess做了一些准备工作,第二部分attemptEscalation触发漏洞提权。

正常编译运行能够完美提权:


一旦在gdb中运行就卡在这里了(原因见文章第一条评论):


prepareNamespacedProcess中主要做了下面这些事情。

1.通过设置setgroups为deny限制在新namespace里面调用setgroups来设置groups;通过设置uid_map和gid_map让子进程设置好挂载点。

转成po文件之后查看内容如下。


6.创建/proc/5600/cwd/(unreachable)/tmp/_nl_load_locale_from_archive/X.X/LC_MESSAGES/util-linux.mo。

1.在fork的子进程中设置大量AANGUAGE = X.X环境变量喷射栈,umount调用realpath发生溢出时加载了前面设置的mo文件,格式化字符串把栈dump到stderr,修改restricted标志位并把AANGUAGE = X.X改成LANGUAGE = X.X。


2.父进程分为下面几个阶段:

第零阶段寻找溢出后的8个A定位数据的位置。

转成po文件之后查看内容如下。


第三阶段读取umount输出(代码里面的注释写的是“result from mount”,应该是笔误把umount写成mount了),进行一些清理工作,等待进入ROP链。




最后于 2018-8-2 22:18 被houjingyi编辑 ,原因:
收藏
免费 2
支持
分享
最新回复 (3)
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
文章有处谬误:
poll timeout 和权限无关,问题在于没下断点直接就r了,此时实际上a.out已经完成所有提权,执行自身本该启动shell了,但是由于在gdb调试环境下getuid返回的注定不是0,然后又去建用户空间搞提权,但缓存区相应位置已经下溢改过了,重复搞是搞不定的。
我用的gdb是7.11.1 SuSE发行版,gdb倒是真有缺陷,包括8.1版本也有,就是catch exec、 set follow-exec-mode new进入execve调起的新程序后,马上做info prog操作gdb会segfault,我前些日子把缺陷提给SuSE了,他们已经改了trunk分支的gdb,7.11.1的补丁应该还没有搞,感觉希望不大。gef默认context.enable为True,进了execve新程序后(比如本文的umount)马上就会去执行info prog,立马coredump,所以用gef跟execve调用程序绕不开这个毛病。
我自己做的suse gdb 7.11.1 patch如下,估计7.11.1版本的代码都差不多,至少可以参照修改。
--- gdb.orig/infcmd.c   2016-06-01 08:36:15.000000000 +0800
+++ gdb/infcmd.c        2018-07-05 01:11:52.476253718 +0800
@@ -2062,7 +2062,9 @@ program_info (char *args, int from_tty)
       get_last_target_status (&ptid, &ws);
     }
 
-  if (ptid_equal (ptid, null_ptid) || is_exited (ptid))
+  if (ptid_equal (ptid, minus_one_ptid))
+    error (_("No selected thread."));
+  else if (ptid_equal (ptid, null_ptid) || is_exited (ptid))
     error (_("Invalid selected thread."));
   else if (is_running (ptid))
     error (_("Selected thread is running."));

不会改gdb代码,也有临时变通办法,就是执行到execve时设置gef config context.enable False,跟踪进入execve调起程序后先做一步si,再恢复context.enable就好了
最后于 2018-7-5 02:27 被zrlw编辑 ,原因: 错字
2018-7-5 02:20
0
雪    币: 5633
活跃值: (7094)
能力值: ( LV15,RANK:531 )
在线值:
发帖
回帖
粉丝
3
zrlw 文章有处谬误:poll timeout 和权限无关,问题在于没下断点直接就r了,此时实际上a.out已经完成所有提权,执行自身本该启动shell了,但是由于在gdb ...
感谢指正!
2018-7-6 15:46
0
雪    币: 1068
活跃值: (30)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
4
大佬好,请问你的gdb参数怎么设置才能到最后rop链的地方?我的参数是set follow-fork-mode child;set detach-on-fork on;b *getdate;b *execl 。运行以后就直接到最后invoking shell了。
2019-12-19 18:47
0
游客
登录 | 注册 方可回帖
返回
//