引用资料
https://blog.lzip.net/post/the_socket_syscall_of_linux_kernel
其他知识
socket系统调用
1 |
|
socket定义
socket()调用sock_create()创建内核socket对象, 调用sock_map_fd()将socket对象映射成fd
1 | // net/socket.c |
创建socket
通过sock_alloc()分配socket结构体,然后通过传入的family获取协议模块,在通过协议模块的ops(pf->create),来初始化socket
1 | // net/socket.c |
初始化协议族
1 | // net/socket.c |
注册协议族
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/net/socket.c#L3110 |
packet_create
1 | static int packet_create(struct net *net, struct socket *sock, int protocol, |
将socket映射为文件
分配一个fd,创建socket对应的struct file对象,建立fd与struct file对象的关系
1 | // net/socket.c |
创建file对象
在sock_alloc_file函数中,会调用alloc_file_pseudo来在伪文件系统(proc伪文件系统)中分配一个伪文件,设置的文件操作函数集是socket_file_ops,以便VFS层调用
1 | // net/socket.c |
漏洞分析
setsockopt系统调用
配置套接字参数的系统调用,允许开发者调整套接字的行为特性
1 |
|
setsockopt
1 | // net/socket.c |
__sys_setsockopt
根据fd找到关联的socket,两种模式来进行setsockopt,走“通用 socket 层”或]还是“具体协议实现”
1 | // net/socket.c |
packet_setsockopt
AF_PACKET 套接字对 setsockopt() 的协议私有处理函数
1 | // net/packet/af_packet.c |
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 | // net/packet/af_packet.c |
init_prb_bdqc
TPACKET_V3 的 RX ring 控制核心初始化函数
1 | // net/packet/af_packet.c |
rx_power_map和pkbdq
首先进行释放pg_vec的操作,packet_ring_buffer.prb_bdqc.pkbdq仍然持有被释放pg_vec,然后将packet socket的版本切换为TPACKET_V2并且再次设置缓冲区的时候,原本保存在pkbdq的pg_vec会被当做rx_owner_map再次释放,造成double free,因为在rx_owner_map和prb_bdqc为一个联合体,偏移相同
1 | // net/packet/internal.h |
alloc_pg_vec
pg_vec的大小为我们可控(block_nr),因此我们可以申请得到几乎任意大小的堆块
1 | // net/packet/af_packet.c |
mmap系统调用
1 |
|
mmap
1 | // arch/x86/kernel/sys_x86_64.c |
ksys_mmap_pgoff
预处理
1 | // mm/mmap.c |
vm_mmap_pgoff
调用do_mmap()进一步完成映射
1 | // mm/util.c |
do_mmap
对映射方式的选择之后,调用核心���数mmap_region()进行映射
1 | // mm/mmap.c |
mmap_region
1 | // mm/mmap.c |
packet_mmap
packet_mmap会将这些内核虚拟地址代表的物理页映射到用户态
1 | static int packet_mmap(struct file *file, struct socket *sock, |
如果传入的page为内核代码的页,以上检查都可以绕过,就可以利用pg_vec修改内核代码段,这个时候传入__sys_setresuid的内核代码页的虚拟地址,然后对逻辑判断处改为jmp跳转就可以完成提权
1 | long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid) |
userfaultfd
userfaultfd是linux的一个系统调用(无libc封装函数),它的出现(Linux 4.3)给用户态提供了缺页处理的能力。userfaultfd系统调用返回给用户态进程的是一个用于处理page faults的文件描述符。
使用userfaultfd
通过userfaultfd系统调用创建userfaultfd object
1
2
3
4uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1) {
ErrExit("userfaultfd");
}用户态进程需要先通过UFFD_API这个ioctl命令,使能userfaultfd
1
2
3
4
5
6
7
8struct uffdio_api api = {
.api = UFFD_API,
.features = 0,
};
if (ioctl((int)uffd, UFFDIO_API, &api) == -1) {
ErrExit("ioctl-UFFDIO_API");
}用户态进程通过UFFDIO_REGISTER这个ioctl命令,注册设置内存地址范围
1
2
3
4
5
6
7
8
9
10
11
12struct uffdio_register reg = {
.range =
{
.start = (unsigned long)fault_page,
.len = len,
},
.mode = UFFDIO_REGISTER_MODE_MISSING,
};
if (ioctl((int)uffd, UFFDIO_REGISTER, ®) == -1) {
ErrExit("ioctl-UFFDIO_REGISTER");
}最后用户态进程可使用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
41if (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, ©) == -1) {
ErrExit("ioctl-UFFDIO_COPY");
}
return NULL;
}

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 | if (!sysctl_unprivileged_userfaultfd && !capable(CAP_SYS_PTRACE)) |
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 | if (!sysctl_unprivileged_userfaultfd && |
查看是否开UFFD_USER_MODE_ONLY
1 | zgrep CONFIG_USERFAULTFD /proc/config.gz |
看当前是否允许非特权进程使用
1 | sysctl vm.unprivileged_userfaultfd |
可以通过编译下面这段完整示例,确认能否使用userfaultfd。该程序会创建一个 pipe,再让工作线程执行 write(),触发内核态 copy_from_user 访问注册页。如果出现[+] in usefaultfd handler, i will sleep 60s\n字符串打印,表明可以使用这套 userfaultfd 示例。
1 | // gcc test.c -lpthread -o test |
setxattr(任意大小 object 分配(GFP_KERNEL)& 释放)
通过setxattr这一系统调用,我们可以在内核空间分配任意大小的object
1 |
|
setxattr定义
1 | // fs/xattr.c |
path_setxattr
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/fs/xattr.c#L617 |
setxattr
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/fs/xattr.c#L593 |
setxattr_copy
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/fs/xattr.c#L545 |
vmemdup_user
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/mm/util.c#L196 |
这里的 value 和 len 都是由我们来指定的,即我们可以分配任意大小的 object 并向其中写入内容,之后该对象会被释放掉
setxattr 与 userfaultfd 堆占位
虽然我们通过 setxattr 系统调用可以在内核空间中分配任意大小的 object 并写入任意内容,但是该 object 在 setxattr 执行结束时又会被放回 freelist 中
重新考虑 setxattr 的执行流程,其中会调用 copy_from_user 从用户空间拷贝数据,那么让我们考虑如下场景:
我们通过 mmap 分配连续的两个页面,在第二个页面上启用 userfaultfd 监视,并在第一个页面的末尾写入我们想要的数据,此时我们调用 setxattr 进行跨页面的拷贝,当 copy_from_user 拷贝到第二个页面时便会触发 userfaultfd,从而让 setxattr 的执行流程卡在此处,这样这个 object 就不会被释放掉,而是可以继续参与我们接下来的利用

内核密钥管理
自 Linux 2.6 起内核引入了 密钥保留服务(key retention service),用以在内核空间存储密钥以供其他服务使用,并提供了用以在用户空间操作密钥的三个新的系统调用
这里只关注可供利用的部分
关注 type 为
"user"的密钥
add_key - 创建带描述密钥(GFP_KERNEL | __GFP_HARDWALL | __GFP_NOWARN)
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 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L74 |
strndup_user
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/mm/util.c#L220 |
memdup_user
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/mm/util.c#L170 |
key_create_or_update
在密钥环中创建新 key,或更新已存在的同名 key
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/key.c#L816 |
description(key_alloc)
完成代表单个密钥的 key 结构体与 description 空间的分配,其中 key 结构体来自独立的 key_jar而 description 的空间则使用 kmemdup() 进行分配,该函数等于 kmalloc_track_caller() + memcpy(),可以直接理解为使用 kmalloc(size, GFP_KERNEL) 分配了一个内核对象并写入了一个字符串 description
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/key.c#L225 |
payload
其间会调用 index_key.type->preparse() 函数指针
index_key.type 根据我们传进来的 type 参数决定,对于 "user" 而言该函数表应当为 key_type_user
1 | key_ref_t key_create_or_update(key_ref_t keyring_ref, |
因此被调用的函数为 user_preparse,会为我们的 payload 再分配一个带有一个 user_key_payload 结构体作为头部的对象来保存我们传入的 payload,分配 flag 为 GFP_KERNEL
user_preparse
1 | int user_preparse(struct key_preparsed_payload *prep) |
最后存在如下调用链将 user_key_payload 存储到 key 中
1 | sys_add_key() |
keyctl - 密钥管理
keyctl() 系统调用提供了对内核中密钥的管理
1 |
|
keyctl定义
通过option来选择不同的操作
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L1869 |
KEYCTL_REVOKE
释放 payload
该选项对应调用的是 keyctl_revoke_key(),其中会调用到 key_revoke(),其中会调用 key->type->revoke(key),对于 type 为 "user" 的 key 而言最后调用到 user_revoke()
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L128 |
KEYCTL_UPDATE
更新 payload 内容
该选项会调用到 keyctl_update_key(),首先会分配一个临时对象从用户空间拷贝数据,之后调用 key_update() 更新 payload,最后释放掉临时对象
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L325 |
在 key_update() 中会调用 key->type->preparse(&prep) 分配新 payload 空间并进行数据拷贝,之后调用 key->type->update(key, &prep) 更新 payload 并释放旧的 payload,最后调用 key->type->free_preparse(&prep)
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/key.c#L1003 |
对于 type 为 "user" 而言 preparse 指针应当为 user_preparse,其会为我们的 payload 再分配一个带有一个 user_key_payload 结构体作为头部的对象来保存我们传入��� payload,分配 flag 为 GFP_KERNEL
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L59 |
对于 type 为 "user" 而言 update 指针应当为 user_update,主要就是将新的 payload 给到 key,调用 user_free_payload_rcu() 释放旧的 payload
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L101 |
对于 type 为 "user" 而言 free_preparse 指针应当为 user_free_preparse,这里只是一个简单的释放操作,但传入的参数 prep->payload.data[0] 在 user_update 中已经被设为 NULL,所以这一步并没有实际作用
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/user_defined.c#L83 |
KEYCTL_READ
读取 payload 内容
该选项对应调用的是 keyctl_read_key(),首先会先分配一个临时对象,之后调用 __keyctl_read_key() 将payload 拷贝到临时对象上,最后从临时对象上拷贝数据到用户空间后释放该临时对象
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyctl.c#L825 |
对于 type 为 "user" 而言应当为 user_read()
1 | long user_read(const struct key *key, char *buffer, size_t buflen) |
如果我们能够用某种方式更改 payload 头部的 datalen 为一个更大值,便能完成内核空间中的越界读取,同时由于其使用先分配一个 buflen/datalen 长度的临时对象进行数据拷贝后再将临时对象上数据拷贝到用户空间的方式,因此不会触发 hardened usercopy 的检查
KEYCTL_UNLINK
释放整个 key
该选项对应调用的是 keyctl_keyring_unlink() ,其最后会调用到 key_unlink() 进行资源的释放
1 | // https://elixir.bootlin.com/linux/v5.19.17/source/security/keys/keyring.c#L1545 |
密钥调用板子
1 |
|
创建子命名空间
因为普通用户无法创建原始套接字(RAW_SOCKET),所以我们可以创建子命名空间来绕过,并最终在父进程中进行提权
使用如下函数:
1 | void unshare_setup(void) { |
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 |
|
定义好如上内容后,使用如下方式即可简单地获得一个指定大小的,由pg_vec数组组成的obj:
1 | int packet_fd = alloc_pgv(count, 0x1000); |
count 是指pg_vec中含有的struct pgv的数量。而struct pgv的定义如下:
1 | struct pgv { |
因此,若我们要申请一个大小为0x130的obj,即可以使用如下方式:
1 | int packet_fd = alloc_pgv(0x130 / 8, 0x1000); |
分配pg_vec之前还要先分配一个0x20的obj
篡改虚拟内存数组的内容
我们可以利用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_map和prb_bdqc为一个联合体,并且偏移一致,导致残留的pg_vec会变成rx_owner_map指针,导致最后释放操作的时候,造成doble free
N1CTF-2022-praymoon
漏洞分析
linux5.18.10,开启KASLR、SMEP、SMAP、KPTI
run.sh
1 | #!/bin/sh |
内核版本
1 | / $ uname -a |
KPTI
1 | / $ cat /sys/devices/system/cpu/vulnerabilities/* |
init
1 |
|
编译选项
1 | CONFIG_SLAB_FREELIST_RANDOM=y // SLAB/SLUB 空闲链表随机化 |
ida
只有一个ioctl接口,可以一次kmalloc(0x200),两次kfree,这里有double free

目前已知的非固定大小结构体有msg_msg和user_key_payload
msg_msg的申请 带了GFP_KERNEL_ACCOUNT标志(会独立存在),而驱动在申请内存时没有使用该标志,所以它们的堆是隔离开的,无法构成利用
user_key_payload中有datalen长度信息,利用double free(UAF)漏洞结合userfaultfd+setxattr可以改掉datalen,于是通过keyctl操作可以越界读取堆上的内容,泄露内核地址
漏洞利用:
- 创建子进程,子进程中创建命名空间
- kmalloc
- kfree
- user_key_payload占住堆块
- kfree
- setxattr kmalloc:改写user_key_payload的datalen,结合userfaultfd,延迟3s释放
- keyctl_read: 越界读取堆上的内容,泄露内核基址(从data开始读起)
- keyctl revoke:释放user_key_payload占用的堆块
- packet socket:alloc_pg_vec()中kcalloc时占住堆块
- setxattr kfree:延迟3s后的释放
- setxattr malloc:申请的不一定是目标堆块,结合userfaultfd多做几次申请,提高成功率
- 通过mmap映射到用户态,改写内核代码段逻辑(如
_sys_setresuid),使普通用户可以通过调用setresuid(0, 0, 0);getshell
exp:
1 |
|
// EXTERNAL_CHANNEL
COMMENTS