MS08-014 : 未初始化栈变量漏洞案例
MS08-014(CVE-2008-0081)解决了一个Excel中的漏洞,其根本原因是一个未初始化的栈变量。你可能以前见过这类编译器警告:
1
2
3
4
5
|
C:\temp>cl stack.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved.
stack.cpp
c:\temp\stack.cpp(49) : warning C4700: uninitialized local variable 'pNoInit' used ...
|
代码仍然可以正常编译和链接,所以也许你过去甚至忽略了此警告。这类代码缺陷不像缓冲区溢出那样被广泛讨论,但MS08-014是一个很好的例子,说明了为什么使用未初始化变量是潜在危险的。我们将使用以下PoC代码来说明漏洞,展示它如何易受攻击,然后提供更多关于这些编译器警告的细节:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
// stack.cpp
unsigned char scode[] = "shell code";
void parse();
void p1();
void p2();
int main(int argc, char* argv[])
{
parse();
return 0;
}
void parse()
{
p1();
p2();
}
void p1()
{
// 假设p1从文件读取数据到s
// s的内容可能由攻击者控制。
// 为简单起见,将s赋值为0x0013fee8,即返回到stack!main+0x8的地址
int s[64];
for (int i=0; i<64; i++)
{
s[i] = 0x0013fee8; // 喷洒栈缓冲区
}
}
void p2()
{
// 假设p2是下一个解析函数来读取数据。
// 例如,它尝试从文件加载图像,并赋值给其字段。
// 如果图像内容是shellcode,通过修改栈中的返回地址,shellcode就会运行。
struct {
char * pImage;
} *pNoInit;
// 这是为了对齐栈布局以演示攻击
char s[100];
pNoInit->pImage = (char *) scode;
}
|
pNoInit
在函数p2()
中使用之前未被初始化。这是一个可能导致代码执行的编程错误吗?答案在于攻击者是否能在p2()
被调用之前准备好栈。如果是这样,pNoInit
的值将由攻击者控制。
在我们的例子中,函数p1()
在p2()
之前被调用。在函数p1()
中,我们尝试设置栈中特定位置的值。这些值即使在p1
返回后仍保留在栈中。因此,当p2
进入时,攻击者控制了pNoInit
的值。在这种情况下,它指向main()
的返回地址,然后攻击就开始了。
在调试器中的样子如下:
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
26
27
28
|
stack!p2:
00401090 55 push ebp
0:000> k
ChildEBP RetAddr
0013fed4 0040101d stack!p2 [h:\work\stack\stack\stack.cpp @ 57] 0013fedc 00401008 stack!parse+0xd [h:\work\stack\stack\stack.cpp @ 33]
0013fee4 004012ae stack!main+0x8 [h:\work\stack\stack\stack.cpp @ 26] 0013ffc0 7c816fd7 stack!mainCRTStartup+0x173 [f:\vs70builds\3077\vc\crtbld\crt\src\crt0.c @ 259]
WARNING: Stack unwind information not available. Following frames may be wrong.
0013fff0 00000000 kernel32!RegisterWaitForInputIdle+0x49
0:000> dv
pNoInit = 0x0013fee8
s = char [100] "???"
stack!p2+0x32:
004010c2 8b458c mov eax,dword ptr [ebp-74h] ss:0023:0013fe60=0013fee8
0:000> t
eax=0013fee8 ebx=7ffdf000 ecx=00000063 edx=7c90eb63 esi=00000a28 edi=00000000
eip=004010c5 esp=0013fe5c ebp=0013fed4 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
stack!p2+0x35:
004010c5 c70030704000 mov dword ptr [eax],offset stack!scode (00407030) ds:0023:0013fee8=004012ae
0:000> k
ChildEBP RetAddr
0013fed4 0040101d stack!p2+0x3b [h:\work\stack\stack\stack.cpp @ 79] 0013fedc 00401008 stack!parse+0xd [h:\work\stack\stack\stack.cpp @ 33]
0013fee4 00407030 stack!main+0x8 [h:\work\stack\stack\stack.cpp @ 26] 0013ffc0 7c816fd7 stack!scode
WARNING: Stack unwind information not available. Following frames may be wrong.
|
当编译器警告我们关于未初始化变量时,它试图保护我们免受真正的漏洞侵害!
你还可以通过使用/W4
开关从编译器获得更多帮助。检查以下test.cpp:
1
2
3
4
5
6
7
8
9
10
11
12
|
int main(int argc, char *argv[])
{
int *pNoInit ;
int *pMaynotInit;
if (argc < 3)
{
pMaynotInit = 0;
}
return *pNoInit + *pMaynotInit;
}
|
1
2
3
4
5
6
7
|
C:\test>cl /W4 test.cpp
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 13.10.3077 for 80x86
Copyright (C) Microsoft Corporation 1984-2002. All rights reserved.
test.cpp
c:\test\test.cpp(11) : warning C4700: local variable 'pNoInit' used without having been initialized
c:\test\test.cpp(11) : warning C4701: local variable 'pMaynotInit' may be used without having been initialized
|
现在我们看到C4700警告,和之前一样,还有一个C4701警告,“可能”未初始化的局部变量。/W4
会产生大量警告(这就是为什么大多数人不使用/W4
编译),但大多数4701警告很容易修复。这些额外的警告可能是噪音,或者它们可能指示一个细微的错误。
例如,以下代码将被标记为潜在使用未初始化变量:
1
2
3
4
5
6
7
8
9
10
11
12
|
MYOBJECT *p;
HRESULT hr = S_OK;
if(SUCCEEDED(hr))
{
hr = MyInitializationFunction(&p);
}
if(SUCCEEDED(hr))
{
p->DoSomething(); /*flagged as potential uninitialized use*/
}
|
编译器不知道MyInitializationFunction()
在成功时会初始化*p
。然而,在声明时将p
初始化为NULL可以避免任何歧义——并且这是良好的实践,以防MyInitializationFunction
中存在错误,导致它在没有初始化其参数的情况下返回成功!
4701警告的另一个常见噪音来源是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
foo(BYTE packet_type)
{
MYOBJECT *p;
switch(packet_type)
{
case REQUEST:
p= new MYOBJECT ...;
case RESPONSE:
p= new MYOBJECT ...;
}
p->DoSomething(); <- flagged as potential uninitialized use
...
|
编译器捕捉到packet_type
既不是REQUEST也不是RESPONSE的理论代码路径。switch语句的隐式’default’情况不会初始化p
——因此产生警告。也许真的只有两种数据包类型可以到达此代码(例如,因为packet_type
的验证在此函数被调用之前发生)。如果是这样,那么switch语句之一将总是被执行,p
将总是被初始化。然而,再次强调,警告很容易通过在声明时将p
初始化为null来修复——并且如果上述代码可以被到达(例如,通过恶意攻击者 crafting 一个带有伪造类型字段的网络数据包),那么我们将一个几乎确定的代码执行漏洞减少为一个空解引用。当然,更好的修复方法是添加一个显式的default语句,当它发现意外的packet_type
值时返回错误代码。
我们希望以上讨论表明这些编译器警告应该被严肃对待。事实上,我们希望在下一个版本的SDL中将它们添加为禁止的警告类型。
2008年4月16日更新:修订代码以更准确。
- 安全漏洞研究与防御博客作者
发布内容“按原样”提供,不提供任何保证,也不授予任何权利。