一、 Windows下常用的溢出漏洞扫描方法
对于漏洞扫描,我们常用的方法有以下几种:
1.检测服务端口开放与否。这是最基本,也是准确率最低的一种方式。直接向目标机器的指定端口发送带有SYN标志的TCP数据包,以检测端口是否开放。如果端口开放,则认为漏洞存在,否则认为不存在漏洞。在无法通过其他方式进行扫描的时候可以采用这种方法,我在写CCProxy溢出的漏洞扫描代码的时候就是这样做的:
//连接目标机器808端口
SOCKET csock;
chock = socket(AF_INET,SOCK_STREAM,0);
sockaddr_in target;
Target.sin_family = AF_INET;
target.sin_addr.s_addr = dwDestIp;
target.sin_port = htons(808);
int ret = connect(csock, (sockaddr *)&target, sizeof(target));
if(ret == 0)
{
printf("CCProxy漏洞:/n 目标机无漏洞/n/n");
}
2.获取服务软件的Banner信息。我们通过客户端与服务软件进行连接的过程中,有时服务进程会返回相关的Banner信息,这些信息里面包含了服务软件的版本等内容。通过Banner信息来判断服务软件是否存在溢出漏洞是一种常用的方式,也有一定的准确率。因为很多软件升级之后以前的漏洞就不存在了,而升级之前的版本用户可能又疏于打漏洞补丁。Serv_U MDTM溢出漏洞的扫描技术的一个例子,Serv_U5.0.0.9及以下版本都有这样的漏洞,而更高的版本则不存在。该漏洞比较老了,其扫描代码我这里不在罗嗦,有兴趣的朋友可以和我联系。
3.直接爆破。将直接攻击作为扫描,如果攻击成功,则说明存在漏洞。假设你无路可走,不妨试试这一招。如果你只想作安全检测而不想破坏服务的话,最好不要采用这种方式,否则直接攻击可能导致目标服务死掉。大家可以想想,其实很多蠕虫不就是采用的这种方式吗?
4.根据安装补丁前后的响应报文的不同之处进行判断。这种方法最为安全、准确,实现难度也最大,要求分析者对漏洞自身的机制要很熟悉。很多存在漏洞的服务软件,在打补丁前后,我们发送应用层请求后,返回的报文内容是有细微的差别,而我们恰恰就是可以通过分析这一点点差别来进行判断溢出漏洞的存在。我在写DCOM RPC、SQL Resolution、LSA、PNP等多数漏洞的扫描都是这样实现的。大家可能要问了,这种方法的难点何在呢?其实难点就在于对特定服务的实现细节的理解和应用层通信协议(特别是SMB、RPC等协议)的分析。
这部分做的不好的话,是写不出有效的Client的。更多的时候我们很难清楚了解服务软件内部机制,特别是对于微软的一些服务软件。这种情况之下,我们可以拿到别人的未公布源代码的扫描工具,通过反汇编、跟踪分析网络数据包等方式逆向还原出工具的代码。大家还没有能力对系统底层进行深入分析的时候,更多的采用这种方式是明智的。只有知识一步步积累多了,我们才能做更加深入的分析。
好了,光是纸上谈兵是没用的,我们还是来看看两个实际的例子吧。
二、SQL 和PNP漏洞扫描代码实现
1.SQL Resolution溢出漏洞扫描代码的实现。SQL Server 2000 SP2及以前的版本中的 Resolution服务存在漏洞。如果向服务主机UDP 的1434端口发送以0x4开头的报文,则服务会返回当前运行的SQL SERVER实例。但如果发送的该报文长度过长,则发生堆栈溢出。此外,如果发送其他格式的报文,例如以0xa开头的请求,则服务进程会将该请求作为错误的请求报文处理。漏洞细节和利用我就不多说了,网上有现成的代码。这里我们来看看漏洞的扫描。通过拦截数据包并分析,我们发现在打补丁前后,向服务进程发送错误请求(0xa开头的)后,返回的报文内容是不一样的。如果仍然返回以0xa开头的报文,则证明是存在漏洞的,否则不存在漏洞。主要代码如下:
建立空连接:
//探测线程函数
UINT ListenThreadProc(LPVOID pPram)
{
Total_Param *tp;
tp = (Total_Param *)pPram;
SOCKADDR_IN Udpfrom;
int UdpfromLen = sizeof(Udpfrom);
//下面发送请求数据,以'/x0a'开头
tp->bufSend[0] = '/x0a';
int iError;
if(sendto(tp->sock, tp->bufSend, sizeof(tp->bufSend), 0, (sockaddr*) &tp->addr_in, sizeof(tp->addr_in))==SOCKET_ERROR)
{
iError = WSAGetLastError();
return 1;
}
//接收返回报文
while(true)
{
memset(tp->bufRecv, 0, sizeof(tp->bufRecv));
int n = recvfrom(tp->sock, tp->bufRecv, sizeof(tp->bufRecv), 0, (sockaddr *)&Udpfrom, &UdpfromLen);
if(n > 0 && tp->bufRecv[0] == '/x0a')
{
//printf("%s/n", tp->bufRecv);
tp->IsVulnerable = true;
break;
}
}
return 0;
}
这个漏洞的扫描代码实现起来很简单,大家可以直接提取上面的代码。
下面再看一个复杂一点的漏洞——Windows 2000的PNP溢出漏洞(MS05--039)。Windows 2000及以上版本的PNP(Plug and Play)服务是通过IPC$命名管道+RPC接口的形式向外提供服务的,也就是说,我们要访问远程机器上的PNP服务,首先要通过TCP 139或445端口与目标机器建立IPC空连接,进而获取PNP服务进程中创建的命名管道“Browser”的客户端句柄。获取了管道句柄后,再通过该命名管道与PNP服务进程中的RPC接口进行通信,完成对服务的访问。在对PNP漏洞进行攻击的时候,我们是采用这样一个通信流程,在漏洞扫描的时候我们也可以按照这样一个步骤。由于我对SMB和RPC协议暂时还没有完全分析清楚,所以填充的应用层数据包也是直接从网络数据包中截获而来,而这些数据包呢,正是在分析MACFEE公布的扫描工具时候得到的。通过分析,发现打补丁前后返回数据报内容刚好存在一个字节的差别,呵呵,还原漏洞扫描代码就易如反掌了。下面给出部分代码:
建立空连接:
SOCKET ClientSock;
sockaddr_in ClientAddr;
int len;
unsigned char szRecv[0x1000] = {0};
ClientSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ClientAddr.sin_family = AF_INET;
ClientAddr.sin_port = htons(445);
ClientAddr.sin_addr.S_un.S_addr = inet_addr(argv[1]);
printf("[*] Connecting to 445 ... ");
//先连接445端口,如果不成功,则连接139
if(connect(ClientSock, (const sockaddr *)&ClientAddr, sizeof(ClientAddr)) < 0)
{
printf("Cann't connecting to 445 ... ");
printf("[*] Connecting to 445 ... ");
ClientAddr.sin_port = htons(139);
if(connect(ClientSock, (const sockaddr *)&ClientAddr, sizeof(ClientAddr)) < 0)
{
printf("Cann't create NULL session!/n");
return -1;
}
//发送72字节的reqNBSS
if(send(ClientSock, (const char *)reqNBSS, sizeof(reqNBSS) - 1, 0) < 0)
{
printf("[-] Send reqNBSS failed/n");
closesocket(ClientSock);
return -1;
}
len = recv(ClientSock, (char *)szRecv, sizeof(szRecv) - 1, 0);
if(len < 0)
{
printf("2/n");
closesocket(ClientSock);
return -1;
}
}
//发送SMB_Negotiate
if (send(ClientSock, (const char *)SMB_Negotiate, sizeof(SMB_Negotiate) - 1, 0) < 0)
{
printf("/n[-] send SMB_Negotiate failed/n");
return -1;
}
len = recv(ClientSock, (char *)szRecv, sizeof(szRecv) - 1, 0);
if ((len <= 10) || (szRecv[9] != 0))
{
printf("3/n");
return -1;
}
//发送SMB_SessionSetupAndX
if(send(ClientSock, (const char *)SMB_SessionSetupAndX, sizeof(SMB_SessionSetupAndX)-1, 0) < 0)
{
printf("/n[-] send SMB_SessionSetupAndX failed/n");
return -1;
}
len = recv(ClientSock, (char *)szRecv, 4096, 0);
if (len <= 10 || (szRecv[9] != 0))
{
printf("4/n");
exit(0);
}
//发送SMB_TreeConnectAndX
if(send(ClientSock, (const char *)SMB_TreeConnectAndX, sizeof(SMB_TreeConnectAndX), 0) < 0)
{
printf("/n[-] send failed/n");
return -1;
}
len = recv(ClientSock, (char *)szRecv, 4096, 0);
if ((len <= 10) || (szRecv[9] != 0))
{
printf("6/n");
return -1;
}
这里,我将建立IPC空连接的过程按照SMB协议分别写出,而没有直接用WNetAddConnection2函数的原因有两个:1是WNetAddConnection2函数只连接445端口,如果连接失败不再尝试连接139端口。2是我发现用WNetAddConnection2建立空连接后,代码在访问命名管道的时候会出错。
获取管道客户端句柄:
char szPipe[MAX_PATH];
HANDLE hFile;
//打开命名管道“//server/pipe/browser”客户端句柄
_snprintf(szPipe, sizeof(szPipe), "%s//pipe//browser", argv[1]);
hFile = CreateFile(szPipe, GENERIC_READ|GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);
if(hFile == (HANDLE)(-1))
{
printf("cann't find named pipe/n");
return -1;
}
接下来就是访问RPC接口了,RPC接口是需要先Bind后发送请求的。
//BIND rpc接口
if(!BindRpcInterface(hFile, "8d9f4e40-a03d-11ce-8f69-08003e30051b", "1.0"))
{
printf("can't get namedpipe! %d/n", GetLastError());
return -1;
}
BindRpcInterface函数实现为:
BOOL BindRpcInterface(HANDLE PH, char *Interface, char *InterfaceVer)
{
BYTE rbuf[0x1000];
DWORD dw;
struct RPCBIND RPCBind;
BOOL bRet;
memcpy(&RPCBind, &PRPC, sizeof(RPCBind));
UuidFromString((unsigned char *)Interface, &RPCBind.InterfaceUUID);
UuidToString(&RPCBind.InterfaceUUID, (unsigned char **)&Interface);
RPCBind.InterfaceVerMaj = atoi(&InterfaceVer[0]);
RPCBind.InterfaceVerMin = atoi(&InterfaceVer[2]);
bRet = TransactNamedPipe(PH, &RPCBind, sizeof(RPCBind), rbuf, sizeof(rbuf), &dw, NULL);
return bRet;
}
最后进行漏洞判断:
//发送请求RPC request包
BOOL ret = ScanPNP(hFile);
ScanPNP实现细节:
BYTE rbuf[0x100] = {0};
DWORD dw;
if(!TransactNamedPipe(PipeHandle, szScan, sizeof(szScan), rbuf, sizeof(rbuf), &dw, NULL))
{
printf("Cann't get scan data!/n");
return FALSE;
}
if(dw != 0x48)
{
return FALSE;
}
if(rbuf[0x44] != 0x0b) //大家看看,就这一个自己的差别
{
return FALSE;
}
return TRUE;
代码中用到的SMB和RPC协议数据,以及完整的扫描代码我会和文章附在一起,大家可以看看。