MS08-067已经出来三个月了,大部分服务器已经打上了补丁,看来利用价值不大。但是做为一个学习的材料还是挺好的。
这个漏洞的原理在很多文章中都解释的非常详细了,但是关于漏洞利用的文章却比较少(可能是我没找到),我自己摸索了一整子,参考了不少高手们写的exploit,终于摸索出一个还算稳定的利用方法,将摸索的过程整理成本文。
如果您发现有什么不足之处,或者有更好的方法,请联系我:
LittleWallE#yahoo.cn.(#替换为@)
或者在博客上留言:http://blog.csdn.net/littlewalle
交流才能进步,很高兴能成为你的朋友。
MS08-067的问题出在一个格式化路径的函数上,暂命名为ConvertPathMacros,它的工作就是去掉路径中的./和../, 例如传入路径为:”/A/B/./C/../D/../../E”,则经过它的处理后会变成”/A/E”。如果给出一个恶意路径,例如”/A/../../../E”,则它会越过目录A去寻找上层目录,从而造成错误的发生。
根据分析,知道了稳定利用的关键是反斜杠的位置,所以目标就是能在栈上找一个相对固定的位置写入一个反斜杠。而且为了保证溢出,反斜杠的位置还必须在发生溢出时esp位置之上。
为了达到这个目标,有两个方法:
1. 看看调用NetpwPathCanonicalize函数时,参数能否影响到栈上数据。有可能很长的参数能够在ConvertPathMacros函数之前在栈上写下数据。
2. 调用另一个RPC函数,在栈上留下一大堆数据后再调用NetpwPathCanonicalize函数,这样找到反斜杠的概率较大。
第一种方法经过测试发现行不通,ConvertPathMacros函数在调用堆栈的最顶端,其他函数没有机会向栈顶写数据。
而第二种方法也需要经过测试才能验证是否可行,以另一个流行的RPC函数NetpwNameCompare函数为例(其实也不流行,是参考了一位高手的exploit,他的邮箱是[email protected]),进行下面的实践。
在虚拟机上安装了windows2003 sp0 中文版, 本机上操作系统是windows xp sp2 英文版。在本机上写好客户端,客户端功能是先后调用虚拟机上的RPC函数NetpwNameCompare和NetpwPathCanonicalize。在服务端装上OLLYDBG, Process Explorer。
启动虚拟机,启动完毕后,建立一个快照(snapshot),便于以后问题重现,也避免调试过程中系统崩溃。
首先得看看NetpwNameCompare函数能否在栈上留下足够的数据供使用。不能的话就换函数。操作如下:
1. 开启Process Explorer,找到提供Computer Browser服务的进程(svchost),记下进程号,在OLLYDBG中附加,如图:
2. 在下面几个关键函数的入口,返回地址处加断点。
NetpwNameCompare
NetpwPathCanonicalize
ConvertPathMacros (7C941084)
断点设置完毕后,按F9,让svchost进程继续运行。
3. 使用客户端调用虚拟机上的服务:
NetpwNameCompare(L"LittleWallE",L"123456789",L"123456789",4,0);
NetpwPathCanonicalize(arg_0,(unsigned short *)arg1,(unsigned char *)arg2,
arg3,arg4,(long *)Buff3,1); //暂时使用正常参数
可以看到svchost进程断在了NetpwNameCompare函数入口,记录下当时esp的值。按F9继续运行,断在NetpwPathCanonicalize函数入口时,同样记录下esp的值,依此类推,有下面的记录:
esp |
function |
015ff47c |
wscpy位置 |
|
问题函数(7C941084) |
015ff8f8 |
NetpwPathCanonicalize(71BA9511) |
015ff908 |
NetpwNameCompare(71BABE87) |
4. 由于溢出时,必须覆盖wscpy位置才能利用成功,而且根据以前的分析,传入的恶意路径的长度要小于0x207,所以我们关心的内存位置为wscpy位置以上的0x207大小的区域(015ff276~ 015ff47c), 只要NetpwNameCompare函数能在这个区域中留下数据,则稳定利用就有希望了。
5. 再次使用客户端同时调用虚拟机上的服务如下:
NetpwNameCompare(L"LittleWallE",L"123456789",L"123456789",4,0);
NetpwPathCanonicalize(arg_0,(unsigned short *)arg1,(unsigned char *)arg2,
arg3,arg4,(long *)Buff3,1); //暂时使用正常参数
当svchost进程断在NetpwNameCompare函数入口时,将esp上的内存全部清空,如图:
按F9继续运行,当进程断在NetpwNameCompare函数返回地址时,查看区域(015ff276~ 015ff47c)的值。谢天谢地,有值了,而且是函数的参数"123456789",如图:
根据这几步验证,证实了NetpwNameCompare函数可以用来向栈上写数据。
1. 由于堆栈里的数据是ASCII码的”123456789”, 而函数的参数是UNICODE的”123456789”,所以中间肯定进行了一些转换。为了查看数据的来源,在堆栈上的”123456789”上下”内存写入断点”。并让程序继续运行,如图:
2. 再次使用客户端调用服务,按几次F9后,进程被”内存写入断点”断下来了。如图:
3. 查看调用堆栈,可以看到当前的函数是NTDLL.DLL中的RtlUpcaseUnicodeToOemN. 剩下的分析只能靠IDA了。用IDA打开后界面如下(流程好长):
4. 刚开始吓一跳,这么长的流程从哪分析起。后来耐心看了看,并结合OLLYDBG的动态调试,其实一大半的流程是可以砍掉的。我们只关心能向区域(015ff276~ 015ff47c)写入数据的分支,其他分支可以忽略掉。在这个函数里面,刚开始有条命令
cmp NlsMbOemCodePageTag, 0
jnz loc_7C92C964
在动态调试中发现,NlsMbOemCodePageTag的值肯定不为0,于是直接用二进制编辑器将将命令更改为:
jmp loc_7C92C964
5. 更改完命令后,再次用IDA分析,OK, 这次分支少多了:
6. 剩下的任务就是逆向工程了,花了点时间将汇编写成C代码,如下。代码的大概意思是将入参L”123456789”经过几个编码表映射后,写入栈上的缓冲区,而这个缓冲区中的数据就是我们所要关心的。只要我们能正确控制参数,使缓冲区上的数据能出现一个反斜杠,那么溢出就成功了。
#define BYTE_HIGH(var) ((var&0xf0)>>4) #define BYTE_LOW(var) (var&0xf) #define WORD_HIGH(var) ((var&0xff00)>>8) #define WORD_LOW(var) (var&0xff) #define DWORD_HIGH(var) ((var&0xffff0000)>>16) #define DWORD_LOW(var) (var&0xffff) /********************************************************************** *function:将argc_SrcBuffer的字符逐个进行编码表转换后放入栈上的缓冲区中 *author: LittleWallE#yahoo.cn ***********************************************************************/ RtlUpcaseUnicodeToOemN(char * arg0_DstBuffer, //栈上的缓冲区 { char *MatrixA;//mark:dword_7c9b8e94=,一个表格,内容是0000 0100 0200 0300..一直继续下去,可能是某种编码表 char *MatrixB;//mark:dword_7C9B8A04 另一个编码表。
char *MatrixUnknownA;//mark:word_7c9b8c90; 映射区域,不确定其功能; char *MatrixUnknownB;//mark:dword_7C9B8E90;映射区域,不确定其功能; char *MatrixUnknownC;//mark:dword_7C9B779C;映射区域,不确定功能;
int var_8_SrcRealLength=arg10_SrcBufferLength/2;
if(var_8_SrcRealLength==0) return 0;
while(1) { if(arg4_DstBufferLength==0) break; WCHAR wcSrcChar_1=*argc_SrcBuffer; argc_SrcBuffer++;
WCHAR wcSrcChar_2=MatrixA[wcSrcChar_1*2];
wcSrcChar_2_high=WORD_HIGH(wcSrcChar_2); wcSrcChar_2_high=MatrixUnknownB[wcSrcChar_2_high*2]; wcSrcChar_2_low=WORD_LOW(wcSrcChar_2); WCHAR wcSrcChar_3; if(wcSrcChar_2_high==0) { wcSrcChar_3=MatrixB[wcSrcChar_2_low*2]; }else { int index=wcSrcChar_2_high+wcSrcChar_2_low; wcSrcChar_3=MatrixUnknownB[index*2]; } if(wcSrcChar_3>='a'&&wcSrcChar_3<='z') { wcSrcChar_3+=0x0ffe0; }else{ char wcSrcChar_3_high=WORD_HIGH(wcSrcChar_3); wcSrcChar_3_high=MatrixUnknownC[wcSrcChar_3_high*2]; char wcSrcChar_3_byte_high=BYTE_HIGH(wcSrcChar_3); wcSrcChar_3_high+=wcSrcChar_3_byte_high; wcSrcChar_3_high=MatrixUnknownC[wcSrcChar_3_high*2]; char wcSrcChar_3_byte_low=BYTE_LOW(wcSrcChar_3); wcSrcChar_3_high+=wcSrcChar_3_byte_low; wcSrcChar_3+=wcSrcChar_3_high; }//上面这段类似不可逆的加密算法 WCHAR wcSrcChar_4=MatrixA[wcSrcChar_3*2]; char wcSrcChar_4_high=WORD_HIGH(wcSrcChar_4); char wcSrcChar_4_low=WORD_LOW(wcSrcChar_4); if(wcSrcChar_4_high!=0) { if(arg4_DstBufferLength<2) break; *arg0_DstBuffer=wcSrcChar_4_high; //这里写入栈上的缓冲区
arg0_DstBuffer++; arg4_DstBufferLength--; } *arg0_DstBuffer=wcSrcChar_4_low; //这里写入栈上的缓冲区
arg0_DstBuffer++; arg4_DstBufferLength--; var_8_SrcRealLength-- if(arg4_DstBufferLength==0||var_8_SrcRealLength==0) break; } }
|
然而实际上却没这么简单,从代码上看,有点像不可逆加密算法。无法知道什么样的参数最后能被转换为反斜杠。事已至此,只好用个笨办法,穷举吧。写了个小程序,调用ntdll.dll中的RtlUpcaseUnicodeToOemN函数,传入/x0000到/xffff,看看哪个字符会输出反斜杠。得到的结果如下:
0x2010, 0x2011, 0x2012……
根据上面的分析,知道了只要调用NetpwNameCompare函数,传入参数中含有0x2010之类的字符就能使栈上出现反斜杠。
另外要注意这个反斜杠必须是个UNICODE的反斜杠,即”/x5c/x00”,反斜杠后面的必须跟一个”/x00”,这个”/x00”用字符串的0结尾来代替即可。在字符串在前面加上一个ASCII字符的UNICODE,如"/x4c/x00",以"/x4c/x00/x10/x20"入参,则在栈上得到的结果为"/x4c/xXX/x5c/x00",正好包含一个UNICODE反斜杠。
分析完毕后就可以利用了。稍微调试下,整理出如下的表格:
esp |
function |
015ff3ce |
反斜杠位置 |
015ff478 |
Wscpy函数里的返回位置. |
015ff47c |
Wscpy调用时 |
|
问题函数(7C941084) |
015ff8f8 |
NetpwPathCanonicalize入口 |
015ff908 |
NetpwNameCompare入口 |
计算出反斜杠与Wscpy函数里的返回位置之间相距0xA8(xp sp2 是0xA0)个字节。
构造字符串如下:
然后相继调用下面两个函数:
NetpwNameCompare
NetpwPathCanonicalize
具体代码我发在博客里了:http://littlewalle.download.csdn.net/。
在windows2000和windows xp中,CanonPathName这个函数有一个另外的问题。 可以简单把流程想象成这样:
CanonPathName(WCHAR *A, WCHAR B) { WCHAR buffer[414]; if(A!=0) { if(wcslen(A)!=0) { wcscpy(buffer,A); } }else { buffer[0]=0; } wcscat(buffer,B); ………. } |
如果参数A不是0,而是一个指向空字符串的指针,则该函数的作用就是直接将B连接到缓冲buffer上,而buffer是一片没有初始化的区域,从而造成后面的字符串出错。
相应的,如果以下面的方式调用函数 (第四个参数是空字符串):
NetpwPathCanonicalize(“LittleWallE”,(unsigned short *)buffer,(unsigned char *)arg2, arg3,L"",(long *)Buff3,1);
则偶尔会利用不成功。
解决方法也很简单,将第四个参数设置为L"."即可.
MS08-067应用范围并不广,主要有以下障碍:
1> windows xp sp2下默认是开启了DEP保护的,即栈上代码不可执行。Shellcode发过去之会让系统弹出一个错误对话框“xxxx地址不可写/执行”。
不过windows2003的DEP却仿佛没有效果,在网上也没搜索到答案,希望高手指点迷津。
2> 系统开了防火墙就无法利用。智能一点的防火墙发现RPC调用时会进行提示或者直接阻止。
3> 系统没有开server, workstation, computer browser这几个服务,RPC调用就会受阻。
所以MS08-067没有想象中的强大。不过研究它的过程还是挺有意思的。