C++ 黑客编程揭秘与防范(第3版)
上QQ阅读APP看书,第一时间看更新

2.3.2 非阻塞模式下简单远程控制的开发

在了解如何将套接字设置为非阻塞模式以后,这里完成一个简单的远程控制工具。这里要编写的远程控制工具是基于C/S模式的,即客户端/服务器端模式的架构。客户端通过发送控制命令,操作服务器端接收到控制命令后响应相应的事件,完成特定的功能。

这个远程控制的服务器端只简单实现以下几个功能。

· 向客户端发送帮助信息。

· 将服务器信息发送给客户端。

· 交换鼠标的左右键和恢复鼠标的左右键。

· 打开光驱和关闭光驱。

1.远程控制软件框架设计

远程控制分为控制端和被控制端,控制端通常为客户端,而被控制端通常为服务器端。对于客户端来说,它需要3种通知码,即FD_CONNECT、FD_CLOSE和FD_READ。对于服务器端来说,它需要3种通知码,即FD_ACCEPT、FD_CLOSE和FD_READ,如图2-12所示。

图2-12 服务器端和客户端通信

这里解释一下图2-12,并对它的框架设计进行补充。对于服务器端(Server端)来说,它需要处于监听状态等待客户端(Client端)发起的连接(FD_ACCEPT),在连接后会等待接收客户端发来的控制命令(FD_READ),当客户端断开连接后就可以结束此次通信了(FD_CLOSE)。对于客户端来说,它需要等待确认连接是否成功(FD_CONNET);当连接成功后就可以向服务器端发送控制命令,并等待接收命令响应结果(FD_READ);当服务器端被关闭后,通信则强制被结束了(FD_CLOSE)。因此,服务器端需要的通知码有FD_ACCEPT、FD_READ和FD_CLOSE,客户端需要的通知码有FD_CONNECT、FD_READ和FD_CLOSE。

客户端向服务器端发送的命令为“字符串”类型的数据。当服务器接收到客户端发来的命令后,需要判断命令,然后执行相应的功能。

服务器向客户端反馈的执行结果可能为字符串,也可能为其他的数据结构类型的内容。由于反馈数据的格式无法确定,那么对于服务器向客户端反馈的信息必须做特殊的标记,通过标记判断发送的数据格式。而客户端接收到服务器端发来的数据后,必须对格式进行解析,以便正确读取服务器端返回的命令反馈结果。服务器端的反馈数据协议格式如图2-13所示。

图2-13 服务器端反馈数据协议格式

从图2-13可以看出,服务器对于客户端的反馈数据协议格式有3部分内容,第1部分bType用于区分是文本数据和特定数据结构的数据,第2部分bClass用于区分不同的特定数据结构,第3部分szValue是真正的数据部分。对于服务器反馈的数据,如果是文本数据,那么客户端直接将szValue中的字符串显示输出;如果反馈的是特定的数据结构,则必须区分是何种数据结构,最后按照直接的数据结构解析szValue中的数据。将该协议格式定义为数据结构体,如下:

        #define TEXTMSG      't'    // 表示文本信息
        #define BINARYMSG    'b'    // 表示特定的数据结构

        typedef struct _DATA_MSG
        {
            BYTE bType;              // 数据的类型
            BYTE bClass;             // 数据类型的补充
            char szValue[0x200];    // 数据的信息
        }DATA_MSG, *PDATA_MSG;

2.远程控制软件代码要点

本节的最开始介绍了WSAAsyncSelect()函数原型和参数的含义,现在来具体介绍如何使用WSAAsyncSelect()函数的使用。WSAAsyncSelect()函数在使用时会将指定的套接字、窗口句柄、自定义消息和通知码关联在一起,使用如下:

        // 初始化Winsock库
        WSADATA wsaData;
        WSAStartup(MAKEWORD(2, 2), &wsaData);

        // 创建套接字并将其设置为非阻塞模式
        m_ListenSock=socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        WSAAsyncSelect(m_ListenSock, GetSafeHwnd(), UM_SERVER, FD_ACCEPT);

