author: 熊潇 of IceSword Lab
概述
原文: Racing against the clock – hitting a tiny kernel race window
- Part.1: 漏洞原理简述
- Part.2: 对比较容易产生疑惑的地方增加了细节说明
- Part.3: 针对文中提高 race 的技巧做了分析
Part.1
The bug & race
The kernel tries to figure out whether it can account for all references to some file by comparing the file’s refcount with the number of references from inflight SKBs (socket buffers). If they are equal, it assumes that the UNIX domain sockets subsystem effectively has exclusive access to the file because it owns all references.
The problem is that struct file can also be referenced from an RCU read-side critical section (which you can’t detect by looking at the refcount), and such an RCU reference can be upgraded into a refcounted reference using
get_file_rcu()
/get_file_rcu_many()
by__fget_files()
as long as the refcount is non-zero.
unix_gc()
的预期逻辑是:total_refs
和inflight_refs
相同就可以认为此时file
是单独占有的,就可以把skb
和file
一起 free 掉- 下面代码 (3) 在 (1) 和 (2)中间执行则 race 成功
- 如果 race 没有成功,
__fget_files
那里就会发现f_count
是 0 或者 file 是 NULL - 但是如果 race 成功的话,
file->f_count
在__fget_files()
中会被加 1 ,在unix_gc
后面的代码中就不会被释放file
的内存,而只是把f_count
减 1,这也意味着在close()
之后依然可以dup()
成功
1 | dup() -> __fget_files() |
unix_gc() 中 file 和 skb 没有同步释放可能造成的影响?
下面这个方式可以触发 skb UAF:
1 | socketpair() // 获取 socket pair fds: 3, 4 |
skb uaf:
- allocated in:
sendmsg() -> unix_stream_sendmsg()
- freed in:
close() -> unix_gc()
- uafed in:
recvmsg() -> unix_stream_read_generic()
Part.2
SCM_RIGHTS unix socket
SCM_RIGHTS
is a socket control message used for passing file descriptors between processes over a UNIX domain socket.It allows a process to send an open file descriptor to another process, which can then use the file descriptor to read or write to the same file or device.
example
sender.c
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
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <file_path>\n", argv[0]);
return 1;
}
char *file_path = argv[1];
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/file_transfer.sock", sizeof(addr.sun_path) - 1);
if (connect(sock, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
perror("connect");
return 1;
}
int fd = open(file_path, O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
struct msghdr msg = {0};
char buf[CMSG_SPACE(sizeof(fd))];
memset(buf, 0, sizeof(buf));
struct iovec io = { .iov_base = "hello", .iov_len = 5 };
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
*((int *) CMSG_DATA(cmsg)) = fd;
if (sendmsg(sock, &msg, 0) == -1) {
perror("sendmsg");
return 1;
}
close(fd);
close(sock);
return 0;
}recver.c
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
int main(int argc, char *argv[]) {
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock == -1) {
perror("socket");
return 1;
}
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/file_transfer.sock", sizeof(addr.sun_path) - 1);
if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
perror("bind");
return 1;
}
if (listen(sock, 1) == -1) {
perror("listen");
return 1;
}
int client_sock = accept(sock, NULL, NULL);
if (client_sock == -1) {
perror("accept");
return 1;
}
char buf[256];
struct iovec io = { .iov_base = buf, .iov_len = sizeof(buf) };
struct msghdr msg = {
.msg_iov = &io,
.msg_iovlen = 1
};
char control[CMSG_SPACE(sizeof(int))];
msg.msg_control = control;
msg.msg_controllen = sizeof(control);
if (recvmsg(client_sock, &msg, 0) == -1) {
perror("recvmsg");
return 1;
}
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (cmsg == NULL || cmsg->cmsg_type != SCM_RIGHTS) {
printf("Invalid message\n");
return 1;
}
int fd = *((int *) CMSG_DATA(cmsg));
if (fd == -1) {
perror("No file descriptor received");
return 1;
}
// Do something with the received file descriptor
char buf2[256];
ssize_t bytes_read;
while ((bytes_read = read(fd, buf2, sizeof(buf2))) > 0) {
printf("%s", buf2);
}
close(fd);
close(client_sock);
close(sock);
return 0;
}
Unix socket sendmsg()
and recvmsg()
- 用于发送和接收
SCM_RIGHTS
unix socket 数据的主要处理函数是:unix_stream_sendmsg
和unix_stream_read_generic
- 特殊的地方在于:
sendmsg
的时候会创建skb
并放在全局列表gc_inflight_list
和接收端的sk_receive_queue
上- 发送的
fd
对应的file
会绑定到skb
上(f_count
也会加 1) recvmsg
的时候从sk_receive_queue
取skb
unix_gc
则从gc_inflight_list
取skb
1 | // net/socket.c |
1 | recvmsg() -> __sys_recvmsg() -> ... |
**struct sk_buff *skb
, struct unix_sock *u
, struct socket *sock
, struct sock *sk
和 struct file *file
之间的关系?**
1 | struct socket *sock = &container_of(file->f_inode, |
unix_gc()
做了什么?
- 遍历
gc_inflight_list
获取unix_sock
对象- 把满足条件的
unix_sock
添加到gc_candidates
- 条件:
unix_sock
的文件引用和skb
引用值相同
- 把满足条件的
- 遍历
gc_candidates
- 把满足条件的
skb
添加到hitlist
- 把满足条件的
- 释放
hitlist
上的skb
内存和与之绑定的struc file
1 | unix_gc() |
unix_gc() 中 file 和 skb 在哪里 free ?
1 | unix_gc() |
unix_gc()
何时被调用?
close()
可以间接触发- 具体入口的
syscall_exit_to_user_mode() - __fput()
- 具体入口的
sendmsg()
也可以触发但只在队列满的时候sendmsg() - wait_for_unix_gc()
1 | // close() 一个 f_count 为 1 的文件时触发 |
1 | // 只有 inflight sockets 超过 UNIX_INFLIGHT_TRIGGER_GC(16000) 才会调用 |
dup() 的作用和实现原理?
- 根据 fd 从 fd table 中获取
struct file *file
- 如果
f_count
不为 0 则file->f_count += 1
- fd table 中新建一个条目指向 file
1 | SYSCALL_DEFINE1(dup, unsigned int, fildes) |
close()
的作用和实现原理?
- 使 fd 重新可用
- 把 fd table 中 fd 对应的条目删除(设置为 NULL)
- fd table 中原来指向的
struct file
的f_count
减 1,如果减到 0 则释放 struct file 的内存 close
不一定会立马释放struct file
, 但是用户态不能再访问该fd
,比如dup(fd)
,read(fd)
..
1 | close() |
增加 kernel delay patch 的 poc 如何 work ?
- line-27 将 pair[0] f_count +1 并添加到
gc_inflight_list
和sk_receive_queue
- line-29 和 line-43 用于触发
unix_gc()
调用, 因为需要一个f_count
为 1 的fd
被close()
- line-36 用于等待
resurrect_fn()->dup()->__fget_files()
调用进入 race window 拿到struct file
, 因为 line-37 会把pair[0]
从 fd table 中移除。 usleep 的时间 100000 us 要小于 kernel patch 的 500ms - line-43 会在
__fget_files()
等待的期间执行unix_gc()
, 在执行到准备释放 skb 的代码时,会等待 line-11 的 dup() 完成。 dup()
完成后执行到 line-16 的recvmsg()
,内核会等待 line-43 触发的unix_gc()
完成 skb 的释放unix_gc()
完成后,recvmsg()
继续执行拿到被释放的 skb,UAF
省略版 POC:
1 | 1 void send_fd(int sock, int fd) { |
kernel patch 增加三个 mdelay
1 | @@ -850,6 +852,13 @@ static struct file *__fget_files(struct files_struct *files, unsigned int fd, |
fixed patch 如何 work ?
- 补丁效果:在 race window 期间,如果 fd 对应的
struct file
已经从 fd table 移除,则回退对f_count
的操作,如果发现回退后变为 0 则直接释放struct file
1 | diff --git a/fs/file.c b/fs/file.c |
Part.3
如何利用 hrtimer 扩大 race 成功率?
timerfd_create
+timerfd_settime
可以在指定时间(纳秒)后触发 timer interrupt- timer interrupt handler 会调用
__wake_up_common
遍历 wait queue 并执行回调函数。这意味着 wait queue 越长,处在 interrupt context 的时间越长 - 利用这一点可以让进程在 race window 中被中断,然后在另一个 CPU 上运行需要与之 race 的进程
wait queue item 在哪里添加和读取 ?
- 每一个
EPOLL_CTL_ADD
会在 timer_fd 的 wait queue 上添加一个执行ep_poll_callback
的 entry - 在
timerfd_triggered
中 从 timer_fd 的 wait queue 中取出 entry
1 | // epoll_ctl(epoll_fds[i], EPOLL_CTL_ADD, timer_fds[j] |
1 | local_apic_timer_interrupt() |
**timerfd_tmrproc
在 timerfd_setup
中设置**
1 | static int timerfd_setup(struct timerfd_ctx *ctx, int flags, |
**struct timerfd_ctx
, struct file
, struct hrtimer
之间的关系**
1 | struct timerfd_ctx *ctx = file->private_data; |
测试代码:
向 wait queue 中添加 500 * 500 个 entry
1 |
|
如何观测延迟效果?
在 GDB 中可以查看队列中的 entry,数量与设置的一致
1 | b timerfd_triggered |
加一点 patch 用 rdtsc
可以粗略测量一下延迟效果
1 | **0xffffffff81b8b67e <+49>: rdtsc** |
1 | diff --git a/fs/timerfd.c b/fs/timerfd.c |
系统正常运行的时候 tick 数大概在 3000 ~ 30000, 创建 500 * 500 个 entry 可以使cpu 运行时间增大 3~4 个数量级(测试虚拟机的CPU是单核 2000 MHz)
1 | [ 1134.053250] [swapper/0] timerfd_triggered exit, 2976 |
一种观测代码被中断位置的方法
原文的附录:
I tried firing an interval timer at 100Hz (using timer_create()), with a signal handler that logs the PC register
代码实现:
1 |
|