深入解析cURL库中格式化函数后向兼容路径的栈缓冲区溢出漏洞

本文详细分析了cURL库mprintf.c文件中一个栈缓冲区溢出漏洞的成因。该漏洞存在于`out_double()`函数的后向兼容实现中,当编译时未定义HAVE_SNPRINTF而被迫使用传统sprintf函数时,对负浮点数的整数部分位数计算存在缺陷,导致栈缓冲区溢出,可能引发程序崩溃或更严重的安全问题。

Stack Buffer Overflow in mprintf.c formatting function (fallback path)

摘要

一个栈缓冲区溢出漏洞存在于 mprintf.c 文件的 out_double() 函数中。此漏洞影响编译时未定义 HAVE_SNPRINTF 的构建,这些构建被迫使用传统的 sprintf 函数。

负责计算浮点数格式化最大安全精度(maxprec)的逻辑无法正确处理负数。具体来说,它未能正确计算负数值整数部分所需的数字位数。结果导致缓冲区大小计算错误,使得后续的 sprintf 调用写入的数据量超过了固定大小的栈缓冲区容量。

受影响组件

  • 文件: lib/mprintf.c
  • 函数: out_double()
  • 触发条件:编译时未定义 HAVE_SNPRINTF

技术细节

该函数使用了一个大小为 BUFFSIZE(326 字节)的本地栈缓冲区(work)。为了防止溢出,代码尝试估算浮点数值整数部分所需的数字位数,并据此减少允许的精度。

相关代码如下(约在master分支的第675行):

1
2
3
4
while(val >= 10.0) {
  val /= 10;
  maxprec--;
}

对于负数值(例如, -1.0e100),在进入循环时条件 val >= 10.0 为假,因此循环被完全跳过。结果就是 maxprec 没有被减少,即使格式化后的值的整数部分将占用大量的缓冲区空间。随后的 sprintf 调用会写入负号、完整的整数部分以及请求的小数精度,超出了326字节的栈缓冲区。

复现步骤

  1. 配置构建 编译 libcurl 时未定义 HAVE_SNPRINTF 以强制使用后备的 sprintf 路径。模拟此情况的一种方法是修改 lib/mprintf.c

    1
    2
    3
    4
    5
    
    #if 0 /* 强制使用后备路径进行测试 */
      (snprintf)(work, BUFFSIZE, formatbuf, dnum);
    #else
      (sprintf)(work, formatbuf, dnum); /* 易受攻击的路径 */
    #endif
    
  2. 编译复现程序 针对修改后的 libcurl 构建,编译以下 C 程序:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    #include <stdio.h>
    #include <curl/curl.h>
    #include <string.h>
    
    int main(void) {
        // -1.0e100 的整数部分需要约 102 个字符
        // 精度 .300 需要 300 个字符用于小数部分
        // 总输出长度约 402 个字符
        // mprintf.c 中的栈缓冲区大小为 326 字节
        double v = -1.0e100;
    
        char *output = curl_maprintf("%.300f", v);
    
        if (output) {
            printf("Output length: %lu bytes\n",
                   (unsigned long)strlen(output));
            curl_free(output);
        }
        return 0;
    }
    
  3. 运行程序

  4. 观察 输出长度超过了 326 字节的缓冲区大小。根据栈保护机制(栈金丝雀,ASLR),这可能导致段错误或栈破坏检测。

建议的修复方案

通过操作绝对值(或等效逻辑)来修正整数位数的计算,以考虑负值:

1
2
3
4
5
6
double absval = fabs(val);

while(absval >= 10.0) {
  absval /= 10.0;
  maxprec--;
}

可选地,限制 maxprec 防止下溢:

1
2
if(maxprec < 0)
  maxprec = 0;

影响

此漏洞导致经典的栈缓冲区溢出。

  • 可用性影响:拒绝服务(应用程序崩溃)
  • 安全影响:在浮点数值和格式字符串受攻击者控制输入影响的情况下,此问题可能被利用来造成进一步的内存破坏,包括控制流操控。
  • 影响范围:易受攻击的代码路径是传统后备实现的一部分,在现代默认的 curl 构建中不被使用。它主要影响传统的 Unix 系统、嵌入式平台或那些 snprintf 不可用或明确禁用的自定义构建。

后续讨论与处理

bagder (curl staff) 评论

@han_ank 请提及一个在使用未修改源代码的情况下会发生此问题的平台。

jimfuller2024 (curl staff) 评论

哪些平台…可能是非常老的 Linux 发行版(我记得一些较旧的 BSD 变体,当然还有 System V!)…可能是一些嵌入式系统(TinyOS 或 FreeRTOS)…这里几乎没有“攻击面”,这可能表明我们可以完全弃用这一切。

jimfuller2024 (curl staff) 评论

忘记提了——但我不确定 TinyOS 能否构建 curl…也许有人用 FreeRTOS 做过。

bagder (curl staff) 评论

90年代有些系统没有 snprintf。我相信即使是那个时候非常老的 Windows 也可能没有。 在过去的二十年左右,即使是嵌入式系统也配备了提供 snprintf 的合适 libc 实现。我找不到过去几十年内不提供 snprintf 的系统。 snprintf() 是 C99 标准强制要求的,正如其名,该标准大约在 1999 年出台。旨在与 C99 兼容的 libc 实现则需要提供该函数。 由于我们找不到任何非遗留、已终止生命周期的系统,我们不能将此视为一个有效的漏洞——而是一个 Bug。 为了避免将来再次发生这种讨论,我们从代码中移除了 sprintf() 后备路径:https://github.com/curl/curl/pull/20218

ankitsingh015 评论

感谢澄清。考虑到没有不提供 snprintf 的受支持平台,我理解将其分类为 Bug 而非安全漏洞。我赞赏完全移除后备路径以避免未来问题的决定。

bagder (curl staff) 关闭了报告并将状态更改为 Informative

感谢您的报告。现已修复此问题,因此关闭并标记为 Informative。

bagder (curl staff) 请求公开此报告

根据项目透明政策,我们希望所有报告都公开披露。

ankitsingh015 同意公开此报告此报告已被公开。

报告信息

  • 报告日期:2026年1月7日,UTC 22:12
  • 报告人:ankitsingh015
  • 报告对象:curl
  • 报告ID:#3493602
  • 状态:Informative
  • 严重性:高 (7 ~ 8.9)
  • 披露日期:2026年1月8日,UTC 09:36
  • 弱点类型:经典缓冲区溢出
  • CVE ID:无
  • 赏金:无
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计