在代码的WSAAsyncSelect()函数中,第1个参数是新创建的用于监听的套接字m_ListenSock,第2个参数使用MFC的成员函数GetSafeHwnd()来得到当前窗体的句柄,第3个参数UM_SERVER是一个自定义的类型,最后一个参数FD_ACCEPT是该套接字要接收的通知码。函数中的第3个参数是一个自定义的消息。在服务器端,该消息的定义如下:

        #define UM_SERVER    (WM_USER + 200)

当有客户端与服务器端连接时,系统会发送UM_SERVER消息到与监听套接字关联的句柄指定的窗口。当窗口收到该消息后,需要对该消息进行处理。该处理函数也需要手动进行添加,添加有3处地方。

第1处是在类定义中添加,代码如下:

        // 生成的消息映射函数
        //{{AFX_MSG(CServerDlg)
        virtual BOOL OnInitDialog();
        afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
        afx_msg void OnPaint();
        afx_msg HCURSOR OnQueryDragIcon();
        afx_msg VOID OnSock(WPARAM wParam, LPARAM lParam);
        afx_msg void OnClose();
        //}}AFX_MSG
        DECLARE_MESSAGE_MAP()

在这里添加afx_msg VOID OnSock(WPARAM wParam, LPARAM lParam);

第2处在类实现中添加对应的函数实现代码,如下:

        VOID CServerDlg::OnSock(WPARAM wParam, LPARAM lParam)
        {

        }

第3处是要添加消息映射,代码如下:

        BEGIN_MESSAGE_MAP(CServerDlg, CDialog)
        //{{AFX_MSG_MAP(CServerDlg)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()
        ON_MESSAGE(UM_SERVER, OnSock)
        ON_WM_CLOSE()
        //}}AFX_MSG_MAP
        END_MESSAGE_MAP()

在这里添加ON_MESSAGE(UM_SERVER, OnSock)。

通过以上3步,在程序中就可以接收并响应对UM_SERVER消息的处理。

3.远程控制界面布局

首先来看远程控制客户端与服务器端的窗口界面,如图2-14所示。

图2-14 远程控制端与服务器端界面布局

在图2-14中,SERVER表示服务器端,Client表示客户端。服务器端(Server)运行在虚拟机中,客户端(Client)运行在物理机中。通过图2-14可以看出,物理机中客户端与服务器端是可以正常进行通信的。

服务器端的软件只有一个用于显示多行文本的编辑框。该界面比较简单。

客户端软件在IP地址后的编辑框中输入服务器端的IP地址,然后单击“连接”按钮,客户端会与远端的服务器进行连接。当连接成功后,输入IP地址的编辑框会处于只读状态,“连接”按钮变为“断开连接”按钮。对于发送命令后的编辑框变为可用状态,“发送”按钮也变为可用状态。

对于软件界面的布局,读者可以自行调整。

4.服务器端代码的实现

当服务器启动时,需要创建套接字,并将套接字设置为异步模式,绑定IP地址和端口号并使其处于监听状态,代码如下:

          BOOL CServerDlg::OnInitDialog()
          {
              ……
              // 添加其他初始化代码
              // 初始化Winsock库
              WSADATA wsaData;
              WSAStartup(MAKEWORD(2, 2), &wsaData);

              // 创建套接字并将其设置为非阻塞模式
              m_ListenSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
              WSAAsyncSelect(m_ListenSock, GetSafeHwnd(), UM_SERVER, FD_ACCEPT);

              sockaddr_in addr;
              addr.sin_family = AF_INET;
              addr.sin_addr.S_un.S_addr = ADDR_ANY;
              addr.sin_port = htons(5555);

              // 绑定IP地址及5555端口,并处于监听状态
              bind(m_ListenSock, (SOCKADDR*)&addr, sizeof(addr));
              listen(m_ListenSock, 1);

              return TRUE;  // return TRUE  unless you set the focus to a control
          }

