Linux内核RDS堆溢出漏洞CVE-2018-5333原理与加固 📅 发布时间:2026/9/17 3:55:21 👁 浏览次数: 前阵子帮客户做内核安全巡检扫描器又把这个老面孔翻了出来CVE-2018-5333。这个编号很多人见过却搞不清楚它到底是 Linux 内核 RDS 子系统的哪一环出了岔子更不知道自己的机器该不该紧张。今天不绕弯子直接把这个堆溢出漏洞的原理、触发路径、验证方式和加固手段一次讲明白。这块内容适合三类人需要维护数据库集群或高性能计算环境的运维、正在做内核漏洞研究的同学以及那些被安全扫描报告追着改系统的管理员。1. 漏洞画像RDS 协议、堆溢出与影响范围1.1 RDS 协议在内核里的位置RDSReliable Datagram Sockets不是大家日常接触的 TCP/UDP它最早是 Oracle 为 RAC 集群设计的一套可靠数据报协议后来被合入 Linux 内核。RDS 位于net/rds/目录配合rds_tcp、rds_rdma等传输模块工作目的是在高性能计算、数据库集群和存储网络里提供比 TCP 更低延迟、更少 CPU 开销的通信方式。RDS 的一大特色是支持 RDMA 操作。传统网络收发要经过内核协议栈数据在用户态和内核态之间反复拷贝而 RDS 可以让用户程序直接描述一块内存内核负责把它注册成 RDMA 内存区由网卡硬件直接把数据写进目标内存。这样一来数据路径短了延迟降了但内核需要处理一堆用户态传过来的复杂控制信息。CVE-2018-5333 的问题恰恰就出在这些控制信息的解析上。1.2 堆溢出是什么为什么杀伤力大堆溢出指的是程序往一块动态分配的内存区域里写入数据时写入长度超过了这块区域的边界导致相邻内存对象被破坏。内核里大部分动态内存来自 SLUB/SLAB 分配器分配出来的对象在物理上互相挨着。超出的数据一旦覆盖了相邻对象的元数据、指针或者引用计数轻则导致内核崩溃重则可以变成任意写原语最终拿到 root 权限。相比栈溢出堆溢出的利用门槛通常更高因为对象布局不稳定还要绕过 SMAP/SMEP、KASLR 等缓解机制。但它的危害等级并不低CVE-2018-5333 在不少发行版的评分系统里被标为 High。如果攻击者能稳定控制堆布局完全有可能从普通用户权限提升到 root。退一步说即使不提权一个畸形的 RDS 控制消息也能让系统直接 panic造成拒绝服务。1.3 哪些场景受影响这个漏洞影响的是编译进内核或作为模块加载的 RDS 子系统。普通 Web 服务器、应用服务器通常不会使用 RDS默认也可能没有加载rds模块所以看起来影响面有限。但 Oracle RAC、部分分布式存储、高性能计算集群会用到 RDS这类机器的内核版本如果长期不更新扫描报告里经常能翻到 CVE-2018-5333。更需要注意的是容器场景。容器和宿主机共享同一个内核只要宿主机内核里启用了 RDS容器内的低权限进程理论上也可以创建 RDS socket 尝试触发漏洞。对这种共享内核的架构来说一个本地内核漏洞往往意味着容器逃逸风险不能因为它“名字冷门”就忽略。2. 触发路径与根因拆解2.1 从 socket 到控制消息攻击入口在哪RDS 的用户态接口很像普通网络协议程序员可以创建PF_RDS类型的 socket通过sendmsg或setsockopt传递数据。不同的是RDS 定义了一系列私有的控制消息选项例如RDS_CMSG_RDMA_ARGS、RDS_CMSG_RDMA_MAP、RDS_CMSG_ATOMIC_*等。这些选项用来告诉内核我要注册哪一块内存、对方地址是什么、要读写多少页。入口路径大致是这样的用户态程序调用socket(PF_RDS, SOCK_SEQPACKET, 0)创建 RDS socket。通过sendmsg在辅助消息里携带 RDS 控制命令或者通过setsockopt直接传一个参数结构体。内核进入net/rds/下的对应处理函数根据用户传入的地址、页数、偏移量来分配内存并执行后续 RDMA 操作。漏洞就藏在“根据用户参数计算要分配多少内存”和“后面实际写多少数据”这两个环节不一致的地方。从攻击者视角看这个入口非常友好不需要 sendmsg 构建复杂的数据报文只需要构造几个数字就能让内核在解析阶段出错。因此这类漏洞常被描述为“畸形输入导致的内核内存破坏”。2.2 根因对用户提供的长度字段过度信任我根据公开补丁和内核源码的演化分析过这类问题。CVE-2018-5333 的根因高度集中在 RDS 对 RDMA 参数中nr_pages这类字段的校验不足。以 RDMA 参数为例用户传入的结构体里通常包含一个表示页数量的字段内核拿到后会用这个字段去计算需要的页指针数组大小然后分配内存。如果攻击者把页数量设成一个非常大的值内核在计算大小时可能发生整数溢出结果分配出一个比预期小得多的缓冲区。接下来内核又会遍历用户提供的地址列表把每一页的地址写进缓冲区于是越写越多最终突破缓冲区边界。用一个生活例子来类比你去吃自助餐服务员看了你的食量记录按“正常人能吃的份量”给你上了一个小盘子但后厨出菜时却按“你以为自己能吃十人份”的顺序一直往同一个盘子里摞菜。盘子不可能装得下菜全撒到旁边人的位置上。内核堆溢出的道理一模一样分配内存时参考了一个错误计算出来的小尺寸写入数据时却参考了另一个没有收敛的大数量两者不匹配溢出就发生了。2.3 为什么是“堆”溢出而不是栈溢出很多同学会把内核漏洞自动归类为栈溢出实际上这两者需要完全不同的分析思路。CVE-2018-5333 属于堆溢出因为出问题的目标是kmalloc一类的动态分配内存而不是固定在栈上的局部数组。RDS 处理 RDMA 参数时需要把用户传过来的多个内存区域转换成内核内部的页描述符列表。这个列表的大小只能运行时确定所以必须动态分配。攻击者控制的nr_pages影响的是动态分配大小而后续的拷贝循环又可能跨越多个控制消息累积页数。只要两个值对不上越界写就会发生在堆对象上。堆溢出的利用价值在于攻击者可以通过合理分配很多相邻对象让溢出的数据恰好覆盖到自己希望篡改的结构体。例如伪造函数指针、修改引用计数、改变对象大小字段等。现代内核缓解机制会提高利用难度但并不能彻底阻断思路。这也是为什么内核安全团队坚持要把这类 bug 修复掉不能只依赖缓解机制兜底。3. 环境准备与验证实验3.1 准备一个可控的旧内核测试环境如果要亲眼看这个漏洞的表现千万不要在生产机上试。我的做法是找一台虚拟机跑一个受影响的旧内核。Ubuntu 16.04/18.04 的默认内核、以及很多发行版的内核包如果没打补丁都在受影响范围内。如果你手里没有旧系统镜像可以自己编译一个 4.14 或更早版本的内核。编译旧内核时注意把 RDS 相关选项打开cd linux-4.14 make olddefconfig scripts/config -e RDS -e RDS_TCP -e RDS_DEBUG make -j$(nproc) make modules_install install配置完成后启动新内核检查 RDS 是否已经加载lsmod | grep rds modprobe rds lsmod | grep rds如果modprobe rds成功/proc/modules里会看到rds模块。这时 RDS 控制消息处理代码已经在内核环境里可以做下一步验证。3.2 更安全的复现思路开启 KASAN为了稳定定位越界写入我强烈建议在测试内核里打开 KASAN。KASAN 通过编译时插桩检测内存越界读写会在越界发生的那一刻打印非常精确的调用栈而不是等到系统随机崩溃。开启 KASAN 的方法是编译内核时启用相关配置scripts/config -e KASAN -e KASAN_INLINE然后重新编译内核。KASAN 会带来明显性能开销所以只适合测试环境。启动 KASAN 内核后RDS 模块也必须是同一内核版本编译出来的否则插桩不生效。有了 KASAN验证思路就很简单让用户态程序创建 RDS socket然后向setsockopt传入构造好的畸形 RDMA 参数。如果漏洞路径存在dmesg 里会出现类似下面的信息BUG: KASAN: slab-out-of-bounds write in rds_cmsg_rdma_args0x...KASAN 会直接告诉你写入的地址、分配的缓冲区地址、以及越界发生在哪个函数。这样比观察系统崩溃要高效得多也方便你后续对比修复前后的行为差异。3.3 写一个探针程序确认路径可达我不建议在这里贴完整触发代码因为完整的畸形参数组合很容易被拿去打成拒绝服务攻击。但你可以写一个非常简单的探针程序确认 RDS socket 在当前内核里可用#include stdio.h #include string.h #include sys/socket.h #ifndef PF_RDS #define PF_RDS 21 #endif int main(void) { int fd; fd socket(PF_RDS, SOCK_SEQPACKET, 0); if (fd 0) { perror(socket(PF_RDS)); return 1; } printf(PF_RDS socket created successfully\n); close(fd); return 0; }如果这段代码能成功创建 socket说明 RDS 协议族已经注册到内核。接下来你可以配合strace观察setsockopt的返回值或者用bpftrace挂载 RDS 处理函数入口确认控制消息确实进入了net/rds的代码路径。这里的关键不是直接触发漏洞而是先确认目标环境与漏洞代码路径是否可达。3.4 如何分析内核崩溃日志如果你手头没有 KASAN直接跑畸形参数可能导致系统 panic。不用慌崩溃日志就是最好的证据。常见的日志特征包括BUG: unable to handle kernel paging request at ffff8800... RIP: 0010:[ffffffff...] rds_cmsg_rdma_args0x...或者general protection fault: 0000 [#1] SMP ... Call Trace: rds_cmsg_send rds_sendmsg sock_sendmsg如果你在 Call Trace 里看到rds_cmsg_send、rds_sendmsg基本可以确定流量确实进入了 RDS 控制消息处理路径。接下来的越界写入可能破坏堆对象导致后续任意时刻崩溃所以日志里不一定直接指向溢出点。这也是为什么我反复说KASAN 环境下的定位效率远高于裸环境。4. 漏洞修复、排查与加固4.1 怎么判断当前内核是否受影响一个常见的误区是只靠uname -r判断。主线内核在 4.15 附近修复了这个问题但各发行版会把补丁 backport 到自己的内核版本号后面。例如某些发行版的 4.14 内核可能已经包含了修复而某些 4.15 初期的内核反而没有完全修复。所以更可靠的方式是看发行版安全公告或者查看当前内核包变更日志。几个基础判断步骤uname -r cat /boot/config-$(uname -r) | grep RDS modinfo rds 2/dev/null | head -20如果CONFIG_RDS没有出现在内核配置里说明 RDS 功能没有编译漏洞面直接消失。如果rds模块存在但未加载风险也较低除非攻击者已经具备加载模块的权限。最值得担心的是rds模块已经加载并且内核版本在受影响区间内。4.2 修复方案与版本升级策略修复路径很明确把内核升级到包含补丁的版本或者安装发行版发布的安全更新包。升级后必须重启让新内核真正运行起来。如果 RDS 模块一直被加载重启之后还要确认模块已经切换到新内核版本编译的实例lsmod | grep rds cat /sys/module/rds/version对于不能立刻重启的生产环境可以先把 RDS 模块卸载掉。前提是没有应用依赖当前 RDS 连接。卸载命令rmmod rds如果卸载失败说明有 socket 还在使用 RDS。你需要先找到相关进程ss -x | grep rds ls -l /proc/[0-9]*/fd 2/dev/null | grep rds然后再停掉对应服务。4.3 临时规避与永久禁用对于绝大多数并不使用 RDS 的机器永久禁用 RDS 是最省心的处置。通过 modprobe 配置把模块访问拦截掉echo blacklist rds /etc/modprobe.d/blacklist-rds.conf echo install rds /bin/false /etc/modprobe.d/disable-rds.conf然后执行update-initramfs -u重启后再次检查lsmod | grep rds没有输出就说明 RDS 没有自动加载。如果再配合内核配置文件里把CONFIG_RDSn直接连模块都不编译漏洞面就从根本上消除了。需要注意一点install rds /bin/false会阻止任何进程通过modprobe rds加载模块但如果你需要暂时开启 RDS 做诊断会很麻烦。我在生产环境一般只加blacklist rds因为普通用户没有 root 权限无法绕过 blacklist 手动加载模块但如果你内部有自动化工具会主动加载模块再用install /bin/false会更稳妥。4.4 常见问题速查表现象可能原因处理方式创建 PF_RDS socket 返回 EPROTONOSUPPORTrds 模块未加载或内核未编译 RDSmodprobe rds确认 CONFIG_RDSsetsockopt 返回 EOPNOTSUPP当前内核已修复或 RDS 控制消息选项未实现对比发行版安全公告触发后内核 panic日志指向 rds_sendmsg漏洞路径可达内存已被破坏立即升级内核必要时先 rmmod rds升级内核后问题仍在旧模块未卸载或未重启检查 /sys/module/rds重启后确认版本容器内扫描出现该漏洞宿主机内核存在 RDS 模块按宿主机内核升级和禁用 RDS5. 从 CVE-2018-5333 看内核安全审计方法论5.1 为什么这类漏洞很难被“顺手”发现CVE-2018-5333 这类问题不是一眼能看出来的。普通开发者看到nr_pages字段会觉得它只影响“要分配多少内存”不会想到它还能影响“后面要写入多少数据”。只有把两条代码路径对照起来看才能发现分配和写入的依据不一致。Fuzz 工具不一定会覆盖到 RDS因为 RDS 不是默认启用的模块传统网络协议的 Fuzz 很少会把PF_RDS加进系统调用列表。而且触发条件需要专门构造控制消息普通随机字节流很难命中那套复杂的参数结构。这也是很多冷门协议爆出漏洞后猛一看都觉得“这也能溢出”的原因。它要求审计者既懂协议语义又懂内存分配器的行为。5.2 审计类似控制消息处理代码时我会重点盯几个点我把这类问题总结成一套检查清单做内核安全审计时可以照着看用户态传来的整数是否直接参与内存大小计算参与前有没有校验上限和负数。乘法计算大小前有没有检查乘积溢出例如nr_pages * sizeof(struct page *)是否可能超过SIZE_MAX。分配内存的长度与后续循环写入次数的依据是否来自同一个来源。如果来自两个地方必须验证两者的一致性。辅助消息里多个控制项是否可能累积数据量超过最初分配的空间。接收入口和释放入口是否对称失败路径是否可能留下已被破坏的对象。这套清单同样适用于网络协议、块设备驱动、文件系统等所有处理用户态数据的子系统。很多时候内核漏洞的根因并不是哪一行代码写得特别蠢而是开发者对输入数据隐含的信任链条没有完全收敛。5.3 给运维和内核安全入门者的几点实操心得最后分享一点我这几年遇到这类漏洞后积累下来的经验。第一别把 CVE 编号想象得那么遥远它的影响范围取决于你的内核配置而不是“这个协议用没用过”。RDS 会出现在数据库集群机器上就算你在业务层完全感知不到它模块也可能已经加载。第二修复动作别只做一个“升级内核”要同步检查模块加载策略不然大版本升级后旧模块残留的情况很常见。第三条件允许就打开 KASAN 做一次复现验证这比猜是否受影响要可靠得多。如果你也在维护一批老内核机器我的建议只有一个先确认 RDS 是不是真的在用不用就永久关闭。这个操作很简单却能直接把这个“冷门但高危”的漏洞从攻击面上抹掉。