cve-2022-2588学习
感觉好牛,看描述是一个exp可以完成多个版本的通杀,因为在exp中并没有使用某一个特定的内核地址,所以就是说这个exp没有地址依赖,没有地址依赖那就没有内核版本限制了。最主要还是想学习一下这个漏洞利用才想着学习这个cve的,但是一看exp人傻了,七八百行,再加上网上的资料很少。。。彳亍。。。一周半起步了。
前置知识浅学
内核路由表
不只是路由器需要路由表,主机自己也得有路由表,路由表的作用其实就类似于导航的作用,它告诉主机数据包应该转发到哪里。如果主机不含路由表,那么它所有的数据包都传送不出去。所以不关事路由器,主机也会有自己的路由表。
可以通过route -n
来查看主机的路由表,下面是我虚拟机的路由表。
1 | 内核 IP 路由表 |
一条路由信息主要包括以下几点。
- 目的地址
- 下一跳地址
- 子网掩码
- 网卡接口
内核子系统
linux内核主要由以下七个子系统组成,其中最主要的四个子系统是内存管理子系统、进程管理子系统、网络子系统、虚拟文件系统。
各个模块的大概依赖如下
稍微对网络子系统和虚拟文件系统做个了解
网络子系统
Linux网络子系统提供了对各种网络标准的存取和各种硬件的支持。下图是其整体结构。其可以分为协议层和网络驱动程序,其中网络协议主要负责实现每一种可能的网络传输协议,而网络驱动程序负责与硬件通信。
虚拟文件系统
Linux虚拟文件系统(VFS)隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口,它是对各种文件系统的一个抽象,其实使用超级块super block存放文件系统相关信息,使用索引节点inode存放文件的物理信息,使用目录项dentry存放文件的逻辑信息,其整体架构如下。
子系统之间通信
内核的子系统之间是互相依赖的,当某个子系统状态发生改变的时候,就必须使用一定的机制告知使用其服务的其他子系统,以便其他子系统采取相应的措施,但到底如何利用netlink进行子系统之间的通信还是没有查到,只知道各个子系统会对不同的消息会有不同的处理措施。
netlink
内核和用户态进程进行双向通信的一种机制,非常强大,不仅可以支持内核子系统和用户态进程的通信,还可以进行内核中不同子系统之间的通信,但是我在谷歌或者百度中并没有找到相关机制说明和代码演示,只有内核和用户态进程之间通信的代码实践。
创建socket套接字的时候的结构体,和用户态socket的sockaddr_in
结构体功能类似。
1 | struct sockaddr_nl |
其中nl_pid
字段比较重要,当有多个用户态进程连接内核时,内核通过这个字段区分不同进程,一般使用getpid()
赋值。
netlink消息体如下
消息头结构体如下
1 | struct nlmsghdr |
用户态和内核态双向通信代码示例
这份代码是基于内核2.x的,不知道如今内核版本是否能用,并未做过实验,仅做记录学习使用。
用户态
1 |
|
内核态
1 |
|
linux流量控制
概念理解
在概念上有了一个大致了解,但不多,在Linux中要实现对数据包接收和发送的这些控制行为,需要使用队列结构来临时保存数据包。在Linux实现中,把这种包括数据结构和算法实现的控制机制抽象为结构队列规程:Queuing discipline
,简称为qdisc
。qdisc
对外暴露两个回调接口enqueue
和dequeue
分别用于数据包入队和数据包出队,而具体的排队算法实现则在qdisc
内部隐藏。不同的qdisc
实现在Linux内核中实现为不同的内核模块。
qdisc
的实现可以非常简单,比如只包含单个队列,数据包先进先出,如: pfifo
, 代码位于net/sched/sch_fifo.c
。也可以实现相当复杂的调度逻辑。比如,可以根据数据包的属性进行过滤分类,而针对不同的分类:class
采用不同的算法来进行处理。class
可以理解为qdisc
的载体,它还可以包含子类与qdisc
。用来实现过滤逻辑的组件叫做filter
,也叫做分类器classfier
, 它需要挂载在qdisc
或者class
上。
基于qdisc
, class
和filter
种三元素可以构建出非常复杂的树形qdisc
结构,极大扩展流量控制的能力。
对于树形结构的qdisc
, 当数据包流程最顶层qdisc
时,会层层向下递归进行调用。如,父对象(qdisc/class
)的enqueue
回调接口被调用时,其上所挂载的所有filter
依次被调用,直到一个filter
匹配成功。然后将数据包入队到filter
所指向的class
,具体实现则是调用class
所配置的Qdisc
的enqueue
函数。没有成功匹配filter
的数据包分类到默认的class
中。
相关数据结构
详情查看https://blog.csdn.net/xiaoyu_750516366/article/details/121177872
系统资源控制
每一个进程都有自己的一组资源限制,在(*)inux系统中我们可以通过
1 | int getrlimit(int resource, struct rlimit *rlim); |
resource:可能的选择有
RLIMIT_AS //进程的最大虚内存空间,字节为单位。
RLIMIT_CORE //内核转存文件的最大长度。
RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这一信号的默认行为是终止进程的执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,核心会以每秒一次的频率给其发送SIGXCPU信号,直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。
RLIMIT_DATA //进程数据段的最大值。
RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情况下将终止进程的执行。
RLIMIT_LOCKS //进程可建立的锁和租赁的最大值。
RLIMIT_MEMLOCK //进程可锁定在内存中的最大数据量,字节为单位。
RLIMIT_MSGQUEUE //进程可为POSIX消息队列分配的最大字节数。
RLIMIT_NICE //进程可通过setpriority() 或 nice()调用设置的最大完美值。
RLIMIT_NOFILE //指定比进程可打开的最大文件描述词大一的值,超出此值,将会产生EMFILE错误。
RLIMIT_NPROC //用户可拥有的最大进程数。
RLIMIT_RTPRIO //进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
RLIMIT_SIGPENDING //用户可拥有的最大挂起信号数。
RLIMIT_STACK //最大的进程堆栈,以字节为单位。
这2个API来取得和设置资源
getrlimit用来取得setrlimit用来设置 这二个参数都需要一个要控制的资源 比如控制CPU、内存、文件描述符个数等等的控制,作为第一个参数传入,第二个参数是一个rlimit的结构体地址(指针),他的结构如下定义:
定义放在头文件/usr/include/bits/resource.h中
1 | struct rlimit |
结构体中 rlim_cur是要取得或设置的资源软限制的值,rlim_max是硬限制
这两个值的设置有一个小的约束:
1) 任何进程可以将软限制改为小于或等于硬限制
2) 任何进程都可以将硬限制降低,但普通用户降低了就无法提高,该值必须等于或大于软限制
3) 只有超级用户可以提高硬限制
一个无限的限制由常量RLIM_INFINITY指定(The value RLIM_INFINITY denotes no limit on a resource )
漏洞模块rtnetlink分析
netlink机制有很多协议,每个协议处理不同的事情,rtnetlink就是netlink的其中一个协议,下面就是netlink协议的一些宏定义。
1 |
|
每种协议处理不同的事情,那rtnetlink是干什么的呢,在我的初步了解中,rtnetlink主要可以更改和获取内核的一些网络配置,比如说网络路由、IP地址、链接参数、邻居设置、排队规则、流量类别和数据包分类器都可以通NETLINK_ROUTE套接字进行控制。
rtnetlink主要由以下消息类型组成
- RTM_NEWLINK、RTM_DELLINK、RTM_GETLINK创建、删除或获取有关特定网络接口的信息。
- RTM_NEWADDR、RTM_DELADDR、RTM_GETADDR添加、删除或接收有关与接口关联的IP地址的信息。
- RTM_NEWROUTE、RTM_DELROUTE、RTM_GETROUTE创建、删除或接收有关网络路由的信息。
- RTM_NEWNEIGH、RTM_DELNEIGH、RTM_GETNEIGH添加、删除或接收有关邻居表条目的信息(例如,ARP条目)。
- RTM_NEWRULE、RTM_DELRULE、RTM_GETRULE添加、删除或检索路由规则。
- RTM_NEWQDISC、RTM_DELQDISC、RTM_GETQDISC添加、删除或获取排队规则。
- RTM_NEWTCLASS、RTM_DELTCLASS、RTM_GETTCLASS添加、删除或获取流量类别。
- RTM_NEWTFILTER, RTM_DELTFILTER, RTM_GETTFILTER添加、删除或接收有关流量过滤器的信息。
rtnetlink相关代码分析
使用NETLINK_ROUTE
就可以和rtnetlink进行通信了,rtnetlink有不同的消息类型,不同的消息类型也有不同的type,所以rtnetlink进行初始化的时候就会针对不同情况注册不同的操作函数。
1 | void __init rtnetlink_init(void) |
主要就是调用了rtnl_register()
函数。
1 | void rtnl_register(int protocol, int msgtype, |
通过rtnl_register()
函数声明可见不同消息类型的不同type有两种操作,一种是doit
,一种是dumpit
。有的类型这两种操作都有,有的类型只有一种。
在rtnl_register()
函数中又调用了rtnl_register_internal
。
1 | static int rtnl_register_internal(struct module *owner, |
涉及到的结构体如下
1 | struct rtnl_link { |
有个全局指针数组static struct rtnl_link __rcu *__rcu *rtnl_msg_handlers[RTNL_FAMILY_MAX + 1];
,他其实是一个二重指针,第一重指针的下标是消息类型,第二重下标是消息的type,所以每一个消息类型的每一个type都对应一个struct rtnl_link
结构体。
除了rtnetlink_init
会注册消息的操作之外,tc_filter_init
也会注册一些消息的操作,其中RTM_NEWTFILTER
这个类型就是添加一个流量过滤器,他只有doit
操作,函数为tc_new_tfilter()
.
1 | static int __init tc_filter_init(void) |
现在稍微理清了每个消息类型的每个type如何在内核中组织存储,那该如何调用这些消息的操作函数呢,比如说RTM_NEWTFILTE
的doit
.
当用户进程通过NETLINK_ROUTE
创建套接字并且发送RTM_NEWTFILTER
消息用于创建一个流量过滤器时,内核会调用rtnetlink_rcv_msg()
函数来处理rtnetlink消息。
struct nlmsghdr *nlh
这个结构体在学习netlink的时候就已经见过了,其中family
就是消息类型也就是protocol
,type就是msgtype
,然后调用link = rtnl_get_link(family, type);
获得对应的link
.获得了link
后就调用link->doit()
函数,进而调用到了tc_new_tfilter()
1 | static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh, |
下面继续分析tc_new_tfilter()
函数,这个函数代码较多就不摆出来了,主要看一下关键代码
在看关键代码之前首先要搞清楚一个数据结构
1 | struct nlattr { |
这个是netlink
一般的数据段格式,图示如下。
一个nlattr
+value
就相当于一个数据段的字段了。其中length
是nlattr+value的总长度。tc_new_tfilter()
函数首先初始化了变量struct nlattr *tca[TCA_MAX + 1]
,他是一个结构体指针数组。数组中的每个指针都指向了一个用户进程传进来的字段的首地址。
获取每一个字段之后,后面就是对字段的解析了,首先是从字段中获取过滤器的名字
1 | if (tcf_proto_check_kind(tca[TCA_KIND], name)) { |
然后是根据chainidx(可控)获取chain,然后根据chain获取一个tp(struct tcf_proto),
1 | tp = tcf_chain_tp_find(chain, &chain_info, protocol, |
如果tp不存在还会根据过滤器名称name
调用tcf_proto_create(
创建一个新的tp
1 | tp_new = tcf_proto_create(name, protocol, prio, chain, |
1 | static struct tcf_proto *tcf_proto_create(const char *kind, u32 protocol, |
在tcf_proto_create()
中会根据name
即kind
调用函数tcf_proto_lookup_ops()
获得对应的ops
,内核本来就有一些ops
,查找对应ops的原理就是对比kind==ops->kind
,如果等于那就返回这个ops的首地址。
比如如果传入的kind="route"
就会返回这样的ops
1 | static struct tcf_proto_ops cls_route4_ops __read_mostly = { |
然后初始化tp
的一些字段。
最后调用tp->ops->init
即route4_init
函数,这个函数创建了一个rout4_head
结构体用于存放过滤器对应的哈希值
1 | static int route4_init(struct tcf_proto *tp) |
1 | struct route4_head { |
然后返回到tc_new_tfilter
函数中,把新创建并且初始化的tp
插入到chain
中
1 | tp = tcf_chain_tp_insert_unique(chain, tp_new, protocol, prio, |
然后调用tp->ops->get
即route4_get()
根据handle从route4_head链表中获取对应的route4_filter。如果为空且n->nlmsg_flags & NLM_F_CREATE)
存在或者不为空但n->nlmsg_flags & NLM_F_CREATE)
不存在则调用tp->ops->change
即rout4_change
创建
1 | err = tp->ops->change(net, skb, tp, cl, t->tcm_handle, tca, &fh, |
而rout4_change()
就是漏洞产生的模块。
硬着头皮看了半个晚上的代码终于大概搞懂了相关的结构体的关系以及漏洞原因。
首先是有一个结构体chain
,这个结构体记录了一个tp
的链表,然后tc_new_tfilter()
函数根据用户传进来的一些参数确定一个tp
如果找不到这个tp
那就创建一个新的tp
,关键的是还会创建一个新的route4_head
,记录在这个新tp
的字段里,这个route4_head
就是一个哈希桶,主要记录route4_filter
结构体,route4_head
结构体如下
1 | struct route4_head { |
可以清晰的看见就是一个哈希桶,tp->ops->get()
是会根据用户传入的handle
找对应route4_filter
,找到的话返回,没找到返回null
。
接着调用tp->ops->change()
函数,把get()
函数找到的旧的过滤器也传入,change
首先是会把新的过滤器插入到哈希桶即route4_head
中,接着判断旧的过滤器fold
是否存在,如果存在的话先把她从哈希桶中移出来,然后把他kfree
掉。
1 | static int route4_change(struct net *net, struct sk_buff *in_skb, |
漏洞原理
关键文件就是出在了route4_change
中把旧的过滤器即struct route4_filter
结构体从哈希桶中移出来,然后把他kfree
掉,但是看关键代码,首先使用if判断这个这个fold
是否存在以及他的handle
是否存在,还要满足f->handle != fold->handle
才进入循环里从哈希桶中脱链,如果条件不满足那就进入下一个判断,这个判断只是判断fold
是否存在,如果存在的话就表示旧的过滤器存在,然后把他kfree
掉。
可见由于脱链时判断旧过滤器是否存在和kfree
时判断旧过滤器是否存在的判断依据不一样,这就会导致歧义的出现。假设这样一种情况,旧过滤器的handle
为0,就会导致这个旧的过滤器不会被脱链但是会被kfree。这就可以造成doublefree.
1 | if (fold && fold->handle && f->handle != fold->handle) { |
漏洞复现
环境搭建
首先是得把漏洞模块编译进入内核,其次还要勾上几个编译选项,这些编译选项最好不要直接在.config中进行修改,因为有些编译选项依赖于其他的编译选项,所以最好是在make menuconfig
中进行修改,想要查找某一个编译选项在什么位置可以使用menuconifg的快捷键/
进行搜索。
1 | CONFIG_BINFMT_MISC=y |
poc学习
poc如下
1 |
|
poc写的比较清晰的,首先是socket(AF_NETLINK, SOCK_RAW|SOCK_NONBLOCK, NETLINK_ROUTE);
,然后发了五次包,第一第二次好似是设置网络设备的,比较重要的是第三第四和四五次发包,第三次发包是创建了一个handle
为0的route4_filter
,第四次发包还是传入一个handle
为0的route4_filer
,这样第一次创建的route4_filer
就被释放当时没有脱链,然后第五次发包是删除第一次发包创建的link
,这样就顺带着把他的route4_filer
也给free掉了,这样就构成了一个doublefree
了。而且不止doubelfree了route4_filter
,还doublefree了一个指针数组,前一个的obj的size为kmalloc-192
,后一个是kmalloc-256
触发了doublefree但是内核并没有直接崩溃。
漏洞利用原理
感觉挺有意思的,也学到了很多东西,漏洞利用主要分为两部分,分别是cross cache attack
和dirty cred
,下面分别就这两点详细展开学习。
cross cache attack
他的主要作用就是绕过内核的slab隔离。在没有看n1ctf那道内核题目之前还不是能完全理解这种攻击思路的强大,现在再来看的是发现简直好用的一。
内核是从kmem_cahches
中申请不同大小的obj的,而keme_caches
即kmalloc slab allocation
则是基于buddy allocator
的,buddy allocator
就是伙伴系统,当kmalloc cache上没有足够的obj的时候,就会向buddy allocator申请order-n page
,具体会调用 new_slab()
-> allocate_slab()
-> alloc_slab_page()
向 buddy allocator 申请页。
1 | /* |
其中order-n中的n到底是多少就看这个slab的类型了,可以通过cat /proc/slabinfo
快速知道,我查看我自己ubuntu16的cred的slab,发现要要是用伙伴系统中的order-2
,也就是一次向伙伴系统中直接申请两个连续的页面用于cred的slab。
1 | ➜ ~ sudo cat /proc/slabinfo | grep cred |
- buddy allocator 为每个 order-n page 保存着一个 FIFO queue 数组,order-n page 表示 2^n个连续页的内存。当你释放chunk后导致slab全部空闲时,slab allocator 就会将页还给 buddy allocator。
- slab对应的order由很多因素决定,如 slab chunk 大小、系统定义、内核编译等,最简单的方法是查看
/proc/slabinfo
。 - 如果所申请的 order-n page 队列为空,则将 order-n+1 的页一分为二,一半返回给申请者,一半保存在 order-n 中;如果1个page返回给 buddy allocator,且其对应的 buddy page 也在同一队列中,则整合后放在下一order的page队列中。
cross cache attack原理攻击的整体思路是,当一个slab 页面被全部释放的时候会被回收,这时被回收的页面是可以被其他种类的slab使用的这样就可以跨slab种类来进行利用,如Zhenpeng Lin 的ppt中演示的:
假定我们有一个非法释放漏洞(或double free),但只能释放普通slab 中的堆块:
- 1.首先喷射一堆该大小的普通堆块,这样会消耗一大堆slab 页面。我们的double free目标指针指向其中一个堆块,先将其释放
- 2.然后将喷射的一大堆普通堆块都释放掉,这样double free目标堆块所在slab 页面中的所有堆块(绝大概率)会被都释放掉,该slab 页面为空,会被系统回收
- 3.这时喷射一大堆filp / 其他slab 类型的堆块,这样目标指针所在页面大概率会被filp 类型slab或其他目标类型slab重新申请到吗,并且目标指针(double free漏洞指针)指向其中一个struct file结构体
- 4.使用漏洞的第二次释放能力,该struct file结构体被非法释放
dirty cred
struct file
很有意思的一个攻击思路。主要的思路就是利用高凭证替换低凭证。而凭证一般就是cred
和file
,下面主要探讨在doublefree
情况下如何进行凭证替换。
file结构体是打开一个文件时就会创建的一个结构体。
1 | struct file { |
下面是介绍dirty cred
的论文中提到的doublefree情况下的利用过程,但我觉得其实没必要这么麻烦的,如果只是高凭证替换低凭证的话,假如ptr1
拥有doublefree,那先把ptr1给free一次,然后让低凭证申请到这个obj,就记作ptr2,然后再free一次ptr1,就把低凭证也给free掉了,接着再堆喷低凭证,再次申请到ptr2指向的内存,记作ptr3,这样ptr2和ptr3就指向了桶一块低凭证struct file
了,然后通过系统调用kcmp
来得知ptr2
和ptr3
指向同一个struct file
(因为是堆喷),然后释放低凭证的struct file
就能替换成高凭证了。
哦我懂了,下面的方法其实更加通用,因为如果能doublefree
的话可能obj的大小不等于struct file
的大小,所以可能出现不对齐的现象,所以需要两个ptr指向同一个obj了。上述方法只适用于刚好对齐。
方法:一般 Double-Free 发生在通用cache中,而内核凭证位于 dedicated cache 中,所以这里需要进行 cross-cache 内存布局。内核会回收未使用的内存页,然后分配给其他需要更多空间的cache。
a-d
:两次触发DF,获得2个指向同一漏洞对象的悬垂指针(ptr1'
/ptr2'
);e
:将该通用cache的内存页全部释放归还给页管理器,这样该内存页就可以分配给dedicated cache
(存放凭证对象);f
:分配大量凭证对象(特殊cache)占据漏洞对象对应的空闲块,现在有3个指针指向该内存块了(2个悬垂指针和一个victim对象中的凭证指针,悬垂指针可能未对齐,指向凭证对象的内部);g
:利用其中1个悬垂指针(ptr2'
)释放凭证对象,创造空洞;h
:分配新的低权限凭证对象占据该位置;- 剩余1个悬垂指针(
ptr1'
)指向低权限凭证对象,再次释放后就能用高权限凭证对象替换低权限凭证对象了。
到目前为止已经能凭证替换了,现在就得利用凭证替换来完成对不可写文件的写入了,在老版本4.13以前使用writev
向某个文件中写入内容时逻辑时这样的
- 进行访问权限校验(是否可写)
- 从用户空间获取写入内容
- 实际写入操作
可以看出在验证完权限和实际写入操作之间还有一步操作,这就可以形成条件竞争了,只要验证完可写权限之后就通过堵塞卡在第二步,然后替换成高凭证。再写入的时候就往不可写文件里写入内容了。
d按时这种办法已经是昨日黄花了,在4.13版本以后writev的逻辑就成这样了
- 从用户空间获取写入内容
- 进行访问权限校验(是否可写)
- 实际写入操作
所以在新版本就没办法利用老办法堵塞增大时间窗(从检查权限到真正操作之间的时间)了。但是增大时间窗还是有的,这就利用了文件的innode锁了。
在已经有一个进程对一个文件进行写入操作的时候,会给文件inode上锁,其他向该文件进行写入的进程需要等待上一个进程写入完成解锁。所以就可以有这样的利用了,这样同样可以增大时间窗。
- 先存在一个进程向一个可写文件写入大量内容,inode锁会锁住较长时间
- 第二个进程尝试向该文件写入”打算写入/etc/passwd等特权文件的内容”
- 第三个进程利用漏洞替换file结构体
到这里对struct file的攻击就已经闭环了。
struct cred
对于file类型凭据我们可以使用普通用户可读特权用户可写的/etc/passwd来进行操作,普通用户就可以喷射大量目标用于攻击。但特权的struct cred就没那么容易了。可以通过:
执行大量suid 程序,如sudo(但大部分情况下并没有这个权限)
使用kernel thread,kernel 自己创建的任务是特权任务,我们可以利用一些内核接口控制内核启动一堆kernel thread:
- 利用workqueue
- 利用usermode helper
reading exp
终于到了阅读exp的阶段了,距离写下这篇文章的第一行似乎已经过了两周了。。。令人感叹。
进程A,随时准备喷射/etc/passwd
文件
1 | if (fork() == 0) { |
下面是进程B的代码,但是在进程B执行之前得等进程C执行完,进程C就是堆喷一堆struct file
来耗尽file slab
中的空闲object,进程B就是干了一件事,堆喷很多的route4_filter
,然后把他释放掉,但是它申请的handler都不为0.所以只起了一个耗尽通用slab的obj的作用,等后面全部free的时候就会把对应页交给伙伴系统了。
但我其实不是很能理解为什么要设置user namespace
。
1 | setup_namespace(); |
进程D是关键进程,首先确定已经可以doublefree了,然后喷射一堆低凭证file,接着第一次kfree大小为0x100的obj,然后喷射一堆低凭证file拿到刚free的obj,接着doublefree这个obj,然后再喷射低凭证file,这样就有两个文件描述符指向同一个file而且这个file的f_count为1,接着开启三个线程,替换第凭证为高凭证,前面我已经说过过程了,就不赘述了.
1 | void *slow_write() { |
具体过程可以看这两张图
调试了半个晚上终于把exp中关于file结构体引用计数的问题解决了,在一个进程中,主线程和所有的支线程共用一个struct files_struct
结构体,所以在创建线程的时候并不会像创建子进程一样给所有的file
的结构体的引用计数f_count
加一,但是只要在子线程中使用了这个文件描述符(注意是使用,只有使用了才会加一,使用完还会减一),就会给对应的file
的f_count
加一,表示这个结构体正在被使用,所以在主线程close这个文件描述符之后,对应的file
并没有被kfree掉,而是引用计数减一,但是这个file指针是在主线程中被清零的。而在某个地方肯定还记录着这个file
的指针,以便后续kfree。
但比较奇怪的是一个进程只有线程的时候,就算使用这个文件描述符,文件描述符对应的file
的引用计数还是没有变的。
这是支线程使用write正在写入时file的样子,可见f_count为2.
当开启了支线程但是没有使用文件描述符时file
的样子,可见f_count为1
总结
这个cve的学习总算告一段落了,除了设置user namespace没怎么搞懂以外其他基本都明白了,也用exp调试打通了自己搭的环境,总的来说确实学到了好多。尤其是cross cache
和drity cred
,还深入的了解了文件描述符到底是个什么东东了。