MS08-067漏洞调试分析详解
一、前言
在《Metasploit渗透测试魔鬼训练营》中有对MS08-067漏洞原理的分析,不过作者的文笔十分晦涩难懂,读起来十分难消化,我反复阅读钻研了几遍,配合实践分析,对该部分的内容大致理解了一些,按照清晰的思路记录了这篇文章,并画了漏洞产生的流程图,增强了对漏洞组件溢出方式、shell插入方式的理解。
二、 简介
MS08-067漏洞是一个经典的、影响广泛的远程代码执行漏洞,在MSRPC over SMB通道调用Server服务程序中的NetPathCanonicalize函数时触发。
NetPathCanonicalize函数在远程访问其他主机时会调用NetpwPathCanonicalize函数,对远程访问的路径进行规范化。在路径规范化的操作中,服务程序对路径字符串的地址空间检查存在逻辑漏洞。
所谓路径规范化,就是将路径字符串中的【/】转换为【\】,同时去除相对路径【\.\】和【\..\】。如:
**/*/./** => **\*\**
**\*\..\** => **\**
攻击者通过精心设计输入路径,可以在函数去除【\..\】字符串时,把路径字符串中的内容复制到路径字符串之前的地址空间中(低地址),达到覆盖函数返回地址,执行任意代码的目的。
三、环境
四、调试
(一)组件静态分析
1. 函数流程分析
根据Microsoft提供的资料,定位到包含MS08-067漏洞的系统模块netapi32.dll(路径C:\windows\system32\)。
在Win2k3靶机中打开路径c:\windows\system32,找到netapi32.dll并拖入IDA Pro中,待其自动分析结束后,在左侧Fuction window查看此动态链接库的函数,找到漏洞所在的NetpwPathCanonicalize函数。
查找动态链接库中的函数
双击NetpwPathCanonicalize函数,定位到该函数位置,观察其流程图,发现NetpwPathCanonicalize并没有直接进行输入路径和规范化,而是继续调用了下级函数CanonicalizePathName。
函数流程图
2. 函数代码分析
IDA分析 NetpwPathCanonicalize 函数代码(F5 + 整理 + 主要代码):
2.1 函数声明及参数
DWORD NetpwPathCanonicalize(
LPWSTR PathName, //需要标准化的路径
LPWSTR Outbuf, //存储标准化后的路径的Buffer
DWORD OutbufLen, //Buffer长度
LPWSTR Prefix, //可选参数,当PathName是相对路径时有用
LPDWORD PathType, //存储路径类型
DWORD Flags // 保留,为0
)
2.2 主要代码
int __stdcall NetpwPathCanonicalize(LPWSTR PathName, LPWSTR Outbuf, DWORD OutbufLen, LPWSTR Prefix, LPDWORD PathType, DWORD Flags)
{
bool v7;
int result;
v7 = !Prefix || !*Prefix;
Prefix = (LPWSTR)*PathType;
if ( *PathType || (result = NetpwPathType(PathName, (int)&Prefix, 0), !result) )
{
if ( v7 || (result = NetpwPathType(Prefix, (int)&Flags, 0), !result) )
{
if ( OutbufLen != 0 )
{
*Outbuf = 0;
result = CanonPathName(Prefix, PathName, Outbuf, OutbufLen, 0); //===================》
核心函数,主要处理在这里,问题也出在这里
if ( !result )
result = NetpwPathType(Outbuf, (int)PathType, 0);
}
else
{
result = 2123;
}
}
}
return result;
}
int __stdcall CanonPathName(LPWSTR PathPrefix, LPWSTR PathName, LPWSTR Buffer, DWORD BufferSize, LPDWORD RetSize)
{
size_t preLen;
size_t pathLen;
wchar_t pathBuffer[MAX_PATH*2 + 1];
if ( PathPrefix && *PathPrefix )
{
preLen = wcslen(PathPrefix);
if ( preLen != 0)
{
if ( preLen > 520 ) //520 = sizeof(pathBuffer) - 1
return 0x7Bu; // ERROR_INVALID_NAME
wcscpy(pathBuffer, PathPrefix);
if ( pathBuffer[preLen-1] != '\\' && pathBuffer[preLen-1] != '/') //=============================》判断前缀是否以'\'或'/'结尾
wcscat(pathBuffer, L"\\");
++preLen;
}
if ( PathName[0] == '\\' || PathName[0] == '/' )
++pathLen;
}
}
else
{
pathBuffer[0] = 0;
}
pathLen = wcslen(PathName);
if (pathLen + preLen > sizeof(pathBuffer) - 1)
return 0x7Bu; // ERROR_INVALID_NAME
wcscat(pathBuffer, PathName);
if ( pathBuffer )
{
do //===========================》该循环把路径中的'/'转换成'\'
{
if ( *pathBuffer == '/' )
*pathBuffer = '\\';
++pathBuffer;
}
while ( *pathBuffer );
}
if ( !sub_71C4A2CA() && !ConPathMacros(pathBuffer) )
//=================》ConPathMacros中存在缓冲区溢出
return 0x7Bu;
pathLen = 2 * wcslen(&pathBuffer) + 2;
if ( pathLen > BufferSize )
{
if ( RetSize )
*RetSize = pathLen;
result = 0x84Bu;
}
else
{
wcscpy(Buffer, &pathBuffer);
result = 0;
}
return result;
}
(二) 漏洞动态调试
在基本的静态分析后,接下来利用动态调试来看一下函数CanonicalizePathName进行了哪些操作导致漏洞发生。
1. 调试前准备
1.1 附着到svchost.exe进程
根据资料,调用漏洞服务Server的进程为svchost.exe,其中命令行为【C:\Windows\System32\svchost.exe-k netsvcs】的进程是目标所在。
通过wmic查看命令行参数为svchost.exe -k netsvcs的进程pid为924。
查看svchost.exe -k netsvcs的进程pid
打开OllyDbg,点击file->attach,可以发现有很多svchost.exe进程,这里通过PID区分,前面知道svchost.exe –k netsvcs的pid为924(十进制),而这里的PID是16进制,924(Dex)= 39c(Hex),所以这里附着到PID为00000039C的svchost.exe进程上。
附着到进程
1.2 定位函数地址并下断点
点击View->Executable modules查看所有可执行模块(Alt+E),双击选中netapi32。
选中netapi32模块
在cpu指令窗口右键选Search for查找exec(label) in current module当前模块名称(Ctrl+N),找到函数NetpwPathCanonicalize,地址为0x71C44A3E,并在此地址按下F2下断点。
NetpwPathCanonicalize函数的地址
2. 追踪漏洞触发过程
1.1 NetpwPathCanonicalize中断
回到OD的CPU指令窗口,按F9运行。
在渗透测试主机Metasploit终端加载渗透模块后,输入命令exploit执行
执行exploit
分析环境中的svchost程序中断在NetpwPathCanonicalize函数的入口地址处。
该函数的传入参数如下所示:
NetpwPathCanonicalize传入参数
esp [esp] * 注释 *
00ECF924 02248D34 指向待整理路径
00ECF928 022321D8 指向输出路径buffer
00ECF92C 000003F1 输出buffer的长度
00ECF930 02248FB0 指向prefix,值为 \x5C\x00 ,即unicode ‘\’
00ECF934 02248FB4 指向路径类型,值为 0x1001
00ECF938 00000000 WORD Flags保留,值为0
1.2 CanonicalizePathName中断
结合IDA Pro对函数NetpwPathCanonicalize的流程分析,在地址0x71C44A9E处将调用下一级函数CanonicalizePathName,在该地址处按F2下断点。
CanonicalizePathName 函数的地址
按F9运行到该地址,然后按F7跟入函数CanonicalizePathName。传入参数如下所示:
CanonicalizePathName传入参数
esp [esp] * 注释 *
00ECF8FC 02248FB0 prefix,值为\x5C\x00
00ECF900 02248D34 指向待整理路径
00ECF904 022321D8 指向输出路径的buffer
00ECF908 000003F1 输出buffer的长度
00ECF90C 00000000 WORD Flags保留,值为0
从上面两个函数的传递的参数可以推出,函数CanonicalizePathName将待整理的路径字符串进行规范化,然后保存到预先分配的输出路径缓冲区buffer中。接下来分析该函数具体对待整理路径是如何处理的。
1.3待整理路径结构
在OD选中左下部分内存显示窗口后,右键 Go to -> Expression(Ctrl+G),输入待整理路径的地址0x02248D34,查看待整理路径的结构。
待整理路径信息
路径是一个Unicode字符串,以【\x5C\x00】(Unicode字符 ”\”)开始,到【\x00\x00】结束,中间包含一些随机的大小写字母,较长一段不可显示的字符是经过编码的Shellcode,其中最关键的部分是两个连在一起的【\..\..\】,这是表示父目录的相对路径。整个待整理路径形如:
\********\..\..\***
1.4 整理路径前的预操作
继续跟踪调试,在待整理路径所在内存地址0x02248D34 处开头的4个字节上右键->Breakpoint->Memory->Read设内存访问断点。
下内存访问断点
按F9运行,程序第一次中断在0x71C516B1。这段汇编是把待整理路径的第一个字符和【\x5C】(即Unicode字符”\”)进行比较,所以这里是检查待整理路径的第一个字符的函数。
第1次中断
继续按F9运行,第二次中断在0x77B04E36,这里是调用msvcrt.dll模块的wcslen函数,用于计算路径的长度。
第2次中断
继续按F9运行,第三次中断在0x77BD4017,这里是在msvcrt.dll模块的wcscat函数中。栈中保存的返回地址为0x71C44B14。
第3次中断
分析栈中两个参数,第一个是目的地址,指向一段以【\x5c\x00】开头的内存空间;第二个是源地址, 右键Follow in Dump显示其指向指向上述待整理路径前两字节【\x5c\x00】后的内容。
wcscat函数传入参数
在MSDN(https://msdn.microsoft.com/zh-cn/library/h1x0y282.aspx)查询wcscat函数知,该函数把src指向的宽字符串添加到dest结尾处,覆盖dest结尾处的【\0】并添加【\0】。因此,程序将把待整理路径全部复制到新申请的内存即dest处,地址为0x00F0F4DC,新路径的前缀为【\】,暂且称其为temp。
因为只能有一个内存断点,所以在0x00ECF4DC 前4字节下硬件断点,右键Hardware->access->DWord类型。
下硬件断点
1.5 复制路径到缓冲区
由于这里探究的是路径规范化的操作过程,所以只留意是否对原始待整理路径或temp中的路径字符串进行的操作,无关的跳过。
F9继续运行,第4次中断在0x77BD4010 ,内存里显示这里将src的前两个字符复制到了dest的【\x5C\x00】后面,这是由于这两个字节设了断点的原因。
第4次中断
F9,第5次中断在0x71C44B1C,位于wcscat函数内,内存显示已将src复制到dest。
第5次中断
src复制到dest
1.6 记录并跳过无关中断
以下11次中断均没有对待处理路径字符串做实质性操作,所以属于无关中断,仅做简单记录不做具体研究。
F9,第6次中断在0x71C44B1C,仍然位于wcscat函数内。
第6次中断
F9,第7次中断在0x71C44B36,由0x71C516CD跳转而来。
第7次中断
F9,第8次中断在0x71C44B2A,由0x71C44B36跳转而来。
第8次中断
F9,第9、10次都中断在0x77F47D13,栈中显示其从ntdll.dll返回而来,属于操作系统内核操作,跳过。
第9、10次中断
F9,第11次中断在0x77F4A419,栈中显示其从ntdll.dll返回而来,跳过。
第11次中断
F9,第12次中断在0x77F4A9B1, ntdll.dll调用。
第12次中断
F9,第13次中断在0x77F4A750, ntdll.dll调用。
第13次中断
F9,第14次中断在0x77F4B781,ntdll.dll调用。
第14次中断
F9,第15次中断在0x71C44BA5,netapi32调用。
第15次中断
F9第16次中断在0x71C44BC0,netapi32调用。
第16次中断
F9第17次中断在0X91C4491F,netapi32调用。
第17次中断
1.7 第一次路径规范化
F9,第18次中断在0x77BD4D36处,属于wcscpy函数,此时将调用函数进行第一次规范化,对待整理的路径进行实质性操作。
第17次中断
如图,当前参数src值为0x00EC6E0,指向【\..\***】;参数dest值为0x00ECF4DC,指向temp中的第一个字符【\】。
wcscpy参数值
而此时wcscpy源地址src在edx寄存器中,指向【\..\***】;目的地址dest在ecx寄存器中,指向待整理路径第一个字符【\】,如图
寄存器地址
所以,这次字符串复制操作就是去掉第一个表示父目录的相对路径,即待整理路径temp中的第一个【\】和第一个【\..\】之间的内容成为无用路径被抛弃。操作完成后,temp中的路径字符形如【\..\***】。
可以推出,由于还存在一个【\..\】父目录路径,所以整理之后的路径字符串还需要一次规范化操作,以去掉第二个表示父目录的相对路径。
1.8 第二次路径规范化
知道了每次路径规范化都会调用wcscpy函数,接下来删除0x00ECF4DC的硬件断点,直接在wcscpy函数的入口地址0x77BD4D28处下断点。
wcscpy函数入口断点
F9运行后中断在wcscpy函数入口0x77BD4D28处,调用wcscpy函数传入的参数如图:
wcscpy函数传入参数
esp [esp] * 注释 *
00ECF4AC 00ECF494 目的地址,指向的内存区域值为\x5c\x00,即【\】
00ECF4B0 00ECF4E2 源地址,指向第二个相对路径【\..\】的最后一个斜杠,即【\***】
正常情况下,这次规范化处理会和第一次执行同样的操作,去除第二个相对路径【\..\】,从而完成第二次的路径规范化。但这里出现了一个意外的情况,temp的首地址是0x00ECF4DC,而此次字符串复制操作的目的地址dest却在0x00ECF494,在temp之前,如图。
第二次路径规范化时调用wcscpy函数
同时注意到,栈指针ESP值为0x00ECF4A8,该地址指向wcscpy函数的返回地址0x71C52FD4。ESP到复制目的dest地址0x00ECF494只有0x14字节,于是,函数wcscpy如果继续执行,将用源字符串src覆盖wcscpy函数的返回地址。
执行到retn命令,可以看到返回地址变成了0x0100129E,,该地址的指令为:
00100129E FFD6 call esi
执行 call esi(ES=0x00F0F4DE)指令,正好将EIP指向复制尽量的字符串中构造好的第8字节空指令,接着是【\xeb\x62】(jmp 0x62),此jmp指令跳过中间的随机字符串,指向经过编码的Shellcode,如图。
返回地址被覆盖
所以这里是由于内存0x00F0F494处的一个【\】(0x5C),使得出现在处理父母了相对路径【\..\】时往前溢出了待处理路径,从而将字符串覆盖到函数wcscpy返回地址的位置,跳转到shellcode造成远程代码执行。
(三) 漏洞原理解析
经过前面的动态调试分析,在这里详细梳理漏洞触发的过程及原理。
1. 路径处理流程
\******\..\..\*** ====> \..\***
2. 路径复制
在这里知道了,在规范化复制时要寻找表示父目录的【\..\】字符串及其前面的一个【\】字符串,将这一段去掉并将新路径复制。
如如图,第一次检查时去掉了第一个相对路径并复制到缓冲区
路径字符串复制过程
但是,当【\..\】字符串在路径字符串的最前面时,那么其前面的一个【\】就在缓冲区外面了,就是在这里产生了向前(低地址)的溢出。
高址向低址溢出
3. 缓冲区溢出
返回查看之前的代码发现,正常情况下,在每次向缓冲区中复制字符串时,无论是用 wcsccpy 还是 wcscat,在复制前总要比较源字符串的长度,保证长度小于207(Hex,即520(Dex)),否则不会继续复制,这一策略确保缓冲区不会向高地址溢出,即当前函数返回时不会发生问题。
而在这个漏洞中,在规范化表示路径,寻找父目录的【\..\】字符串前面的【\】字符时,程序做了判断和边界检查:如果当前比较字符的地址与源字符串地址相同,就表明整个字符串已经查找完毕,程序就会停止查找。
int __stdcall CanonPathName(LPWSTR PathPrefix, LPWSTR PathName, LPWSTR Buffer, DWORD BufferSize, LPDWORD RetSize)
{
size_t preLen;
size_t pathLen;
wchar_t pathBuffer[MAX_PATH*2 + 1];
if ( PathPrefix && *PathPrefix )
{
preLen = wcslen(PathPrefix);
if ( preLen != 0)
{
if ( preLen > 520 ) //路径字符串长度需小于520(Dex)
return 0x7Bu; // ERROR_INVALID_NAME
wcscpy(pathBuffer, PathPrefix);
if ( pathBuffer[preLen-1] != '\\' && pathBuffer[preLen-1] != '/') //=============================》判断前缀是否以'\'或'/'结尾
wcscat(pathBuffer, L"\\");
++preLen;
}
if ( PathName[0] == '\\' || PathName[0] == '/' )
++pathLen;
}
}
else
{
pathBuffer[0] = 0;
}
然而它唯独漏了一种,就是当父目录相对路径【\..\】字符串在源字符串的开头时,在开始查找时比较的字符串(【\】到【\..\】)位于缓冲区之外的情况,这导致了复制的字符串向低地址的溢出,造成函数wcscpy的返回地址被覆盖,从而跳向攻击者精心设计的shellcode,这就是造成该漏洞的原因。
缓冲区溢出流程