KernelPwn之堆利用(userfaultfd+msg消息队列+pipebuffer)

0rb1t Lv2

前置知识

  • UAF
  • 内核堆分配(部分了解)
  • 内存映射

以下分析使用的都是linux-5.9.10的源码。

Userfaultfd机制

进程在访问内存时,并不会直接访问物理内存,而是访问物理内存映射的虚拟内存,然后通过MMU查找虚拟内存对应的物理内存。

选自csdn

MMU通过段表和页表查询虚拟地址对应的物理地址,从而返回数据或者修改数据。

当访问到一块未映射的虚拟内存时,MMU查询没有找到对应的物理内存就会触发缺页异常。

一般系统会自动调用异常处理函数来处理异常,但用户进程可以为自己的虚拟地址设置缺页异常处理函数。

通过调用SYS_userfaultfd可以创建文件标识符uffd,再通过ioctl控制命令UFFDIO_API请求验证版本号和启用某些特性,如果内核userfaultfd的版本号是请求的版本号,并且支持请求启用的所有特性,那么成功启用userfaultfd,返回内核支持的所有特性和控制命令。

最后通过uffdio_register控制命令注册虚拟内存范围,跟踪指定虚拟地址范围的页错误异常。

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
struct uffdio_api {
__u64 api;
__u64 features;//启用特性
__u64 ioctls;
};
struct uffdio_register {
struct uffdio_range range;//虚拟地址检测范围
__u64 mode;//指定页错误异常
__u64 ioctls;
};

void RegisterUserfault(void* start, int len, void* handler) {
int uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
uffdio_register ur = { 0 };
uffdio_api ua = { 0 };
ur.range.start = (ULL)start;
ur.range.len = len;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
ua.api = UFFD_API;
ua.features = 0;//启用默认特性
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
ErrExit("[-] ioctl-UFFDIO_API");
ioctl(uffd, UFFDIO_REGISTER, &ur);//注册虚拟地址
pthread_t pt;
pthread_create(&pt, NULL, (void* (*)(void*))handler, (void*)uffd);//启动异常接收处理函数
}

如果访问注册的虚拟地址范围并产生页错误异常,uffd文件便会转为可读,并存放事件消息,此时原线程会卡住等待handler函数回复。

handler函数则需要通过select或者poll监听文件可读,接收事件消息,其中事件有以下几种类型

  1. UFFD_EVENT_PAGEFAULT:页错误异常。
  2. UFFD_EVENT_FORK:fork调用。
  3. UFFD_EVENT_REMAP:mremap调用。
  4. UFFD_EVENT_REMOVE:madvise(MADV_REMOVE)和madvise(MADV_DONTNEED)调用。
  5. UFFD_EVENT_UNMAP:munmap调用。

然后使用控制命令UFFDIO_COPY把数据复制到触发页错误异常的虚拟页,或者使用控制命令UFFDIO_ZERO把虚拟页映射到零页。

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 uffdio_copy {
__u64 dst;
__u64 src;
__u64 len;
#define UFFDIO_COPY_MODE_DONTWAKE ((__u64)1<<0)
__u64 mode;
__s64 copy;
};

void UserfaultHandler(int uffd) {
uffd_msg msg;
uffdio_copy uc = { 0 };
unsigned long* data = (unsigned long*)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);//创建一块新内存
for (;;) {
pollfd pf = { 0 };
pf.fd = uffd;
pf.events = POLLIN;
poll(&pf, 1, -1);//监听事件
read(uffd, &msg, sizeof(msg));
if (msg.event != UFFD_EVENT_PAGEFAULT) {//判断是否为页错误异常
puts("Pass one event.");
continue;
}
uc.src = (ULL)data;//即将复制的源数据
uc.dst = msg.arg.pagefault.address & ~(getpagesize() - 1);//发生错误的虚拟页
uc.len = getpagesize();
uc.mode = 0;
uc.copy = 0;
ioctl(uffd, UFFDIO_COPY, &uc);
break;
}
}

在linux-5.11之前,普通用户都可以注册缺页异常处理函数,但在之后限定成仅有特权用户可注册。

因此在5.11之前的内核,我们可以给一块未映射的内存注册userfaulthandler,然后内核在调用copy_from_user将该内存复制到内核堆空间时就会卡住等待回复。如果此时另一个线程调用内核函数删除该内核堆,并调用其他函数创建新的堆块,再恢复卡住的线程就会造成uaf。

msg消息队列

内核进程之间或者用户进程之间以及内核和用户之间进行通信时可以使用msg消息队列进行通信。

