2024-05-10 14:28:3314628人阅读
作者:百度安全-AIoT安全团队 Chao Ma, Han Yan, Tim Xia
随着安卓系统的流行,Netlink作为Linux内核与用户态进程之间的一种通信机制,被广泛应用在安卓操作系统内核模块中,但其使用的安全性却未得到足够的重视。针对这一现状,百度安全研究员于北京时间4月19日下午,在BlackHat ASIA 2024上分享了《LinkDoor: A Hidden Attack Surface in the Android Netlink Kernel Modules》议题,探讨了Netlink内核机制的安全边界。议题首先介绍了Netlink的概念和编程模型,然后根据其在Linux中的内核实现机制,抽象出四类威胁模型,并分别分析了在现实世界中的典型漏洞案例,接着介绍了Netlink相关漏洞的验证和利用方法,最后为厂商安全使用Netlink提供了最前沿的安全建议。
BlackHat ASIA 2024议题:安卓Netlink内核模块中的一个隐藏攻击面
一、介绍 Netlink背景 Netlink是一个套接字家族,主要用于内核和用户进程之间的双向通信。与ioctl相比,它具有全双工、异步、组播的通信特点。
Netlink全双工和组播通信
在研究过程中,我们根据Netlink的使用方式将其分为Classic Netlink和Generic Netlink两大类。其中Generic Netlink的协议ID为16,其他ID都是Classic Netlink。 Classic Netlink编程模型 Classic Netlink套接字从1999年的Linux 2.2版本开始支持。因为它是一个套接字家族,因此其用户态编程使用的是Linux Socket API。因其具有全双工的特点,所以Netlink传输消息有Top-Down和Bottom-Up两个方向。 Top-Down方面,内核使用netlink_kernel_create函数注册input函数,该函数主要用于解析从用户空间接收到的消息。 Bottom-Up方面,内核使用nlmsg_unicast或nlmsg_multicast函数向用户空间主动发送组建好的Netlink传输消息。
Classic Netlink全双工编程模型
Classic Netlink缺陷 Classic Netlink主要有两个小瑕疵: 有限的Netlink协议ID:netlink_kernel_create函数的第二个参数unit为协议ID。如果想要自定义使用Classic Netlink,需要增加1个协议ID。但是在Linux内核中,unit一共只有32个协议ID,内核占用了22个,只剩下10个给用户使用。当然也可以修改内核源码扩大该协议ID的最大值,但还有更好的方法。 编程复杂:Classic Netlink由于需要用户自己手动解析传输消息,因此使用起来更加复杂。 为此,Linux内核引入了Generic Netlink。
Generic Netlink编程模型 Generic Netlink套接字从2006年的Linux 2.6.15版本开始支持。所有自定义使用Generic Netlink的用户共享1个协议ID 16。当解析用户空间输入的消息时,只需要关注用户属性即可。 Top-Down方面,内核使用genl_register_family函数注册ops和small_ops,二者的区别在于small_ops提供了更少的消息处理函数,相同点在于都会指定doit函数。doit函数用于解析从用户空间输入的属性。 Bottom-Up方面,内核使用genlmsg_unicast、genlmsg_multicast、genlmsg_multicast_netns或者genlmsg_multicast_allns函数向用户空间主动发送组建好的Netlink属性。 Generic Netlink全双工编程模型 Netlink架构 Netlink架构方面,应用可以直接调用或者间接通过libnl调用Linux Socket API,将Netlink消息发送到内核空间。内核接收到Netlink消息后,会将其路由给Linux内核Netlink子系统,该子系统会根据Netlink类别分为Classic Netlink和Generic Netlink进行处理。下面我们将重点关注Netlink子系统对Netlink消息的处理机制。
Netlink架构 Classic Netlink内核机制 Classic Netlink传输消息可由多组Netlink消息组成。每组Netlink消息都包含了一个Netlink消息头,用户负载和它们的填充。Netlink消息头和用户负载需要4字节对齐。nlmsghdr结构体由五个字段组成:消息长度、消息类型、消息标志、消息序号和消息pid。其中消息长度是每组Netlink消息的总长度,消息pid一般设置为进程ID。
Classic Netlink传输消息格式 Classic Netlink传输消息通过sendto或者sendmsg函数从用户空间发送到内核空间。skb包含了该传输消息,它是input函数的唯一参数,可以在input函数中通过skb->data获取该传输消息。问题是Linux内核Netlink子系统会如何检查该传输消息?答案是什么都不检查。这意味着开发者需要自己检查该传输消息头,而将检查交给开发者本身就是一个危险的行为。
Classic Netlink传输消息解析 Classic Netlink威胁模型 为此,Classic Netlink Top-Down消息解析方面我们总结了三种可能的攻击点: 开发者没有理解skb->len、nlh->nlmsg_len和NLMSG_HDRLEN三者之间的关系。或者开发者根本不做任何检查。skb->len是Netlink传输消息的总长度,由多组Netlink消息组成。nlh->nlmsg_len是一组Netlink消息的总长度,而NLMSG_HDRLEN则是一组Netlink消息头的长度,是一个固定值。一个更简单的检查是通过NLMSG_OK来进行检查。 开发者没有检查用户负载的长度就直接解析负载。 开发者没有充分检查用户负载内容的有效性
Classic Netlink Top-Down传输消息解析 Bottom-Up消息组建方面,则需要结合内核其他攻击面,如file operations、socket等。可以通过检查nlmsg_unicast或nlmsg_multicast函数,逆推出输入源,然后进行漏洞挖掘。
Classic Netlink Bottom-Up传输消息组建 Generic Netlink内核机制 Generic Netlink是基于Classic Netlink创建,从其大体的消息结构可以看出。不同点在于负载的组成,包含了一个Generic Netlink消息头、family头、属性和它们的填充。每个属性又包含了1个属性头、属性负载和它们的填充。
Generic Netlink传输消息格式 family头是可选的、用户自定义的,因此我们聚焦在Generic Netlink消息头和属性头。genlmsghdr结构体由命令、版本和保留三个字段组成,其中命令字段用于指导内核调用具体的doit函数。nlattr结构体由Netlink属性长度和类型两个字段组成,与属性负载一起形成了TLV结构。Netlink属性长度是属性的总长度,包含了属性头;Netlink属性类型一般是属性数组的索引。 Generic Netlink也是通过sendto或者senmsg函数将传输消息从用户空间发送到内核空间。info参数中包含了该传输属性,它是doit函数的一个参数,doit函数由genl_register_family注册,用于解析该传输属性。可以通过info->attrs[nla_type]获取该传输属性。相同的问题是Linux内核Netlink子系统会如何检查该传输属性?答案是通过nla_policy检查。
Generic Netlink传输属性解析
nla_policy即Netlink属性策略是在genl_family或genl_ops结构体中注册,genl_ops结构体中的策略优先于genl_family中的策略。nla_policy结构体包含了属性类型、属性长度、联合体等字段。这里的属性类型指的是属性负载的数据类型。属性长度根据属性类型有不同的含义。如属性类型是NLA_STRING,则属性长度是属性负载的最大值。联合体基于有效性类型来检查有效性。所有的属性检查都是在validate_nla函数中进行。 Generic Netlink威胁模型 Generic Netlink比Classic Netlink做了更多的检查,难道就不存在安全问题吗?当然不是。我们总结了三个开发者可能会忽视的攻击点: 开发者在属性策略注册阶段没有设置或者设置了一个错误的属性策略 开发者在属性解析阶段没有检查属性的有效性 开发者在属性解析阶段没有充分检查属性内容的有效性
Generic Netlink Top-Down传输属性解析
Bottom-Up属性组建方面,则需要结合内核其他攻击面,如file operations、socket等。可以通过检查genlmsg_unicast、genlmsg_multicast、genlmsg_multicast_netns或者genlmsg_multicast_allns函数,逆推出输入源,然后进行漏洞挖掘。
Generic Netlink Bottom-Up传输属性组建
漏洞统计 基于上述攻击面分析,我们调研了4个知名厂商与Netlink相关的内核模块,发现了38个漏洞,截止目前已经分配了19个CVE,所有的漏洞都已经被厂商修复。其中Classic Netlink类型的漏洞比Generic Netlink类型的漏洞数量更多,而且漏洞严重程度也更高。也就是说用户在使用Netlink时,更推荐使用Generic Netlink。
Netlink漏洞列表
Netlink漏洞分布 案例1:攻击Classic Netlink消息解析过程 漏洞CVE-2023-32880 (NETLINK_FGD OOB Read)的场景最常见也是最简单,Netlink传输消息通过sendto函数发送到内核空间,input函数会解析该消息。其中input函数的实现中存在两个越解读漏洞: 未检查Netlink消息头 未检查Netlink消息负载长度就开始解析
漏洞案例1数据流图
引出的思考是所有Netlink越解读漏洞都只是在接收缓冲区中吗?当然不是。这里有两个方法可以造成越解读到接收缓冲区外部: 通过setsockopt函数设置接收缓冲区大小为最小 精心构造Netlink消息,填充满接收缓冲区 案例2:攻击Classic Netlink消息组建过程 漏洞CVE-2024-20833 (NETLINK_FIPS_CRYPTO Use After Free)的场景则体现了Netlink全双工的优势,能够将部分不需要在内核执行的代码转移到用户空间去执行,以减少内核漏洞产生概率。该漏洞挖掘过程中,我们首先定位nlmsg_unicast函数,然后绘制出请求的整个生命周期,最终在input函数中发现UAF漏洞。该漏洞需要结合ioctl去触发,形成的根因是未保护的全局变量。
漏洞案例2数据流图 案例3:攻击Generic Netlink消息解析过程 漏洞CVE-2024-26811 (Linux Kernel ksmbd smb2_read_pipe OOB Read)的场景与案例2相似。但其漏洞是发生在Generic Netlink的doit函数,该函数没有检查属性负载内容的合法性,最终导致了越解读。该漏洞需要结合TCP去触发。
漏洞案例3数据流图 案例4:攻击Generic Netlink消息组建过程 漏洞CVE-2023-52103 (Driver flp OOB Read)的场景是单纯的Generic Netlink Bottom-Up场景。通过genlmsg_unicast可以逆推出整个消息的生命周期,进而漏洞挖掘出flp_write函数中存在越界读,读的数据是通过genlmsg_unicast发送到用户空间。该漏洞需要与write去触发,形成的根因是不严格的消息内容合法性检查。
漏洞案例4数据流图 Classic Netlink验证 如果想要使用Netlink触发竞争条件,经常会遇到源端口占用问题。此时可以通过在多进程中使用进程ID作为源端口、或者在多线程中尝试可用的端口两种简单方式解决。 Generic Netlink验证 如果想要自定义使用Generic Netlink,在用户态首先要解决的问题就是将Family Name转化为Family ID,可以发送固定消息进行获取。
通过Family Name获取Family ID的消息格式 Netlink漏洞利用 漏洞利用方面,采用了CVE-2023-32878 (Arbitrary Read)和CVE-2023-32882 (Write-What-Where)两个漏洞获取了Root权限。值得注意的是Netlink漏洞可以通过在安卓中设置netlink_socket的selinux规则或者在Linux中调用netlink_capable函数检查CAP_NET_ADMIN权限进行缓解。 总结 Netlink是一个深埋在安卓系统中的隐藏攻击面 当自定义Classic Netlink时,内核并不会检查Netlink消息的合法性 当自定义Generic Netlink时,内核会根据Netlink属性策略检查Netlink属性的合法性 Generic Netlink检查比Classic Netlink更多,但也会带来一些其他安全威胁 安全建议 自定义使用Netlink时,尽量使用Generic Netlink替代Classic Netlink 在使用Netlink前,先理解Netlink内核机制和API