当客户端与服务器端进行连接时,需要处理通知码FD_ACCEPT,并且创建与客户端进行通信的新的套接字。对于新的套接字也需要设置为异步模式,并且需要设置FD_READ和FD_CLOSE两个通知码。代码如下:

          VOID CServerDlg::OnSock(WPARAM wParam, LPARAM lParam)
          {
              if ( WSAGETSELECTERROR(lParam) )
              {
                  return ;
              }

              switch ( WSAGETSELECTEVENT(lParam))
              {
                  // 处理FD_ACCEPT
              case FD_ACCEPT:
                  {
                      sockaddr_in ClientAddr;
                      int nSize = sizeof(ClientAddr);

                      m_ClientSock = accept(m_ListenSock, (SOCKADDR*)&ClientAddr, &nSize);
                      WSAAsyncSelect(m_ClientSock, GetSafeHwnd(), UM_SERVER, FD_READ | FD_CLOSE);
                      m_StrMsg.Format("请求地址是%s:%d",
                                inet_ntoa(ClientAddr.sin_addr), ntohs(ClientAddr.sin_port));

                      DATA_MSG DataMsg;
                      DataMsg.bType = TEXTMSG;
                      DataMsg.bClass = 0;
                      lstrcpy(DataMsg.szValue, HELPMSG);
                      send(m_ClientSock, (const char *)&DataMsg, sizeof(DataMsg), 0);

                      break;
                  }
                  // 处理FD_READ
              case FD_READ:
                  {
                      char szBuf[MAXBYTE] = { 0 };
                      recv(m_ClientSock, szBuf, MAXBYTE, 0);
                      DispatchMsg(szBuf);
                      m_StrMsg = "对方发来命令: ";
                      m_StrMsg += szBuf;
                      break;
                  }
                  // 处理FD_CLOSE
              case FD_CLOSE:
                  {
                      closesocket(m_ClientSock);
                      m_StrMsg = "对方关闭连接";
                      break;
                  }
              }

              InsertMsg();
          }

在代码中,当响应FD_READ通知码时会接收客户端发来的命令,并通过DispatchMsg()函数处理客户端发来的命令。在OnSock()函数的最后有一个InsertMsg()函数,该函数用于将接收的命令显示到界面上对应的消息编辑框中。

DispatchMsg()函数用于处理客户端发来的命令,该代码如下:

        VOID CServerDlg::DispatchMsg(char *szBuf)
        {
            DATA_MSG DataMsg;
            ZeroMemory((void*)&DataMsg, sizeof(DataMsg));

            if ( !strcmp(szBuf, "help") )
            {
                DataMsg.bType = TEXTMSG;
                DataMsg.bClass = 0;
                lstrcpy(DataMsg.szValue, HELPMSG);
            }
            else if ( !strcmp(szBuf, "getsysinfo"))
            {
                SYS_INFO SysInfo;
                GetSysInfo(&SysInfo);
                DataMsg.bType = BINARYMSG;
                DataMsg.bClass = SYSINFO;
                memcpy((void *)DataMsg.szValue, (const char *)&SysInfo, sizeof(DataMsg));
            }
            else if ( !strcmp(szBuf, "open") )
            {
                SetCdaudio(TRUE);
                DataMsg.bType = TEXTMSG;
                DataMsg.bClass = 0;
                lstrcpy(DataMsg.szValue, "open命令执行完成");
            }
            else if ( !strcmp(szBuf, "close") )
            {
                  SetCdaudio(FALSE);
                  DataMsg.bType = TEXTMSG;
                  DataMsg.bClass = 0;
                  lstrcpy(DataMsg.szValue, "close命令执行完成");
              }
              else if ( !strcmp(szBuf, "swap") )
              {
                  SetMouseButton(TRUE);
                  DataMsg.bType = TEXTMSG;
                  DataMsg.bClass = 0;
                  lstrcpy(DataMsg.szValue, "swap命令执行完成");
              }
              else if ( !strcmp(szBuf, "restore") )
              {
                  SetMouseButton(FALSE);
                  DataMsg.bType = TEXTMSG;
                  DataMsg.bClass = 0;
                  lstrcpy(DataMsg.szValue, "restore命令执行完成");
              }
              else
              {
                  DataMsg.bType = TEXTMSG;
                  DataMsg.bClass = 0;
                  lstrcpy(DataMsg.szValue, "无效的指令");
              }
              // 发送命令执行情况给客户端
              send(m_ClientSock, (const char *)&DataMsg, sizeof(DataMsg), 0);
          }

在DispatchMsg()函数中,通过if()…else if()…else()比较客户端发来的命令执行相应的功能,并将执行的结果发送给客户端。

