复现一下 dirty pipe漏洞
漏洞简介
漏洞发现者 Max Kellermann 并不是专门从事漏洞挖掘工作的,而是在服务器中多次出现了文件错误的问题,用户下载的包含日志的gzip文件多次出现CRC校验位错误, 排查后发现CRC校验位总是被一段ZIP头覆盖。
根据作者介绍, 可以生成ZIP文件的只有主服务器的一个负责HTTP连接的服务,但是该服务并没有写 gzip 文件的权限。即主服务器同时存在一个 writer 进程与一个 splicer 进程, 两个进程以不同的用户身份运行, splicer进程并没有写入writer 进程目标文件的权限, 但存在 splicer 进程的数据写入文件的 bug 存在。
有兴趣可以看看漏洞发现者写的原文。作者竟然是从一个小小的软件bug一步一步深挖到了内核漏洞,实在是佩服作者的这种探索精神。
环境准备
这里我按照自己复现准备的一个真实状态来写。
首先是准备一个被漏洞影响的版本内核,这里我使用了 5.8.0-63-generic
版本。本来之前打算直接 Ubuntu20.04LTS
一了百了,但是发现它内核有自动更新,下过来就是最新的,漏洞已经被修复了。所以这里直接用这个讲讲换这个内核的版本。
首先 apt 寻找这个版本:
1 | apt-cache search linux | grep 5.8.0-63 |
然后我们在列表中寻一下
再用 apt 安装这个内核
1 | sudo apt install linux-image-5.8.0-63-generic |
安装完成之后, reboot
重启,开机界面按 shift+TAB
进入 ubuntu 引导界面,然后选择高级选项 advance
,选择我们刚刚安装的那个内核进入启动。
成功替换指定的版本。
前置芝士
pipe
匿名管道,用于作为一个读写数据的通道,参数给一个长度为 2 的 int
数组,并通过这个数组返回,创建成功则返回对应的读写描述符,一端只能用于读,一端只能写。
1 |
|
Splice
splice用于在两个文件描述符之间移动数据, 也是零拷贝。
函数原型是 :
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
参数解释:
fd_in
:输入文件描述符fd_out
:输出文件描述符len
:移动字节长度flags
:控制数据如何移动。
返回值:
>0
:表示成功移动的字节数==0
:表示没有字节可以移动<0
:表示出现某些错误
1 |
|
bug复现
做完上面两个前置芝士之后,应该能对 pipe 和 splice 稍微有点理解了,那么我们来复现一下 paper 上面说的 bug。把两个服务简化一下写出这两个程序:
创建一个 tmpfile
,user
属主,权限 755。
1 | //server1,user运行的服务,生成可执行文件为 p1 |
下面这个是要准备去写之前准备的那个文件了。
1 | //server2,hacker运行的服务,生成可执行文件为p2 |
我们这么操作:
先使用 user 用户创建一个 tmpfile
,运行 p1
文件。
在这个文件中我们写了 5000 个 A,并且权限归 user 所有,只有它有写的权限。
切换回 hacker 用户,运行 p2 文件,我们发现 tmpfile 中间居然出现了 BBBBB
。
这就证明了这个漏洞的存在,这个漏洞的危害在于:只要文件可读,那我就可以写,这是非常危险的。
但是,我们不做任何操作,重启机器之后发现文件又变回了全 A 的状态。这说明,p2程序对tmpfile文件的修改仅存在于系统的页面缓存(page cache)中。
但是这个页面缓存在内核中,我们打开的文件会短暂停留在 page cache 当中,一段时间内我们打开的文件就相当于这个 page cache 的内容,所以我们更改了 page cache 其实也是可以达到修改文件的目的的。
exploit以及分析
这里我根据了我自己对这个漏洞的理解写了一个便于我们获取 root 权限的 exploit。
1 |
|
一步一步分析一下它的 exp。
首先我用了一个 SetCanMerge
函数去填满 pipe
,这一步的作用是走完一遍 pipe
的缓冲区。pipe
的缓冲区是以页为单位的,总共是 16 页的环形缓冲区。这一步就是去设置所有 page 的 Can Merge
属性。这个 Can Merge
标识了这个 pipe
能否被续写。
我们先来介绍一下续写的概念,续写就是说在我总共 16 页的缓冲区中,我写了一个字符,那么它会被存储在下标为 0 的页的首地址中,我第二次写的时候会写在哪里呢?两个选择,一个是紧跟着后面去写,第二个是在后一页去写,显然为了内存考虑我们大部分会选择紧跟着后面去写,因为总共 16 页,不可能我扔了 16 个字符进管道管道直接就满了。所以我们设置了一个 flags
,它们当中有一位就是标志了是否能在上面续写。
显然我一个一个往里面去丢字符,它肯定会把 16 个页都设置为可以续写,所以这一步就是把 pipe
缓冲区都设置为可续写。第二步我们打开了一个 /etc/passwd
文件,这个文件所有用户可读,只有 root
用户可写,我们以只读的方式打开它获得一个文件描述符,然后通过 splice
向管道中拷贝一个字节的内容。
这里的拷贝一个字节因为是零拷贝,所以它不是真正的拷贝,而是直接把缓存页给挂到了 pipe
缓冲区当中。然而此时 Can Merge
属性又存在,所以我们再往管道里写数据的时候,会因为 Can Merge
而直接写 Page Cache
绕过了权限检查。
于是我们向 /etc/passwd
的第二个字节起写上五个字节 oots:
,这样的话 /etc/passwd
的第一行变成了 roots::...
,原本第一行的内容为 root:x:...
,中间的 x
表示此用户有密码,而我们把 x
取消掉了,那么我们生成了一个 uid 为 0 且没有密码的用户 roots
,那么我们通过 su roots
就能直接切换到 root
权限。
内核源码分析
以后再写吧……