用户可通过msgget获取唯一标识符msgid,通过__key查找对应的msg队列,IPC_PRIVATE表示创建新的msgid__msgflg0666|IPC_CREAT表示key有对应的msg队列就返回该队列id,否则创建新的队列并返回id。

1
2
int msgget (key_t __key, int __msgflg);
int msgid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);

得到msgid后可通过msgsnd发送消息。

1
2
3
4
5
6
7
8
9
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz,
int msgflg)
{
long mtype;

if (get_user(mtype, &msgp->mtype))//一般mtype为1即可。
return -EFAULT;
return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
}

其中msgp表示需要发送的消息,mtype表示类型,mtext为可拓展成员,具体大小自定义。

1
2
3
4
struct msgbuf {
__kernel_long_t mtype; /* type of message */
char mtext[1]; /* message text */
};

do_msgsnd关键代码如下

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

......

msg = load_msg(mtext, msgsz);//处理用户的msg
msg->m_type = mtype;
msg->m_ts = msgsz;
......
msq = msq_obtain_object_check(ns, msqid);
......
if (msgflg & IPC_NOWAIT) {//IPC_NOWAIT为00004000,一般设置该flag
err = -EAGAIN;
goto out_unlock0;
}
......
if (!pipelined_send(msq, msg, &wake_q)) {
/* no one is waiting for this message, enqueue it */
list_add_tail(&msg->m_list, &msq->q_messages);
msq->q_cbytes += msgsz;
msq->q_qnum++;
atomic_add(msgsz, &ns->msg_bytes);
atomic_inc(&ns->msg_hdrs);
}
......

先了解三个结构体,msg_msg msg_queue以及msg_msgseg

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
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */

struct list_head q_messages; //多个msg_msg结构体互链形成的双向链表
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;

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

struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};

每个msgid都对应一个msg_queuemsg_queue中存放着msg_msg组成的双向消息链表。

上述源码中的load_msg便是将用户传入的消息buf处理,生成msg_msg

然后将其链入msgid对应的msg_queue->q_messages

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
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);//根据传入的len创建msg
if (msg == NULL)
return ERR_PTR(-ENOMEM);
//PAGE_SIZE也可能为0x2000,以实际情况为主
alen = min(len, DATALEN_MSG);//DATALEN_MSG = ((size_t)PAGE_SIZE-sizeof(struct msg_msg)) = 0x1000-0x30 = 0xfd0

//然后遍历复制用户buf
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;

可见load_msg先调用alloc_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
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);//可以自定义创建0x30-PAGE_SIZE的堆块。
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);//能创建8-PAGE_SIZE的堆块
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}

return msg;

借用lotus的关系图,可知msg_msgmsg_msgseg的关系如下

msg_msg的0x30偏移开始存放用户data。
如果msgsz不大于((size_t)PAGE_SIZE-sizeof(struct msg_msg))时,只会创建msg_msg来保存整个消息。

msgsz大于((size_t)PAGE_SIZE-sizeof(struct msg_msg))时,便会扩展创建msg_msgseg单向链表来存储多余的数据。

由上分析可知,我们能利用msgsnd创建8-PAGE_SIZE的堆块。

然后分析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);
}

do_msgrcv部分源码

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
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;
......
if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy))
return PTR_ERR(copy);
}
mode = convert_mode(&msgtyp, msgflg);
......
msq = msq_obtain_object_check(ns, msqid);
......
msg = find_msg(msq, &msgtyp, mode);
......
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);
......
out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}

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

return bufsz;

msg通过find_msg函数获取

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

它会遍历msq->q_messages双向链表,通过testmsg匹配消息并返回。

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

testmsg根据mode选择筛选模式,然后根据type找到指定消息,如果modeSEARCH_NUMBER就会直接从首链取出msg。

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

msgflgMSG_COPY时,modeSEARCH_NUMBER

此时会从消息链表中取出第一个msg(此时还不会将其从链中删除)。

并且prepare_copy会创建一个消息副本,并把取出的msg复制到副本中并返回,然后直接跳转到out_unlock0而非调用list_del将msg从链中删除。

1
2
3
4
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}
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
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
struct msg_msgseg *dst_pseg, *src_pseg;
size_t len = src->m_ts;
size_t alen;

if (src->m_ts > dst->m_ts)//m_ts表示着消息内容大小
return ERR_PTR(-EINVAL);

alen = min(len, DATALEN_MSG);
memcpy(dst + 1, src + 1, alen);

