未初始化栈变量漏洞:MS08-014漏洞深度解析

本文详细分析了MS08-014 Excel漏洞,其根本原因是未初始化栈变量。通过PoC代码演示攻击原理,探讨编译器警告C4700和C4701的重要性,并给出修复建议,强调在安全开发生命周期中严肃对待这些警告。

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日更新:修订代码以更准确。

  • 安全漏洞研究与防御博客作者 发布内容“按原样”提供,不提供任何保证,也不授予任何权利。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计