命令功能的实现函数如下:

        VOID CServerDlg::GetSysInfo(PSYS_INFO SysInfo)
        {
            unsigned long nSize = 0;

            SysInfo->OsVer.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
            GetVersionEx(&SysInfo->OsVer);
            nSize = NAME_LEN;
            GetComputerName(SysInfo->szComputerName, &nSize);
            nSize = NAME_LEN;
            GetUserName(SysInfo->szUserName, &nSize);
        }

        VOID CServerDlg::SetCdaudio(BOOL bOpen)
        {
            if ( bOpen )
            {
                // 打开光驱
                mciSendString("set cdaudio door open", NULL, NULL, NULL);
            }
            else
            {
                // 关闭光驱
                mciSendString("set cdaudio door closed", NULL, NULL, NULL);
            }
        }

        VOID CServerDlg::SetMouseButton(BOOL bSwap)
        {
            if ( bSwap)
            {
                // 交换
                SwapMouseButton(TRUE);
            }
              else
              {
                  // 恢复
                  SwapMouseButton(FALSE);
              }
          }

这里面对于getsysinfo命令,需要定义一个结构体,具体如下:

        #define HELPMSG "帮助信息: \r\n" \
                      "\t help          : 显示帮助菜单 \r\n" \
                      "\t getsysinfo  : 获得对方主机信息\r\n" \
                      "\t open          : 打开光驱 \r\n" \
                      "\t close        : 关闭光驱 \r\n" \
                      "\t swap          : 交换鼠标左右键 \r\n" \
                      "\t restore      : 恢复鼠标左右键" \

        #define NAME_LEN 20

        typedef struct _SYS_INFO
        {
            OSVERSIONINFO OsVer;                // 保存操作系统信息
            char szComputerName[NAME_LEN];    // 保存计算机名
            char szUserName[NAME_LEN];        // 保存当前登录名
        }SYS_INFO, *PSYS_INFO;

该结构体不是文本类型的数据,需要在反馈协议中填充bClass字段。对于getsysinfo命令,该bClass字段填充的内容为“SYSINFO”。SYSINFO的定义如下:

        #define SYSINFO  0x01L

调用mciSendString()函数需要添加头文件和库文件,具体如下:

        #include <mmsystem.h>
        #pragma comment (lib, "Winmm")

至此,服务器端的主要功能就介绍完了,最后还有两个函数没有列出,分别是InsertMsg()函数和释放Winsock库的部分,代码如下:

        void CServerDlg::OnClose()
        {
            // 添加处理程序代码或调用默认方法
            // 关闭监听套接字,并释放Winsock库
            closesocket(m_ClientSock);
            closesocket(m_ListenSock);
            WSACleanup();

            CDialog::OnClose();
        }

        VOID CServerDlg::InsertMsg()
        {
            CString strMsg;
            GetDlgItemText(IDC_MSG, strMsg);

            m_StrMsg += "\r\n";
            m_StrMsg += "----------------------------------------\r\n";
            m_StrMsg += strMsg;
            SetDlgItemText(IDC_MSG, m_StrMsg);
            m_StrMsg = "";
        }

5.客户端代码的实现

客户端的代码基本与服务端的代码类似,这里就不再说明。