for (dst_pseg = dst->next, src_pseg = src->next;
src_pseg != NULL;
dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {

len -= alen;
alen = min(len, DATALEN_SEG);
memcpy(dst_pseg + 1, src_pseg + 1, alen);
}

dst->m_type = src->m_type;
dst->m_ts = src->m_ts;

return dst;
}

由上可知,当msgflg满足MSG_COPY时,会直接将第一个消息复制一份并返回。

了解了msggetmsgsndmsgrcv后,我们可以将其与userfaultfd结合辅助实现uaf。

流程如下:

  1. 调用RegisterUserfault为一个未映射的内存设置缺页异常处理函数。
  2. add一个内核堆块。
  3. 创建一个线程函数UAF,等待信号1然后delete该内核堆块。
  4. edit调用copy_from_user将未映射区作为参数。
  5. 此时主线程断住,handler线程接收事件并进行操作。
  6. handler线程发送信号1并等待信号2,UAF线程接到信号删除该堆块,并调用msgsnd创建一样大小的堆块,再发送信号2。
  7. handler接收信号2,继续修改该内核堆块,此时该堆块和msg的堆块属于同一个堆块,所以我们可以修改msg的m_ts变大。
  8. msgrcv接收消息,可实现越界读。

pipe_buffer结构体利用

用户进程可通过pipe函数创建管道,其中pipedes数组用与接收readwrite的管道描述符。

1
int pipe (int __pipedes[2]);

其中pipe有如下一条调用链:

pipe->do_pipe2->__do_pipe_flags->create_pipe_files->get_pipe_inode->alloc_pipe_info

其中alloc_pipe_info函数部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
......

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

其中sizeof(struct pipe_inode_info)大小为80,sizeof(struct pipe_buffer)为0x28,

PIPE_DEF_BUFFERS为0x10。

所以kcalloc(pipe_bufs, sizeof(struct pipe_buffer),GFP_KERNEL_ACCOUNT);会创建0x400的堆块(实际使用0x28*0x10 = 0x280),即12个pipe_buffer形成的环形队列。

pipe_buffer结构体如下

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

可以看到pipe_buffer带有一个ops

1
2
3
4
5
6
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);
bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};

当我们调用write或者readpipe中写入或者读取内容,会调用file->fop指向的函数表中的函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const struct file_operations pipefifo_fops = {
.open = fifo_open,
.llseek = no_llseek,
.read_iter = pipe_read,
.write_iter = pipe_write,
.poll = pipe_poll,
.unlocked_ioctl = pipe_ioctl,
.release = pipe_release,
.fasync = pipe_fasync,
};
f = alloc_file_pseudo(inode, pipe_mnt, "",
O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT)),
&pipefifo_fops);


可以看到write会调用pipe_write进行处理

pipe_write部分源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct file *filp = iocb->ki_filp;
struct pipe_inode_info *pipe = filp->private_data;
unsigned int head;
ssize_t ret = 0;
size_t total_len = iov_iter_count(from);
ssize_t chars;
bool was_empty = false;
bool wake_next_writer = false;
......
head = pipe->head;
was_empty = pipe_empty(head, pipe->tail);
chars = total_len & (PAGE_SIZE-1);
if (chars && !was_empty) {
unsigned int mask = pipe->ring_size - 1;
struct pipe_buffer *buf = &pipe->bufs[(head - 1) & mask];
int offset = buf->offset + buf->len;

if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
offset + chars <= PAGE_SIZE) {
ret = pipe_buf_confirm(pipe, buf);

可以看到调用write会取出pipe_buffer队列中的第一个pipe_buffer然后判断其flagsoffset是否满足条件,满足则调用pipe_buf_confirm转而调用buf->ops->confirm

1
2
3
4
5
6
7
static inline int pipe_buf_confirm(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
if (!buf->ops->confirm)
return 0;
return buf->ops->confirm(pipe, buf);
}

所以我们可以劫持pipe_bufferops实现rip劫持,其中rsi为pipe_buffer的堆地址,可供后续magic_gadget利用。

  • Title: KernelPwn之堆利用(userfaultfd+msg消息队列+pipebuffer)
  • Author: 0rb1t
  • Created at : 2024-08-12 13:19:16
  • Updated at : 2024-08-18 22:56:32
  • Link: https://redefine.ohevan.com/2024/08/12/KernelPwn之堆利用-userfaultfd-msg消息队列-pipebuffer/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
KernelPwn之堆利用(userfaultfd+msg消息队列+pipebuffer)