CVE-2023-4207


环境搭建

commit:2c85ebc57b3e1817b6ce1a6b703928e113a90442

总的config:

defconfig+menuconfig

CONFIG_CONFIGFS_FS=y  #支持img
CONFIG_SECURITYFS=y #支持img
CONFIG_DEBUG_INFO=y #调试
CONFIG_USER_NS=y #支持新的namespace
CONFIG_USERFAULTFD=y #支持userfaultfd
CONFIG_NET_SCHED=y #漏洞触发必要选项
CONFIG_NET_CLS_FW=y #漏洞触发必要选项

CONFIG_NETFILTER_XT_TARGET_MARK=y
CONFIG_NET_SCH_DRR=y #使用drr

CONFIG_BPF=y #漏洞利用所必须
CONFIG_BPF_JIT=y #漏洞利用所必须
CONFIG_HAVE_EBPF_JIT=y #漏洞利用所必须

CONFIG_PREEMPT=y

(同样是修改了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);
socketpair(AF_UNIX, SOCK_STREAM, 0, stopfd);
struct rlimit rlim = {
.rlim_cur = 0xf000,
.rlim_max = 0xf000};
setrlimit(RLIMIT_NOFILE, &rlim);

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);
int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
int fd = open("./ip0", O_RDONLY);
int n = read(fd, buf, 0x1000);
setsockopt(s, 0, 64, buf, n);
fd = open("./ip1", O_RDONLY);
n = read(fd, buf, 0x1000);
setsockopt(s, 0, 65, buf, n);
set_cpu(0);

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_RCVBUFoptval 是一个指向整数的指针,表示缓冲区的大小。

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

/bin/iptables-legacy -t mangle -A POSTROUTING -d 127.0.0.1/24 -j MARK --set-mark 1

ip link set dev lo up

/bin/tc qdisc add dev lo root handle 1: drr

/bin/tc class add dev lo parent 1: classid 1:10 drr quantum 60

/bin/tc filter add dev lo parent 1: pref 100 protocol ip handle 1 fw classid 1:10

/bin/tc filter replace dev lo pref 100 protocol ip handle 1 fw classid 1:10

/bin/tc class delete dev lo classid 1:10

创建新的过滤器

源代码:

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利用

eBPF

这里首先要保证以下几个编译选项:

CONFIG_BPF=y	
CONFIG_BPF_JIT=y
CONFIG_HAVE_EBPF_JIT=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适配成功

参考

https://github.com/google/security-research/tree/499284a767851f383681ea68e485a0620ccabce2/pocs/linux/kernelctf/CVE-2023-4207_lts_cos

看图理解linux内核网络流量控制工具tc(Traffic Control) (wujiuye.com)

qemu + busybox + gdb 构建linux内核调试环境_qemu gdb busybox-CSDN博客

Linux socket设置mark的必要性_socket mark-CSDN博客

TODO

这里简单记录一下有些exp中挺有意思但是笔者没有实现的部分:

  1. 利用多线程喷射ctl_buf,这个其实应该还是蛮有用的,尤其是一些比较不好的size的uaf写;

文章作者: q1ming
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1ming !
  目录