Python栈溢出CVE-2021-3177分析
问题背景
我正在学习缓冲区溢出,同时又对各个语言的安全问题感兴趣。最近看到 Python CVE-2021-3177 ,所以分析一下。
从描述上看,这是一个sprintf函数引发的栈溢出漏洞。
分析过程
因为本文主要分析的和Python执行流程关系不大,所以也可以忽略第一步调试环境搭建,直接看调试分析。
搭建调试环境
我用的IDE是Clion
先搭建Python调试环境,编译出可调试的Python二进制程序。这一步可以参考官网 Guide
准备好漏洞测试的Python代码
1
2
3
4from ctypes import *
x = c_double.from_param(1e300)
print(x)调试分析
在
_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代码中漏洞是怎么利用的。
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);
}_ctypes/callproc.c
其他代码有没有相同的栈溢出问题?
self->value.d在_ctypes/ctypes.h文件有定义,是一个union数据类型。这个union类型中看着好像还有几个字段也能用在sprintf栈溢出中,如下:
1
2
3
4
5
6
7
8
9
10
11
12union {
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
7union {
...
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应用存在漏洞?
- 漏洞触发的调用链是什么?