Python栈溢出CVE-2021-3177分析

问题背景

我正在学习缓冲区溢出,同时又对各个语言的安全问题感兴趣。最近看到 Python CVE-2021-3177 ,所以分析一下。

从描述上看,这是一个sprintf函数引发的栈溢出漏洞。

分析过程

因为本文主要分析的和Python执行流程关系不大,所以也可以忽略第一步调试环境搭建,直接看调试分析。

  1. 搭建调试环境

    我用的IDE是Clion

    先搭建Python调试环境,编译出可调试的Python二进制程序。这一步可以参考官网 Guide

    准备好漏洞测试的Python代码

    1
    2
    3
    4
    from ctypes import *
    x = c_double.from_param(1e300)

    print(x)
  2. 调试分析

    _ctypes/callproc.c下面代码打好断点

    1
    2
    3
    4
    5
    6
    ...
    case 'd':
    sprintf(buffer, "<cparam '%c' (%f)>",
    self->tag, self->value.d); // 1e300
    break;
    ...

    在IDE中可以看到 self->value.d 是double类型,且值就是我们传入的参数。

    从这里可以看出来,这个CVE漏洞和下面这段C代码漏洞是一样的

    1
    2
    3
    4
    5
    6
    7
    #include <stdio.h>
    int main(){
    char buf[2];
    double s=1e300; //s值用户可控

    sprintf(buf, "%f", s);
    }

    所以接下来我就去研究学习上面的C代码中漏洞是怎么利用的。

  3. sprintf引起的栈溢出分析

    网上文章分析sprintf溢出都以sprintf(buf, "%s", s);举例

    对于 “%s” 格式符这种覆盖很容易理解,如果s=”a…a” 很多a字符时,就可以将rip覆盖成”\x6161616161616161”。

    对于 “%f” 这种用浮点数字去覆盖buf变量,我就有一个疑问,浮点数怎么控制rip指针。比如s=11111111111111112222222233333333.0时,rip会被覆盖成什么?

    先说自己的结论:

    1
    2
    3
    4
    5
    6
    在漏洞代码中,double浮点数可以用来向很大内存中写入 `\x30-\x39`、`\x2e` 的字节。

    * 只能写入`\x30-\x39`、`\x2e`,而不能写其他字节,是因为 %f 转换成小数只能有`[0-9.]`,数字1-9对应的十六进制就是 `\x30`-`\x39`
    * 能覆盖多大的内存?
    * 因为double类型,表示的小数位数能很大,所以至少可以覆盖300个字节
    * 如果是float类型,就要小的多了

    结论是怎么来的呢?

    首先我们要知道浮点数在计算机并不总是能够精确的被表示,只能被近似表示。更具体的细节需要去了解”浮点数在计算机中的存储”,可以看文末的参考资料。

    所以 double s=11111111111111112222222233333333.0 在内存中,s的值并不一定是 11111111111111112222222233333333.0

    我在代码中加了一行printf("%f\n",s);打印s变量实际的值。

    从下面的分析来看,在64位系统上 11111111111111112222222233333333.0 会把rip覆盖成”\x30\x38\x30\x32\x34\x33\x35\x33”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <stdio.h>
    int main(){
    char buf[2];
    double s=11111111111111112222222233333333.0; //16个1,8个2,8个3

    // s实际的值是 1111111111111111 19575513 35342080.000000
    // buf栈 rbp rip
    printf("%f\n",s);

    /*

    print("".join([hex(ord(i)).replace("0x","\\x") for i in "35342080"]))
    "35342080"字符串对应 \x33\x35\x33\x34\x32\x30\x38\x30

    >>> x=[hex(ord(i)).replace("0x","\\x") for i in "35342080"]
    >>> x.reverse()
    >>> print("".join(x))
    \x30\x38\x30\x32\x34\x33\x35\x33

    */
    // 所以rip会被覆盖成 \x30\x38\x30\x32\x34\x33\x35\x33
    // gdb验证后,符合预期

    sprintf(buf, "%f", s);
    }
  4. _ctypes/callproc.c其他代码有没有相同的栈溢出问题?
    self->value.d在_ctypes/ctypes.h文件有定义,是一个union数据类型。

    这个union类型中看着好像还有几个字段也能用在sprintf栈溢出中,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    union {
    char c;
    char b;
    short h;
    int i;
    long l;
    long long q;
    long double D; // 可以利用
    double d; // 可以利用
    float f; // 能覆盖的内存有限
    void *p; // 可能可以
    } value;

    在 _ctypes/callproc.c 文件看了看,结论是只有d变量可以利用。

    1
    2
    3
    4
    5
    6
    7
    union {
    ...
    long double D; // 没有用到这个变量
    double d; // 可以利用
    float f; // 能覆盖的内存有限
    void *p; // 因为只用到了 %p 打印地址,没有 %s ,所以也无法利用
    } value;

    _ctypes/callproc.c文件中,也只有下面的代码sprintf d变量。所以只有这一处是存在栈溢出的。

    1
    2
    3
    4
    5
    6
    ...
    case 'd':
    sprintf(buffer, "<cparam '%c' (%f)>",
    self->tag, self->value.d); // 1e300
    break;
    ...

总结

本文仅仅分析了参数怎么控制rip指针,其中我学习到的点是”浮点数在内存中的表示”。

关于漏洞还有其他很多方面都没有分析,比如:

  • 怎么自动化检测这种漏洞?
  • 漏洞的实际影响是什么?
    • 有多少Python应用存在漏洞?
  • 漏洞触发的调用链是什么?

参考资料

C语言float、double的内存表示