Windows Workstation Service远程溢出的分析By snake. snake@cnns.net
Company page: http://www.cnns.net
题注:世界著名安全组织eEye Digital Security,于11月11日在http://www.eeye.com 站点主页公布了他们关于对Windows Workstation服务存在缓冲溢出缺陷的发现。这个缺陷牵涉到的是多数Windows操作系统赖以正常运行的基本服务,可以被远程利用,相关的TCP端口是139和445。这个缺陷的公布,对正处于多事之秋的Windows操作系统安全体系又是一次冲击。不过,正如CNNS提出的“攻击是检验网络安全的最佳手段”,每一次操作系统的严重缺陷和他们的攻击利用代码,以及后来出现的蠕虫,都迫使厂商加速推出安全解决方案,有关这些服务的安全性也将得到一定提升。这种攻与防的循环,将伴随任何一个主流操作系统的生命周期。
本文是CNNS研发部门对此缺陷的攻击利用技术进行逐步分析的记述,以裨专业人员参考。时间仓促,有任何错误与不足之处,欢迎来信交流:snake@cnns.net
///////////////////////////////////////////////////////////////////////////////////////////////////////////
Eeye在11月11日公布了Windows Workstation存在远程溢出的漏洞,这是微软的又一个基本服务,存在严重的被攻击缺陷。
本文将就如何利用这个漏洞做逐步分析。
根据eeye的一些公开的信息来看,漏洞是出在 wkssvc.dll的 vsprintf调用。推断应该是没有检查输入缓冲的长度。利用函数NetValidateName 可以直接攻击。
下面的环境是:
客户端:win2k,和服务端建立 ipc$连接,然后,用 NetValidateName 进行交互,触发溢出。具体的sample代码不贴出来了,packetstorm和其他站点已经公布了不少。。。
服务端:被攻击端(简体中文win2k + sp3)
打开windbg,跟踪相关的函数,开始是 RPCRT4.dll的 NdrServerCall2调用,这个短时间内没有办法细看和消化,不理会,继续跟踪进去。。。
然后又是一些调用。包括 NdrServerInitializeNew的调用, NdrPointerUnmarshell和 NdrConformantStringUnmarshall的调用,这些也可以不理会,继续跟踪,呵呵,机器其实已经重新启动了N次,没有关系,虚拟机。:)
下面是出错函数的分析,上面的一些初始化动作不理会,总之,错误是出在这里面,利用也是在这个函数返回的时候利用。。
.text:76724CD7 ; int __stdcall sub_76724CD7(HANDLE hFile,int,int)
.text:76724CD7 sub_76724CD7 proc near ; CODE XREF: sub_76724DB5+20p
.text:76724CD7
.text:76724CD7 var_81A = byte ptr -81Ah
.text:76724CD7 var_819 = byte ptr -819h
.text:76724CD7 Buffer = byte ptr -818h
.text:76724CD7 var_817 = byte ptr -817h
.text:76724CD7 NumberOfBytesWritten= dword ptr -14h
.text:76724CD7 SystemTime = _SYSTEMTIME ptr -10h
.text:76724CD7 hFile = dword ptr 8
.text:76724CD7 arg_4 = dword ptr 0Ch
.text:76724CD7 arg_8 = dword ptr 10h
.text:76724CD7
.text:76724CD7 push ebp
.text:76724CD8 mov ebp, esp
.text:76724CDA sub esp, 818h ; //!!这里只分配了 0x818=2072个字节的空间给全部变量
.text:76724CE0 cmp [ebp+hFile], 0 ; //判断是否无效的文件句柄
.text:76724CE4 jz locret_76724DB1 ; //如果是,则返回
.text:76724CEA push edi
.text:76724CEB mov edi, offset unk_76727C60
.text:76724CF0 push esi
.text:76724CF1 push edi ; lpCriticalSection
.text:76724CF2 call ds:EnterCriticalSection ; //进入临界空间
.text:76724CF8 xor esi, esi
.text:76724CFA cmp dword_76727A3C, esi ; 判断是否需要打印时间信息
.text:76724D00 jz short loc_76724D3C
.text:76724D02 lea eax, [ebp+SystemTime] ; 下面进行时间信息字符串的输出。
.text:76724D05 push eax ; lpSystemTime
.text:76724D06 call ds:GetLocalTime
.text:76724D0C movzx eax, [ebp+SystemTime.wSecond]
.text:76724D10 push eax
.text:76724D11 movzx eax, [ebp+SystemTime.wMinute]
.text:76724D15 push eax
.text:76724D16 movzx eax, [ebp+SystemTime.wHour]
.text:76724D1A push eax
.text:76724D1B movzx eax, [ebp+SystemTime.wDay]
.text:76724D1F push eax
.text:76724D20 movzx eax, [ebp+SystemTime.wMonth]
.text:76724D24 push eax
.text:76724D25 lea eax, [ebp+Buffer]
.text:76724D2B push offset a02u02u02u02u02 ; "%02u/%02u %02u:%02u:%02u "
.text:76724D30 push eax
.text:76724D31 call ds:sprintf ; //at first, format the time string...
.text:76724D37 add esp, 1Ch
.text:76724D3A mov esi, eax
.text:76724D3C
.text:76724D3C loc_76724D3C: ; CODE XREF: sub_76724CD7+29j
.text:76724D3C push [ebp+arg_8]
.text:76724D3F lea eax, [ebp+esi+Buffer] ; 得到输出缓冲的地址,这里是 esi-0x818
.text:76724D3F ; 其中,esi是调整的输出指针。如果打印了时间信息,
.text:76724D3F ; 则=时间字符串的长度。否则,=0。
.text:76724D46 push [ebp+arg_4] ; 这里的格式是:
.text:76724D46 ; NetpValidateName: checking to see if '%ws' is valid as type %d name.
.text:76724D46 ;
.text:76724D46 ; *** 注意,是 %ws 和 %d 的参数。
.text:76724D46 ; %ws。。。。比较麻烦的转换。嘿嘿,还是有办法的。
.text:76724D46 ;
.text:76724D49 push eax
.text:76724D4A call ds:vsprintf ; 这里发生了溢出
.text:76724D50 add esp, 0Ch
.text:76724D53 add esi, eax ; 这里判断是否 esi+eax = 0。如果没有输出,则做个标记=0
.text:76724D55 jz short loc_76724D6D
.text:76724D57 cmp [ebp+esi+var_819], 0Ah ; ...搞不懂为什么这里要判断。如果没有回车,也做个标记。。:(
.text:76724D57 ;
.text:76724D5F jnz short loc_76724D6D
.text:76724D61 mov dword_76727A3C, 1
.text:76724D6B jmp short loc_76724D78 ; 增加一个回车到输出缓冲的开头,很好玩,
.text:76724D6B ;
.text:76724D6D ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:76724D6D
.text:76724D6D loc_76724D6D: ; CODE XREF: sub_76724CD7+7Ej
.text:76724D6D ; sub_76724CD7+88j
.text:76724D6D xor eax, eax
.text:76724D6F test eax, eax
.text:76724D71 mov dword_76727A3C, eax
.text:76724D76 jz short loc_76724D91
.text:76724D78
.text:76724D78 loc_76724D78: ; CODE XREF: sub_76724CD7+94j
.text:76724D78 mov [ebp+esi+var_819], 0Dh ; 增加一个回车到输出缓冲的开头,很好玩,
.text:76724D78 ;
.text:76724D80 mov [ebp+esi+Buffer], 0Ah
.text:76724D88 and [ebp+esi+var_817], 0
.text:76724D90 inc esi
.text:76724D91
.text:76724D91 loc_76724D91: ; CODE XREF: sub_76724CD7+9Fj
.text:76724D91 lea eax, [ebp+NumberOfBytesWritten]
.text:76724D94 push 0 ; lpOverlapped
.text:76724D94 ; 这里进行写文件的动作。
.text:76724D94 ; 注意,WriteFile的第4个参数
.text:76724D94 ; lpNumberOfBytesWritten 是在
.text:76724D94 ; ebp-14的位置,会改写buffer,所以,
.text:76724D94 ; 如果有shellcode放到那里,就要小心
.text:76724D94 ; 这个位置的数据了。。
.text:76724D96 push eax ; lpNumberOfBytesWritten
.text:76724D97 lea eax, [ebp+Buffer]
.text:76724D9D push esi ; nNumberOfBytesToWrite
.text:76724D9E push eax ; lpBuffer
.text:76724D9F push [ebp+hFile] ; hFile
.text:76724DA2 call ds:WriteFile
.text:76724DA8 push edi ; lpCriticalSection
.text:76724DA9 call ds:LeaveCriticalSection ; 这里 LeaveCriticalSection。还好,参数edi没有被改掉。
.text:76724DA9 ; 否则,进行攻击的时候又多了很多麻烦了。
.text:76724DAF pop esi
.text:76724DB0 pop edi
.text:76724DB1
.text:76724DB1 locret_76724DB1: ; CODE XREF: sub_76724CD7+Dj
.text:76724DB1 leave
.text:76724DB2 retn 0Ch ; ok,函数返回,嘿嘿,处理好了,就会执行我们的shellcode。
.text:76724DB2 sub_76724CD7 endp
.text:76724DB2
.text:76724DB5
如上分析,程序在vsprintf中,的参数 %ws进行格式化,将NetValidateName的第2个参数作为输入,格式化后,输出数据到堆栈中去,当内容太长的时候,就会发生堆栈溢出。
现在分析被攻击的可能性。
1. 这个函数开始就检查文件句柄的合法性,如果没有办法打开 %windir%\debug\netsetup.log的话,则这个函数没有办法被执行。所以,当触发该服务器执行文件记录时,连接的账号如果没有权限打开该文件,则不能进行以后的攻击。除非,服务器权限设置不正确,或者是fat32的文件格式,没有办法进行权限限制。嘿嘿。。。
2. 输入的长度不长的时候,会发生堆栈溢出,只要在溢出点(大概是 0x818-12)的位置,填入 jmp esp的内容,然后,在下一个地址开始的地方,写入shellcode,就可以运行代码了。
3. 输入的长度很长的时候,会触发windows的结构化异常保护,如果数据足够大,把异常结构都覆盖了,也可以实现跳转,不过,这个时候就比较危险了,系统再也无法被第2次溢出,因为函数开始的时候,进入了临界区,而退出的时候,这种情况下,该临界变量没有被释放。
4. NetValidateName第2个参数的缓冲,输入的时候,是Unicode的,被vsprintf的时候,是 %ws,会被转换回ANSI字符串。这里调用的vsprintf是msvcrt.dll的同名函数,而非libc的标准库中的函数。这个vsprintf在转换的时候,并不是100%都能够转换,跟踪了一下,发现是调用 wctomb的函数来执行。到最后,即使能够被转换,最后输出可能也是0。标准的可见字符串是可以被转换的,但是,多字节语言的数据就没有那么好弄了。也就是说,执行的代码暂时只能限制在可见字符内,其它的,要看如何小心构造的。更多的细节,需要详细研究。( 结论就是:多语种通用的攻击代码,要实现,还有比较长的一段路要走。。。)
5. 输入的缓冲中,0x818-12-0x14-strlen(“NetpValidateName: checking to see if '”)的位置的数据,会被 WriteFile的参数改写,所以,这里要注意shellcode被破坏。
6. 这个攻击如果在shellcode执行完成以后,采用exitthread,则应该可以无限次被溢出。。。
总之,上面已经分析了不少东西了,写出具体的通用的攻击程序只是时间的问题。里面好像也没有更多的技术难点和技巧,没有什么好说了。希望已经实现了的大侠慎重公布攻击工具和代码,本文章只是从技术角度进行分析,这种”垃圾代码”其实还是比较容易防护的,只要把vsprintf转换成 vsnprintf就可以避免出现类似的问题!。
Eeye还有一个攻击方法,是 NetAddAlternateComputerName的攻击,针对NTFS的格式有效,我没有看,XP下可以,2K下没有这个函数,不知道XP能否攻击2K。。。有空再弄了。
*** 后记 ***
真不知道如何评价这个漏洞,微软的软件,这个SERVER和WORKSTATION的服务提供了很强大的服务功能,但是,偏偏存在那么严重的漏洞。。。。恐怖,自己写程序的时候还是要小心点,尤其是广为使用的程序。
SNAKE. 2003/11/17 morning.