随着安全事件的不断涌现,人们的主机防护意识越来越强,各种各样的防火墙和反病毒软件都开始对来自外部的网络连接进行监控,所以传统的采用正向连接的木马已经不再适应现在的网络环境了。为了能够继续进行远程控制,木马不得不从体制上进行改变,这就出现了采用反向连接技术的“反弹型”木马。“反弹”木马现在已经见得比较多了,但是对其技术细节介绍的文章还是比较少。
从本质上来说,反向连接和正向连接的区别并不大。在正向连接的情况下,服务器端也就是被控制端,在编程实现的时候是采用服务器端的编程方法的;而控制端,在编程实现的时候采用的是客户端的编程方法。当我们采用反弹的方法编程的时候,实际上就是将被控制端变成了采用客户端的编程方法,而将控制端变成了采用服务器端的编程方法(Socket:不知道这样解释读者朋友们能不能听明白?反正俺是觉得云里雾里)。还是通过简单的编程实例来解释一下吧,免得有人。虽然下面我给出的例子比较小,但是却很有代表性,读者可以从中体会到反向连接技术的实现方法。同时,对于不熟悉网络编程的读者,也是一次促进学习的机会,可以通过这个例子来熟悉一下网络编程。
以反向连接为例,我们先来看看控制端的编程实现方法。
通过上面的介绍,我们已经知道了,控制端应该采用服务器端编程的方法来实现。要用Winsock编程,不要忘记了包含相应的头文件,并且要加载相应的库文件,如果你使用的是VC6.0的工具,可以不用在这里使用#pragma选项,而是可以在[工程]——[设置]——[link]——[对象/库模块]里面加上ws2_32.lib文件就可以了。当然了,你也可以使用下面的#pragma选项。
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
定义使用的端口和对应于通信端口的缓冲区,同时设置一些下面将要用到的变量。
#define DEFAULT_PORT 5150
#define DEFAULT_BUFFER 4096
int iPort = DEFAULT_PORT; // 监听端口
BOOL bInterface = FALSE,
bRecvOnly = FALSE; // 不返回时间
char szAddress[128];
定义一个函数,用来提示用户本程序的使用方法。
void usage()
{
printf("usage: server [-p:x] [-i:IP] [-o]\n\n");
printf(" -p:x Port number to listen on\n");
printf(" -i:str Interface to listen on\n");
printf(" -o Don't echo the data back\n\n");
ExitProcess(1);
}
下面这个函数用来测试用户输入的参数的正确性,如果用户输入了非法的参数,那个就要提示用户输入正确的使用方法的提示。
void ValidateArgs(int argc, char **argv)
{
int i;
for(i = 1; i < argc; i++)
{
if ((argv[i][0] == '-') || (argv[i][0] == '/'))
{
switch (tolower(argv[i][1])) //为了便于下面的比较,使用tolower()函数将参数中的指定标识转换为小写。
{
case 'p':
iPort = atoi(&argv[i][3]); //atoi()函数的作用:将字符串转换为整数
break;
case 'i':
bInterface = TRUE;
if (strlen(argv[i]) > 3)
strcpy(szAddress, &argv[i][3]);
break;
case 'o':
bRecvOnly = TRUE;
break;
default:
usage();
break;
}
}
}
}
定义一个主函数,让这个主函数实现定义套接字,绑定套接字,然后在特定套接字上进行监听,如果听到了信息,就进入一个循环将消息读出来。对消息进行操作的过程最好开辟一个线程来专门处理,否则的话就会把消息队列阻塞。
我们一起来顺着这个程序的流程来解释一下。首先来看Main()函数的开始部分,定义了一些参数。
int main(int argc, char **argv)
{
WSADATA wsd;
SOCKET sListen, sClient;
int iAddrSize;
HANDLE hThread;
DWORD dwThreadId;
struct sockaddr_in local, client;
接下来,在主程序中,我们要来验证一下输入参数的格式是否正确。
ValidateArgs(argc, argv); //验证输入的参数是否正确
下面的WSAStartup函数的调用是为了加载WS2_32.lib库文件,只有当这个库被加载之后才能够使用一些通信的API函数。
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
{
printf("Failed to load Winsock!\n");
return 1;
}
下面使用Socket函数创建一个流套接字,使用面向连接的协议TCP进行通信。如果用户要编写UDP通信的程序,这里的第二个参数就要使用SOCK_DGRAM,而第三个参数也要对应改为IPPROTO_UDP。有的时候可能也会用到原始套接字编程,这时其对应的第二和第三个参数应该写为SOCK_RAW和IPPROTO_RAW。这里将创建的Socket赋值给SListen,就是创建了一个服务端的套接字。后面的所有的通信操作全都对应着这个套接字进行。
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (sListen == SOCKET_ERROR)
{
printf("socket() failed: %d\n", WSAGetLastError());
return 1;
}
对地址结构Local的参数进行赋值,这里首先要判断一下在调用服务端程序的时候是否输入了IP地址。如果用户没有输入IP地址,那么就会调用Usage()函数,提示用户按照正确的格式进行输入。
if (bInterface)
{
local.sin_addr.s_addr = inet_addr(szAddress);
if (local.sin_addr.s_addr == INADDR_NONE)
usage();
}
else
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(iPort);
既有了套接字,又有了地址结构,下面就要将这两个结合在一起,这就要用到Bind()函数。
if (bind(sListen, (struct sockaddr *)&local, sizeof(local)) == SOCKET_ERROR)
{
printf("bind() failed: %d\n", WSAGetLastError());
return 1;
}
小知识:Bind()的第一个参数是服务端的套接字,第二个参数是服务端的地址结构,第三个参数是地址结构的大小。
一切都准备好了,下面开始监听。这里使用Listen函数对SListen套接字进行监听,第二个参数指定可以同时处理的连接的个数,这里指定这个参数为8。
listen(sListen, 8);
进入了监听之后,就要进入While循环来等待要求通信的客户端的请求了,这就要用到Accept函数,一旦Accept函数检测到有连接请求,就要进行处理。
小提示:这里要采用多线程的技术处理每个通信请求,否则就会每次只能处理一个请求,其它的请求就会被阻塞掉,这显然不是我们所希望的。while (1)
{
iAddrSize = sizeof(client);
sClient = accept(sListen, (struct sockaddr *)&client, &iAddrSize);
if (sClient == INVALID_SOCKET)
{
printf("accept() failed: %d\n", WSAGetLastError());
break;
}
printf("Accepted client: %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
hThread = CreateThread(NULL, 0, ClientThread, (LPVOID)sClient, 0, &dwThreadId);
if (hThread == NULL)
{
printf("CreateThread() failed: %d\n", GetLastError());
break;
}
CloseHandle(hThread);
}
如果循环因为某种原因退出了,那么要将套接字关闭,然后调用WSACleanup()函数来卸载掉对Ws2_32.lib的调用。
closesocket(sListen);
WSACleanup();
return 0;
}
上面这个主程序中,我们采用了同时对8个通信进行监听,这就要涉及到多线程的实现技术,那么线程函数就要参照如下的方法来实现。在下面的这个线程函数里面,我们对收到的请求,要调用Recv()和Send()进行数据的收发。下面我们一起来看一看吧。
在前面的主函数里面使用了这条语句CreateThread(NULL, 0, ClientThread, (LPVOID)sClient, 0, &dwThreadId),这个函数中的第三个参数为线程处理函数的地址,第四个参数为发送请求的客户端,这个参数会被传给线程处理函数的LpParam参数,然后就可以在线程处理函数中通过LpParam来使用客户端的参数。这里我们仅仅是做一个简单的测试,所以不准备收到很大的消息,我们收到消息之后就直接将它打印出来。不过在Recv函数返回之后,要对返回值Ret进行判断:如果Ret的值为零,我们就直接跳出循环结束。如果Ret等于SOCKET_ERROR,那么就打印出错误消息。如果不是这两种情况,说明接收到了消息,那么就在接收消息的缓冲区的末尾加上字符串结束符,然后将这个消息以字符串的形式打印出来。另外,如果我们设定了BRecvOnly为假,那么这时候,我们在接收到消息之后还要做一个发送消息的测试。这里就要用到了Send函数,我们可以就把刚才接收到的消息发送回去。小知识:Send函数的结构如下:
int send (SOCKET s,const char FAR * buf,int len,int flags );
这个函数中的第三个参数是存放消息的缓冲区中的消息的长度,函数的返回值为此次发送消息的长度。每次传送完毕都要对Ret的值进行判断,如果Ret的值等于零就说明已经传送完毕,如果等于SOCKET_ERROR,说明传送出了错误,如果不是这两种情况,说明此次传送了数据,那么我们要相应的将NLeft减去Ret,并且将Idx加上Ret,这样就可以从未传送的位置开始发送数据。DWORD WINAPI ClientThread(LPVOID lpParam)
{
SOCKET sock=(SOCKET)lpParam;
char szBuff[DEFAULT_BUFFER];
int ret, nLeft, idx;
while(1)
{
ret = recv(sock, szBuff, DEFAULT_BUFFER, 0);
if (ret == 0) // Graceful close
break;
else if (ret == SOCKET_ERROR)
{
printf("recv() failed: %d\n", WSAGetLastError());
break;
}
szBuff[ret] = '\0';
printf("RECV: '%s'\n", szBuff);
if (!bRecvOnly)
{
nLeft = ret;
idx = 0;
while(nLeft > 0)
{
ret = send(sock, &szBuff[idx], nLeft, 0);
if (ret == 0)
break;
else if (ret == SOCKET_ERROR)
{
printf("send() failed: %d\n",
WSAGetLastError());
break;
}
nLeft -= ret;
idx += ret;
}
}
}
return 0;
}
下面我们来看一下客户端程序的编写。
首先,我们要定义一些常量,在程序中会使用到的,如端口号、要发送的消息之类。
#define DEFAULT_COUNT 20
#define DEFAULT_PORT 5150
#define DEFAULT_BUFFER 2048
#define DEFAULT_MESSAGE "This is a test of the emergency broadcasting system"
接下来,定义一些全局变量,供程序中使用。
char szServer[128], // Server to connect to
szMessage[1024]; // Message to send to sever
int iPort = DEFAULT_PORT; // Port on server to connect to
DWORD dwCount = DEFAULT_COUNT; // Number of times to send message
BOOL bSendOnly = FALSE; // Send data only; don't receive
下面,我们一起从主函数来看看客户端的实现。我省略了客户端的Usage()函数和Void ValidateArgs(int argc, char **argv)函数的解析,因为我们已经在上面写得比较多了,这里就不再占用过多的篇幅了。另外,客户端程序中跟服务端编程中用到的重复的东西,我也不再过多的解释了。不过,为了保持主函数的完整性,我还是尽量将这些部分放在了文章中,也方便读者阅读。
int main(int argc, char **argv)
{
WSADATA wsd;
//声明一个客户端的套接字
SOCKET sClient;
char szBuffer[DEFAULT_BUFFER];
int ret, i;
//声明服务器端的地址结构
struct sockaddr_in server;
struct hostent *host = NULL;
//验证输入参数的合法性
ValidateArgs(argc, argv);
//加载对WS2_32.lib库文件的调用
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
{
printf("Failed to load Winsock library!\n");
return 1;
}
//将默认的消息常量拷贝到szMessage缓冲区中
strcpy(szMessage, DEFAULT_MESSAGE);
//定义一个TCP类型的客户端的套接字
sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sClient == INVALID_SOCKET)
{
printf("socket() failed: %d\n", WSAGetLastError());
return 1;
}
//对server地址结构里面的成员进行赋值
server.sin_family = AF_INET;
server.sin_port = htons(iPort);
server.sin_addr.s_addr = inet_addr(szServer);
if (server.sin_addr.s_addr == INADDR_NONE)
{
//如果用户输入的是服务端的全限定域名,那么就要使用gethostbyname来进行解析,从而得到对方的IP地址。
host = gethostbyname(szServer);
if (host == NULL)
{
printf("Unable to resolve server: %s\n", szServer);
return 1;
}
//对Server地址结构进行赋值
CopyMemory(&server.sin_addr, host->h_addr_list[0], host->h_length);
}
//向客户端发出连接请求,Connect函数的第一个参数是发出请求的客户端的套接字,第二个参数是服务端的地址结构,第三个参数是Server地址结构的长度。
if (connect(sClient, (struct sockaddr *)&server, sizeof(server)) == SOCKET_ERROR)
{
printf("connect() failed: %d\n", WSAGetLastError());
return 1;
}
//使用一个循环将缓冲区中的内容分段发送出去
for(i = 0; i < dwCount; i++)
{
ret = send(sClient, szMessage, strlen(szMessage), 0);
if (ret == 0)
break;
else if (ret == SOCKET_ERROR)
{
printf("send() failed: %d\n", WSAGetLastError());
break;
}
printf("Send %d bytes\n", ret);
//如果bSendOnly为False,那么我们还要通过一个小的测试程序接收服务端的消息,这里的处理方法跟上面服务端的程序编写思路基本上一致。
if (!bSendOnly)
{
ret = recv(sClient, szBuffer, DEFAULT_BUFFER, 0);
if (ret == 0) // Graceful close
break;
else if (ret == SOCKET_ERROR)
{
printf("recv() failed: %d\n", WSAGetLastError());
break;
}
//在缓冲区的末尾加上字符串结束符,并将接收到的消息打印出来。
szBuffer[ret] = '\0';
printf("RECV [%d bytes]: '%s'\n", ret, szBuffer);
}
}
//通信结束,关闭套接字
closesocket(sClient);
//清理对WS2_32.lib的加载
WSACleanup();
return 0;
}
到这里为止,基本上把反向连接通信的服务端和客户端的实现方法介绍清楚了。不过,你需要注意的就是,反向连接中,服务端实际上是处在控制端,而客户端处在被控制端,这一点就是反向与正向的本质区别。同时你也可以看到,采用了这种编程方法,实际上控制端如果控制的好,可以利用服务端可以同时处理的客户端请求的数量这一设置,来实现一对多的控制,也就是说,可以让多个被控制端同时来连接一个控制端,让一个控制端可以同时来控制多个控制端。这是一种新的控制思路,对于那些想在一台机器上同时控制多个机器的用户来说,应该是一个利好消息。
作者在 2009-03-08 12:31:07 发布以下内容