连接远程服务器的代码如下:

        void CClientDlg::OnBtnConnect()
        {
        // 添加处理程序代码
        char szBtnName[10] = { 0 };
        GetDlgItemText(IDC_BTN_CONNECT, szBtnName, 10);

        // 断开连接
        if ( !lstrcmp(szBtnName, "断开连接") )
        {
            SetDlgItemText(IDC_BTN_CONNECT, "连接");
            (GetDlgItem(IDC_SZCMD))->EnableWindow(FALSE);
            (GetDlgItem(IDC_BTN_SEND))->EnableWindow(FALSE);
            (GetDlgItem(IDC_IPADDR))->EnableWindow(TRUE);
            closesocket(m_Socket);
            m_StrMsg = "主动断开连接";
            InsertMsg();
            return ;
        }

        // 连接远程服务器端
        char szIpAddr[MAXBYTE] = { 0 };
        GetDlgItemText(IDC_IPADDR, szIpAddr, MAXBYTE);

        m_Socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
        WSAAsyncSelect(m_Socket,GetSafeHwnd(),UM_CLIENT, FD_READ | FD_CONNECT | FD_CLOSE);

        sockaddr_in ServerAddr;
        ServerAddr.sin_family = AF_INET;
        ServerAddr.sin_addr.S_un.S_addr = inet_addr(szIpAddr);
        ServerAddr.sin_port = htons(5555);

        connect(m_Socket, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
    }

响应通知码的函数如下:

    VOID CClientDlg::OnSock(WPARAM wParam, LPARAM lParam)
    {
        if ( WSAGETSELECTERROR(lParam) )
        {
            return ;
        }

        switch ( WSAGETSELECTEVENT(lParam))
        {
            // 处理FD_ACCEPT
        case FD_CONNECT:
            {
                (GetDlgItem(IDC_SZCMD))->EnableWindow(TRUE);
                (GetDlgItem(IDC_BTN_SEND))->EnableWindow(TRUE);
                (GetDlgItem(IDC_IPADDR))->EnableWindow(FALSE);

                SetDlgItemText(IDC_BTN_CONNECT, "断开连接");
                m_StrMsg = "连接成功";
                break;
            }
            // 处理FD_READ
        case FD_READ:
            {
                DATA_MSG DataMsg;
                recv(m_Socket, (char *)&DataMsg, sizeof(DataMsg), 0);
                DispatchMsg((char *)&DataMsg);
                break;
            }
            // 处理FD_CLOSE
        case FD_CLOSE:
            {
                (GetDlgItem(IDC_SZCMD))->EnableWindow(FALSE);
                (GetDlgItem(IDC_BTN_SEND))->EnableWindow(FALSE);
                (GetDlgItem(IDC_IPADDR))->EnableWindow(TRUE);
                  closesocket(m_Socket);
                  m_StrMsg = "对方关闭连接";
                  break;
              }
          }

          InsertMsg();
      }

发送命令到远程服务器端的代码如下:

    void CClientDlg::OnBtnSend()
    {
    // 添加处理程序代码
        char szBuf[MAXBYTE] = { 0 };
        GetDlgItemText(IDC_SZCMD, szBuf, MAXBYTE);

        send(m_Socket, szBuf, MAXBYTE, 0);
    }

处理服务器端反馈结果的代码如下:

    VOID CClientDlg::DispatchMsg(char *szBuf)
    {
        DATA_MSG DataMsg;
        memcpy((void*)&DataMsg, (const void *)szBuf, sizeof(DATA_MSG));
        if ( DataMsg.bType == TEXTMSG )
        {
            m_StrMsg = DataMsg.szValue;
        }
        else
        {
            if ( DataMsg.bClass == SYSTEMINFO )
            {
                ParseSysInfo((PSYS_INFO)&DataMsg.szValue);
            }
        }
    }

解析服务器端信息的代码如下:

    VOID CClientDlg::ParseSysInfo(PSYS_INFO SysInfo)
    {
        if ( SysInfo->OsVer.dwPlatformId == VER_PLATFORM_WIN32_NT )
        {
            if ( SysInfo->OsVer.dwMajorVersion == 5 && SysInfo->OsVer.dwMinorVersion == 1 )
            {
                m_StrMsg.Format("对方系统信息:\r\n\t Windows XP %s", SysInfo->OsVer. szCSDVersion);
            }
            else if ( SysInfo->OsVer.dwMajorVersion == 5 && SysInfo->OsVer.dwMinorVersion== 0)
            {
                m_StrMsg.Format("对方系统信息:\r\n\t Windows 2K");
            }
        }
        else
        {
            m_StrMsg.Format("对方系统信息:\r\n\t Other System \r\n");
        }

        m_StrMsg += "\r\n";
        m_StrMsg += "\t Computer Name is ";
        m_StrMsg += SysInfo->szComputerName;
        m_StrMsg += "\r\n";
        m_StrMsg += "\t User Name is";
        m_StrMsg += SysInfo->szUserName;
    }

到这里,远程控制的代码就完成了。如果要实现更多的功能,可能该框架无法进行更好的扩充。该实例主要为了演示非阻塞模式的Winsock应用的开发。如果该实例中的套接字使用阻塞模式的话,那么就必须配合多线程来完成,将接收的部分单独放在一个线程中,否则接收数据的函数recv()在等待接收数据的到来时会将整个程序“卡死”。