在复现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:
这里不对漏洞做过多分析,主要阐述编译过程
使用的poc如下:
https://github.com/sang-chu/CVE-2022-2588
漏洞exp以及漏洞原理简述如下:
https://github.com/Markakd/CVE-2022-2588
漏洞所在函数是net\sched\cls_route.c: route4_change
所使用内核版本:5.13.19,正好手头有上次复现剩下的,就不重新下载了
直接定位到漏洞所在函数所在文件net\sched\cls_route.c,找到跟这个文件同一目录下的Makefile,以该文件名作为关键字搜索:
obj-$(CONFIG_NET_CLS_ROUTE4) += cls_route.o
可以看到,该文件所需的编译选项是
CONFIG_NET_CLS_ROUTE4=y
这里使用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错误编号的含义,那就只能一步一步确认了。
由于我对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,方法和上面一样,断住所有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
总共就三个编译选项,废了这么多事。