linux keyring原理,Linux keyring提权漏洞分析(CVE
8种机械键盘轴体对比本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?0x0 序本文深入分析今年初公开的一个Linux提权漏洞,分析该漏洞之前先介绍下Linux密钥保留服务,该服务主要用于在内核中缓存身份验证数据,使内核可以快速访问所需的密钥,还可以将密钥的操作(keyctl, add_key, request_key)委托给用户态进程,同时该服务定义了两个标准密钥类型user和keyri.

8种机械键盘轴体对比
本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?
0x0 序
本文深入分析今年初公开的一个Linux提权漏洞,分析该漏洞之前先介绍下Linux密钥保留服务,该服务主要用于在内核中缓存身份验证数据,使内核可以快速访问所需的密钥,还可以将密钥的操作(keyctl, add_key, request_key)委托给用户态进程,同时该服务定义了两个标准密钥类型user和keyring。每个进程都可以为当前会话创建或替换一个匿名或指定名称的keyring,keyring对象有一个usage字段保留进程对该对象的引用次数,漏洞发生在替换相同名称keyring时,usage引用计数泄露。
0x1 漏洞原因
先来看看内核中的漏洞函数:
long join_session_keyring(const char *name)
{
const struct cred *old;
struct cred *new;
struct key *keyring;
long ret, serial;
//从当前task拷贝一份cred对象
new = prepare_creds();
//other code
//...
//find_keyring_by_name查找到和name匹配的keyring对象后
//内部调用atomic_inc_not_zero(&keyring->usage),增加引用计数
keyring = find_keyring_by_name(name, false);
if (PTR_ERR(keyring) == -ENOKEY) {
keyring = keyring_alloc(
name, old->uid, old->gid, old,
KEY_POS_ALL | KEY_USR_VIEW | KEY_USR_READ | KEY_USR_LINK,
KEY_ALLOC_IN_QUOTA, NULL);
if (IS_ERR(keyring)) {
ret = PTR_ERR(keyring);
goto error2;
}
} else if (IS_ERR(keyring)) {
ret = PTR_ERR(keyring);
goto error2;
} else if (keyring == new->session_keyring) {
ret = 0;
goto error2;//BUG在这里,直接跳到error2,没有调用key_put()减去引用计数
}
ret = install_session_keyring_to_cred(new, keyring);
if (ret < 0) goto error2; commit_creds(new); mutex_unlock(&key_session_mutex); ret = keyring->serial;
key_put(keyring); //减去引用计数
okay:
return ret;
error2:
mutex_unlock(&key_session_mutex);
error:
abort_creds(new);
return ret;
}
keyring->usage字段类型为atomic_t,实际上是一个int:
typedef struct {
int counter;
} atomic_t;
我们都知道int无论32位还是64位上都是4字节,那么在调用join_session_keyring 2^32次后,usage会上溢为0,从下面汇编片段也可以看到:
//atomic_inc_not_zero
gdb-peda$ disassemble $rip-0x10, $rip+30
Dump of assembler code from 0xffffffff812e790d to 0xffffffff812e793b:
0xffffffff812e790d : add BYTE PTR [rbp+0x455e75c0],al
0xffffffff812e7913 : test ch,ch
0xffffffff812e7915 : je 0xffffffff812e7958
0xffffffff812e7917 : mov edx,DWORD PTR [rbx]
0xffffffff812e7919 : test edx,edx
0xffffffff812e791b : je 0xffffffff812e7970
=> 0xffffffff812e791d : lea ecx,[rdx+0x1] //使用32位寄存器保存refcount
0xffffffff812e7920 : mov eax,edx
0xffffffff812e7922 : cmpxchg DWORD PTR ds:[rbx],ecx
0xffffffff812e7926 : cmp edx,eax
0xffffffff812e7928 : mov ecx,eax
0xffffffff812e792a : jne 0xffffffff812e7998
0xffffffff812e792c : call 0xffffffff810dd160
0xffffffff812e7931 : mov QWORD PTR [rbx+0x60],rax
0xffffffff812e7935 : sub DWORD PTR ds:[rip+0xca4f20],0x100
用户态下,可以调用keyctl(KEYCTL_JOIN_SESSION_KEYRING, name)来调用内核的join_session_keyring函数,也就是说如果用户态调用2^32次keyctl的话,就可以使usage字段上溢为0,为漏洞利用埋下了伏笔。
0x2 漏洞利用
在内核上下文中,存在一个叫做key_garbage_collector的垃圾回收器,它的一个作用是遍历key serial tree,寻找usage字段为0的keyring,将该keyring添加到该函数的一个静态变量graveyard中,接着调用key_gc_unused_keys将keyring放回缓存:
/*
* Garbage collector for unused keys.
*/
static void key_garbage_collector(struct work_struct *work)
{
static LIST_HEAD(graveyard);
//other code
//...
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 (atomic_read(&key->usage) == 0)//寻找usage为0的keyring
goto found_unreferenced_key; //跳转到found_unreferenced_key
//other code
//...
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; //跳转到maybe_resched,进行释放
//other code
//...
maybe_resched:
//other code
//...
if (!list_empty(&graveyard)) {
kdebug("gc keys");
key_gc_unused_keys(&graveyard);//内部调用kmem_cache_free()放回slab缓存
}
return;
}
}
贴一段gdb中,usage上溢为0后,key_gc_unused_keys()释放key时的状态:

那么现在不就转化成了Use-After-Free了吗,从gdb和源码看struct key结构大小等于0xb8,如果现在申请一个大小0xb8的内核对象,并且内容可控的话,就有机会控制内核去执行用户态的一段shellcode了。

EXP中选择使用Linux IPC机制,发送消息msg来尝试占用原keyring对象的空间:
if ((msgid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1)
{
perror("[-] msgget");
exit(1);
}
for (i = 0; i < 256; i++)
{
if (msgsnd(msgid, &msgp, sizeof(msgp.mtext), 0) == -1)
{
perror("[-] msgsnd");
exit(1);
}
}
用户态的msgsnd()在内核中会调用do_msgsnd(),do_msgsnd()根据用户态传递的buffer和size参数调用load_msg(mtext, msgsz),load_msg()先调用alloc_msg(msgsz)创建一个msg_msg结构体,然后拷贝用户空间的buffer紧跟msg_msg结构体的后面,相当于给buffer添加了一个头部,因为msg_msg结构体大小等于0x30,因此用户态的buffer大小等于0xb8 - 0x30
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts;/* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
alen = min(len, DATALEN_MSG);
//从slab缓存申请内存,尝试占用keyring空间
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL);
//other code
//....
return msg;
}
返回的msg_msg结构指针占用了keyring对象的空间并且内容就是buffer,接下来研究怎样构造用户态buffer的内容,才可以控制内核执行shellcode。
keyring实际上是一个struct key结构体,该结构体有一个成员type,类型为struct key_type*,比较幸运的是这个struct key_type里面大部分成员都是函数指针。如果buffer里面构造一个type指针,使其指向用户空间一个伪造的key_type结构体,这个结构体有一个函数指针指向shellcode,那么现在再去调用keyring->type的某个函数,不就使内核调用shellcode了吗!
struct key {
atomic_tusage;/* number of references */
key_serial_tserial;/* key serial number */
//other member
//...
* the key type and key description string
* - the desc is used to match a key against search criteria
* - it should be a printable string
* - eg: for krb5 AFS, this might be "[email protected]"
*/
union {
struct keyring_index_key index_key;
struct {
struct key_type*type;
char*description;
};
};
}
struct key_type {
/* name of the type */
const char *name;
//EXP中覆盖了此函数
void (*revoke)(struct key *key);
void (*destroy)(struct key *key);
//other member
//...
}
//由用户态keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING)调用
//此函数key->type->revoke,最终转向shellcode
void key_revoke(struct key *key)
{
//...
if (!test_and_set_bit(KEY_FLAG_REVOKED, &key->flags) &&
key->type->revoke)
key->type->revoke(key);
//...
}
下面这张图可以形象的描述上面过程,手绘轻喷:

下图gdb中看到key->type已经被修改,revoke指针指向shellcode:

那么整个利用过程可分为以下几步:
调用2^32次join_session_keyring,使keyring->usage上溢为0
当usage为0,key_garbage_collector回收keyring对象空间,放入slab缓存
使用IPC发送message,申请的message大小和keyring对象一样大,尝试从slab取回原空间,并伪造key->type
调用keyring的revoke函数,触发内核执行shellcode
在EXP基础上加了个反弹shell,最终得到一个root shell:

最后想说的是,尽管编译内核时候关闭了SMAP并再boot时加了nosmep,但是内核的RCU机制着实让成功利用此漏洞很艰难。join_session_keyring函数一开始调用prepare_creds()同步使keyring->usage += 1,函数结束时调用abort_creds()产生一个RCU callback,callback中会使keyring->usage -=1,这时就会产生一个问题,因为key_garbage_collector对usage字段是只读操作,并不需要等待callback完成。那么如果这时候usage并不为0,就不会放回slab缓存内,以至于后面IPC申请message不会得到keyring的地址。EXP中尝试使用sleep()来等待RCU完成,但是效果不太稳定。重要的是,漏洞本身是可以被利用的,从这个漏洞加强了Linux内核调试的技巧。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐



所有评论(0)