环境搭建
commit:2c85ebc57b3e1817b6ce1a6b703928e113a90442
总的config:
defconfig+menuconfig
CONFIG_CONFIGFS_FS=y #支持img |
(同样是修改了objtool的一个代码)
替换busybox中的tc
busybox中的tc只有查看的功能,我们需要将其替换为我们虚拟机中的tc;
就是将我们的tc复制过去(笔者复制到了/bin/目录下),然后ldd查看相关依赖,将所有依赖文件全部按照相同路径复制即可;
前置知识&&代码片段解读
先总结一下本CVE要用到的前置知识:
首先要根据看图理解linux内核网络流量控制工具tc(Traffic Control) (wujiuye.com)大体了解tc、qdisc到底是个什么东西;
然后根据POC里的命令行加深理解,以及调试相关例如fw_change函数等等;
环境方面主要是解决替换busybox中的tc的问题;
在 CVE-2023-4207 漏洞的背景下,涉及到的 cls_fw
是 Linux 内核中的一种过滤器,基于防火墙规则来分类数据包。过滤器在更新或修改时,内核会通过 fw_change()
函数来处理旧的过滤器实例并创建新的实例。由于内核处理这些过滤器实例的方式不正确,导致了 use-after-free 漏洞的产生。
socketpair
下面分析如下代码片段:
socketpair(AF_UNIX, SOCK_STREAM, 0, cfd); |
AF_UNIX是协议族;socketpair可以创建一对用于相互通信的socket,其中cfd的定义是ing cfd[2]
,一次性返回我们两个文件描述符;
设置文件描述符限制
rlimit 是一个结构体,用于定义资源限制。这段代码中, 结构体被初始化为:
rlim_cur = 0xf000
:设置当前(软限制,soft limit)为 61440(十六进制0xf000
表示的十进制数)。rlim_max = 0xf000
:设置最大(硬限制,hard limit)也为 61440。
set_cpu(1); |
AF_INET
:表示使用 IPv4 地址族。
SOCK_RAW
:表示这是一个原始套接字,允许对底层协议直接进行操作,而不是通过标准的 TCP 或 UDP 协议。
IPPROTO_RAW
:表示套接字的协议类型为 RAW IP
,允许发送自定义的 IP 数据包。
设置套接字规则
setsockopt(s, 0, 64, buf, n);
这行代码使用 setsockopt
设置套接字 s 的选项:
s
:套接字描述符。0
:指定的协议层,这里可能是 IP 层(IPPROTO_IP)。64
:选项名(optname
),具体选项的含义取决于系统的定义(可能与 IP 层的某个设置相关)。buf
:包含选项数据的缓冲区。n
:选项数据的长度。
setsockopt
setsockopt 函数原型如下:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); |
sockfd是套接字描述符;
level 是协议层或者级别;
optname 是设置的选项名称;
optval 指向包含选项值的缓冲区的指针。这个值的类型和内容取决于所设置的选项。例如,对于 SO_RCVBUF
,optval
是一个指向整数的指针,表示缓冲区的大小。
optlen optval
的大小,通常是 sizeof(*optval)
,也就是 optval
所指向的数据的长度。例如,如果 optval
是一个指向整数的指针,optlen
通常是 sizeof(int)
。
通过c代码实现tc功能
漏洞分析
是一个kmalloc-128的UAF漏洞;
源代码:
https://elixir.bootlin.com/linux/v5.10/source/net/sched/cls_fw.c#L237 |
首先在源码中看fw_filter结构体的定义,注意到其中有个tcf_result类型的res成员:
在fw_change中这个f是一个fw_filter结构体:
在fw_change执行过程中,会新分配一个fw_filter 叫做 fnew,然后直接将这个tcf_result复制过去,这就相当于两个过滤器同时具有一个成员:
POC
unshare --mount --uts --ipc --net --pid --fork --map-root-user --user --mount-proc /bin/sh |
创建新的过滤器
源代码:
https://elixir.bootlin.com/linux/v5.10/source/net/sched/cls_api.c#L1944 |
fw_change
源代码:
https://elixir.bootlin.com/linux/v5.10/source/net/sched/cls_fw.c#L237 |
其在整个Linux源代码中只有一处引用,就是这个ops;
在tc_new_tfilter函数中找到一处关于这个函数表中change的调用:
https://elixir.bootlin.com/linux/v5.10/source/net/sched/cls_api.c#L1944 |
https://elixir.bootlin.com/linux/v5.10/source/net/sched/cls_api.c#L2129 |
首先发现不管是创建还是替换都会在这个地方退出:
这里很奇怪调试明明是个5,结果telescope出来一个0,直接寄了,即便改了也还是无法成功返回;
复制tcf_result导致引用计数减少
在fw_change中首先是把旧的filter的res直接给了新的filter:
然后是对着旧的filter调用tcf_unbind_filter:
下面跟进看tcf_unbind_filter:
xchg是一个用于交换的宏;
https://elixir.bootlin.com/linux/v5.10/source/net/sched/sch_drr.c#L197 |
释放drr_class
首先是在drr_destroy_class函数中,如果drr_class的引用计数为0,就会先qdisc_put释放掉qdisc,然后kfree掉这个drr_class:
https://elixir.bootlin.com/linux/v5.10/source/net/sched/sch_drr.c#L142 |
下面找到了两处关于该函数的调用:
drr_delete_class又是qdisc_class_ops函数表中的一个函数,对应delete:
所以总结下来就是希望通过删除drr_class最终导致qdisc的误删,(先是给qdisc)
下面进入这个qdisc_put函数:
https://elixir.bootlin.com/linux/v5.10/source/net/sched/sch_generic.c#L965 |
其核心是qdisc_destroy:
然后会通过rcu释放,就调用到了qdsic_free,因为笔者在调试qdisc_free的时候也看到了qdisc_free_cb这个东西;
最后贴一张笔者分析的图:
漏洞触发
qdisc和class之间的关系
发送消息之后qdisc的行为:
如果我们发送一个满足要求的消息,对应的qdisc就会在drr_enqueue函数中被引用,
创建drr_class的时候会创建一个新的qdisc
相关函数调用链如下:
源代码路径如下:
https://elixir.bootlin.com/linux/v5.10/source/net/sched/sch_drr.c#L54 |
在创建class的时候会在这个地方吃瘪:
这里发现两次replace可以将这个filter_cnt变成-1:
流量走不到子qdisc的原因
这要求我们给我们的流量打上标记,其实这个在exp中有所体现:
似乎需要CONFIG_NETFILTER_XT_TARGET_MARK
内核编译选项才行;
经过调试,发现他们给的ip0和ip1我用不了呜呜呜,但是笔者在Linux socket设置mark的必要性_socket mark-CSDN博客这篇文章中找到了解决办法,并成功触发poc;
然后通过进一步调试发现,其实是首先调用drr_enqueue,这是我们的流量走到了我们一开始创建的qdisc中,而正是在这个drr_enqueue中会继续通过mark匹配流量,然后就会走到我们误删的那个qdisc上,找它的第一个指针作为入队函数,劫持了它就相当于劫持了控制流;
调试
gdb -ex "target remote localhost:1234" -ex "file /mnt/hgfs/VMshare2/cve/all/CVE-2023-4207/vmlinux" -ex "c" |
这里记录一下调试技巧,几个重要的函数:qdisc_alloc、drr_enqueue、qdisc_free
首先是在我们激活接口的时候会调用qdisc_alloc分配一个qdisc1,然后我们add一个qdisc的时候会分配一个qdisc2,之后释放掉qdisc1,然后我们创建一个class的时候会创建一个qdisc3(这是我们的攻击目标);
之后添加一个filter,此时qdisc2作为总的qdisc,它有一个filter,filter中有class,class中又有一个qdisc,就是qdisc3;
当我们replace掉filter,旧的filter的res被复制到了新的中(仍在使用),但是旧的filter的res会参与tcf_unbind_filter,导致drr_class的引用计数减少,如果是1就减少到了0,(笔者尝试过,如果多次replace还能减少到负数);
之后我们删除掉这个class,就会检查引用计数发现是0,则可以删除drr_class,并在kfree之前先释放其qdisc(就是我们的qdisc3);
而之后如果我们发送带有相应标记的数据包,先到了总的qdisc上(也就是qdisc2),然后会继续走classify,找到qdisc3,然后调用,但是此时qdisc3已经是UAF了,所以函数指针就崩了,panic咯!
利用思路
本来想利用pg_vec的,但是由于开启了NX保护,所以会panic,同时还有个问题搞不懂就是pg_vec的第一个物理页的头几个字节好像很固定,写了之后很快就被赋值了。
drr_class利用
其实不仅仅qdisc被误删了,drr_class其实也是被误删了,并且UAF的qdisc也是通过这个drr_class找到的,而qdisc位于drr_class的0x60偏移位置,所以可以通过喷射drr_class的obj出来,劫持控制流;
删除drr_class到底是什么意思,是只有drr_class的引用为0的时候才能删除?我们在一个qdisc上加了一个类、加了一个filter;然后替换filter,此时class还有引用,不应该被删除,但是还是被删除了;
可见drr_classify还是要先找class,然后找到qdisc,进行子qdisc的drr_enqueue;
所以笔者采用了以下方法,利用pg_vec对drr_class进行uaf,这样drr_class->qdisc的位置上就会被填入了一个物理页地址,虽然我们不知道其值,但是我们可以通过mmap将其映射到一个用户态地址,然后向里边写进去一个数值,这就会被作为我们的目标地址去执行;
现在我们已经有了一个很稳定的控制流劫持:
eBPF利用
这里首先要保证以下几个编译选项:
CONFIG_BPF=y |
源代码:
https://elixir.bootlin.com/linux/v5.10/source/kernel/bpf/core.c |
可以看到在bpf_jit_alloc_exec函数就是module_alloc函数的封装,其会分配一个模块加载地址,然后写入翻译好的机器码;
LEAK&&rdmsr 指令
rdmsr
(Read Model-Specific Register)指令是用于从处理器的特定型号寄存器(MSR,Model-Specific Register)中读取数据的指令。MSR 是一组特定于处理器型号的寄存器,主要用于控制和调试功能,例如性能监视、系统管理模式、调试等。
攻击思路就是在eBPF机器码中先给rcx寄存器赋值为 0xC0000082,然后执行rdmsr指令,此时entry_SYSCALL_64 的地址就会被存放到 edx | eax 中,然后我们就可以在后续的shellcode中计算出来内核基地址,目标是core_pattern
和 _copy_from_user
,然后调用覆盖core_pattern实现提权;
core_pattern
攻击成功
思维导图如下:
bullseye适配成功
参考
看图理解linux内核网络流量控制工具tc(Traffic Control) (wujiuye.com)
qemu + busybox + gdb 构建linux内核调试环境_qemu gdb busybox-CSDN博客
Linux socket设置mark的必要性_socket mark-CSDN博客
TODO
这里简单记录一下有些exp中挺有意思但是笔者没有实现的部分:
- 利用多线程喷射ctl_buf,这个其实应该还是蛮有用的,尤其是一些比较不好的size的uaf写;