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

文章目录

    • 背景
    • 目标
      • 前置知识
        • 内核与内核模块
        • 内核的编译选项
    • 过程
      • 基本信息
      • 根据Makefile确定漏洞函数所在模块
      • 使用ubuntu内核编译方法初步编译
      • 定位错误编号 -95
      • 定位错误编号 -2
    • 总结

背景

在复现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可以显示某个编译选项所依赖的其他选项。

[kernel] 编译能复现指定poc的内核的排错过程_第1张图片

可以看出编译选项CONFIG_NFT_CONNLIMIT的depends on中有些依赖还是m状态,那么该选项(CONFIG_NFT_CONNLIMIT)就无法被设置成y,只有当满足的依赖都是y的情况下,才能设置成y:

[kernel] 编译能复现指定poc的内核的排错过程_第2张图片

  • 还有一些编译选项是自动设置的,根据其依赖的选项,依赖项都是m那么就自动设置成m,依赖项设置成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,正好手头有上次复现剩下的,就不重新下载了

根据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

[kernel] 编译能复现指定poc的内核的排错过程_第3张图片

然后编译:

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。

[kernel] 编译能复现指定poc的内核的排错过程_第4张图片

#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,但可以查看调用栈:
[kernel] 编译能复现指定poc的内核的排错过程_第5张图片

先看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函数:

[kernel] 编译能复现指定poc的内核的排错过程_第6张图片

然后分析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

然后重新跑,新报错:

[kernel] 编译能复现指定poc的内核的排错过程_第7张图片

定位错误编号 -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

重新编译重新跑:

[kernel] 编译能复现指定poc的内核的排错过程_第8张图片

成功断住关键函数route4_change,并且触发uaf:

[kernel] 编译能复现指定poc的内核的排错过程_第9张图片

[kernel] 编译能复现指定poc的内核的排错过程_第10张图片

总结

总共需要这么几个编译选项:

CONFIG_NET_CLS_ROUTE4=y
CONFIG_DUMMY=y
CONFIG_NET_SCH_QFQ=y

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

你可能感兴趣的:(#,linux,kernel,二进制,linux,kernel,linux,内核学习,内核,内核安全)