安全 Linux 内核提权漏洞分析
漏洞复现
在 ubuntu-20.04-LTS 的虚拟机中进行测试, 内核版本号 5.10.0-1008-oem, 在 POC 执行后成功获取到 root shell。
从 POC 看漏洞利用流程
限于篇幅,这里截取 POC 的部分代码。
创建 pipe;
使用任意数据填充管道(填满, 而且是填满 Pipe 的最大空间);
清空管道内数据;
使用 splice()读取目标文件(只读)的 1 字节数据发送至 pipe;
write()将任意数据继续写入 pipe, 此数据将会覆盖目标文件内容;
只要挑选合适的目标文件(必须要有可读权限), 利用漏洞 Patch 掉关键字段数据, 即可完成从普通用户到 root 用户的权限提升, POC 使用的是/etc/passwd 文件的利用方式。
仔细阅读 POC 可以发现, 该漏洞在覆盖数据时存在一些限制, 我们将在深入分析漏洞原理之后讨论它们。
【一>所有资源获取<一】1、网络安全学习路线 2、电子书籍(白帽子)3、安全大厂内部视频 4、100 份 src 文档 5、常见安全面试题 6、ctf 大赛经典题目解析 7、全套工具包 8、应急响应笔记
复现原始 Bug
在作者的 paper 中可以了解到, 发现该漏洞的起因不是专门的漏洞挖掘工作, 而是关于日志服务器多次出现的文件错误, 用户下载的包含日志的 gzip 文件多次出现 CRC 校验位错误, 排查后发现 CRC 校验位总是被一段 ZIP 头覆盖。
根据作者介绍, 可以生成 ZIP 文件的只有主服务器的一个负责 HTTP 连接的服务(为了兼容 windows 用户, 需要把 gzip 封包即时封包为 ZIP 文件), 而该服务没有写入 gzip 文件的权限。
即主服务器同时存在一个 writer 进程与一个 splicer 进程, 两个进程以不同的用户身份运行, splicer 进程并没有写入 writer 进程目标文件的权限, 但存在 splicer 进程的数据写入文件的 bug 存在。
简化两个服务进程
根据描述, 简易还原出 bug 触发时最原本的样子, poc_p1 与 poc_p2 两个程序:
编译运行 poc_p1 程序, tmpFile 内容为全A
。
运行 poc_p2 程序, tmpFile 文件时间戳未改变, 但文件内容中出现了B
。
仔细观察每次出现脏数据的间隔, 发现恰好为 4096 字节, 4kB, 也是系统中一个页面的大小。
如果将进程可使用的全部 Pipe 大小进行一次写入/读出操作, tmpFile 的内容发生了变化。
同时可以注意到, tmpFile 文件后续并不是全部被B
覆盖, 而是在 4096 字节处保留了原本的内容。
此时不执行任何操作, 重启系统后, tmpFile 将变回全A
的状态, 这说明, poc_p2 程序对 tmpFile 文件的修改仅存在于系统的页面缓存(page cache)中。
以上便是漏洞出现的初始状态, 要分析其详细的原因, 就需要了解造成此状态的一些系统机制。
Pipe、splice()与零拷贝
限于篇幅, 这里简要介绍一下该漏洞相关的系统机制。
CPU 管理的最小内存单位是一个页面(Page), 一个页面通常为 4kB 大小, linux 内存管理的最底层的一切都是关于页面的, 文件 IO 也是如此, 如果程序从文件中读取数据, 内核将先把它从磁盘读取到专属于内核的
页面缓存(Page Cache)
中, 后续再把它从内核区域复制到用户程序的内存空间中;如果每一次都把文件数据从内核空间拷贝到用户空间, 将会拖慢系统的运行速度, 也会额外消耗很多内存空间, 所以出现了 splice()系统调用, 它的任务是从文件中获取数据并写入管道中, 期间一个特殊的实现方式便是: 目标文件的页面缓存数据不会直接复制到 Pipe 的环形缓冲区内, 而是以索引的方式(即 内存页框地址、偏移量、长度 所表示的一块内存区域)复制到了 pipe_buffer 的结构体中, 如此就避免了从内核空间向用户空间的数据拷贝过程, 所以被称为"零拷贝";
管道(Pipe)是一种经典的进程间通信方式, 它包含一个输入端和一个输出端, 程序将数据从一段输入, 从另一端读出; 在内核中, 为了实现这种数据通信, 需要以页面(Page)为单位维护一个
环形缓冲区(被称为pipe_buffer)
, 它通常最多包含 16 个页面, 且可以被循环利用;当一个程序使用管道写入数据时, pipe_write()调用会处理数据写入工作, 默认情况下, 多次写入操作是要写入环形缓冲区的一个新的页面的, 但是如果单次写入操作没有写满一个页面大小, 就会造成内存空间的浪费, 所以 pipe_buffer 中的每一个页面都包含一个
can_merge
属性, 该属性可以在下一次 pipe_write()操作执行时, 指示内核继续向同一个页面继续写入数据, 而不是获取一个新的页面进行写入。
描述漏洞原理
这也就解释了之前原始 bug 造成的一些问题:
由于 pipe buffer 页面未清空, 所以第一次 poc_p2 测试时, tmpFile 从 4096 字节才开始被覆盖数据;
splice()调用至少需要将文件页面缓存的第一个字节写入 pipe, 才可以完成将 page_cache 索引到 pipe_buffer, 所以第二次 poc_p2 测试时, tmpFile 并没有全部被覆盖为"B", 而是每隔 4096 字节重新出现原始的"A";
每一次 poc_p2 写入的数据都是在 tmpFile 的页面缓存中, 所以如果没有其他可写权限的程序进行 write 操作, 该页面并不会被内核标记为“dirty”, 也就不会进行页面缓存写会磁盘的操作, 此时其他进程读文件会命中页面缓存, 从而读取到篡改后到文件数据, 但重启后文件会变回原来的状态;
也正是因为 poc_p2 写入的是 tmpFile 文件的页面缓存, 所以无限的循环会因文件到尾而写入失败, 跳出循环。
阅读相关源码
要了解漏洞形成的细节, 以及漏洞为什么不是从 splice()引入之初就存在, 还是要从内核源码了解 Pipe buffer 的can_merge
属性如何迭代发展至今。
1.Linux 2.6, 引入了splice()
系统调用;
2.Linux 4.9, 添加了 iov_iter 对 Pipe 的支持, 其中copy_page_to_iter_pipe()
与push_pipe()
函数实现中缺少对 pipe buffer 中flag
的初始化操作, 但在当时并无大碍, 因为此时的can_merge
标识还在ops
即pipe_buf_operations
结构体中。 如图, 此时的buf->ops = &page_cache_pipe_buf_ops
操作会使can_merge
属性为 0, 此时并不会触发漏洞, 但为之后的代码迭代留下了隐患;
Linux 5.1, 由于在众多类型的 pipe_buffer 中, 只有
anon_pipe_buf_ops
这一种情况的can_merge
属性是为 1 的(can_merge
字段在结构体中占一个 int 大小的空间), 所以, 将pipe_buf_operations
结构体中的can_merge
属性删除, 并且把 merge 操作时的判断改为指针判断, 合情合理。正是如此,copy_page_to_iter_pipe()
中对buf->ops
的初始化操作已经不包含can_merge
属性初始化的功能了, 只是push_write()
中 merge 操作的判断依然正常, 所以依然不会触发漏洞;
page_cache_pipe_buf_ops
类型也在此时被修改。
然后是新的判断can_merge
的操作, 直接判断是不是anon_pipe_buf_ops
类型即可。
Linux 5.8中, 把各种类型的
pipe_buf_operations
结构体进行合并, 正式把can_merge
标记改为PIPE_BUF_FLAG_CAN_MERGE
合并进入 flag 属性中, 知道此时, 4.9 补丁中没有flag字段初始化
的隐患才真正生效合并后的
anon_pipe_buf_ops
不能再与can_merge
强关联。再次修改了 merge 操作的判断方式。添加新的
PIPE_BUF_FLAG_CAN_MERGE
定义, 合并进入 pipe buffer 的 flag 字段。内核漏洞补丁, 在
copy_page_to_iter_pipe()
和push_pipe()
调用中专门添加了对 buffer 中flag
的初始化。
拓展与总结
关于该漏洞的一些限制:
显而易见的, 被覆写的目标文件必须拥有可读权限, 否则 splice()无法进行;
由于是在 pipe_buffer 中覆写页面缓存的数据, 又需要 splice()读取至少 1 字节的数据进入管道, 所以覆盖时, 每个页面的第一个字节是不可修改的, 同样的原因, 单次写入的数据量也不能大于 4kB;
由于需要写入的页面都是内核通过文件 IO 读取的 page cache, 所以任意写入文件只能是单纯的“覆写”, 不能调整文件的大小;
该漏洞之所以被命名为 DirtyPipe, 对比 CVE-2016-5195(DirtyCOW), 是因为两个漏洞触发的点都在于 linux 内核对文件读写操作的优化(写时拷贝/零拷贝); 而 DirtyPipe 的利用方式要比 DirtyCOW 的更加简单, 是因为 DirtyCOW 的漏洞触发需要进行条件竞争, 而 DirtyPipe 可以通过操作顺序直接触发;
值得注意的是, 该内核漏洞不仅影响了 linux 各个发行版, Android 或其他使用 linux 内核的 IoT 系统同样会受到影响; 另外, 该漏洞任意覆盖数据不只是影响用户或系统文件, 块设备、只读挂在的镜像等数据一样会受到影响, 基于此, 实现容器穿透也是有可能的。
总结
想想自己刚开始做漏洞复现的时候, 第一个复现的内核提权就是大名鼎鼎的 DirtyCOW, 所以看到 DirtyPipe 就不由得深入研究一下。这个漏洞的发现经历也非常有趣, 作者居然是从软件 bug 分析一路走到了内核漏洞披露, 相当佩服作者这种求索精神, 可以想象一个人在代码堆中翻阅各种实现细节时的辛酸, 也感谢作者如此详细的披露与分享。
评论