3.1.1 IP和Socket编程初步

“我们的ShellCode的功能是完成网络的远程连接,那自然会涉及到网络通信。网络通信有好几种实现方法,这里我们使用Windows Socket编程。”

小知识:Windows下网络通信编程的几种方式

第一种是基于NetBIOS的网络编程,这种方法在小型局域网环境下的实时通信有很高的效率;

第二种是基于Winsock的网络编程;这种方法使用一套简单的Windows API函数来实现应用层上的编程;

第三种是直接网络编程;比如Winpcap、libnet等网络数据包构造技术可以完成链路层或网络层上的网络编程;

第四种是基于物理设备的网络编程,即MAC层编程接口。

“Socket 是什么?有什么用处呢?”玉波问道。

“和IP作类比吧。在国际互联网Internet上,有成千百万台主机,为了区分这些主机,人们给每台机器都分配了一个专门的‘地址’作为标识,这就是IP地址。IP地址就像是计算机在网上的身份证,通过它才能确定网上不同的机器,大家才能互相通信。”

小知识:

Internet IP地址由Inter NIC(Internet网络信息中心)统一负责全球地址的规划、管理。通常,每个国家需成立一个组织统一向有关国际组织申请IP地址,然后再分配给客户。

IP地址分为A、B、C、D和E类。IP地址通常以圆点为分隔号的4个十进制数字表示,每个数字对应于8个二进制的比特串,如某一台主机的IP地址表示格式为: 128.20.4.1 。

“我们要看自己机器上的IP很方便。”老师接着说,“一种方法是在‘网上邻居’上点击鼠标右键,在弹出菜单中选择‘属性’;然后在‘本地连接’图标上点击右键,选择‘属性’,选中‘Internet协议(TCP/IP)’并双击;就可看到图3-1所示的窗口。”

“可以看到,这台机器的IP是211.83.154.20,但如果是通过DHCP服务器自动获得IP地址,那这种方法就不行了。”老师换了一台机器进行演示,“看,不会在这里显示出IP地址,如图3-2。”

“这个时候,我们就要用另一种方法了。点击‘开始’→‘运行’,输入‘cmd’并确定,在命令行下输入 ipconfig 就可看到IP,如图3-3,IP是10.4.4.79。”

小知识:IP地址危机和NAT转换

最初设计IP协议时,设计者没有料到网络会如此的高速发展,现在IP地址正迅速的枯竭,如果没有IP地址,主机或者移动通信设备在网络上就没有唯一的身份识别,也就不能发送或接收数据了。

有两种解决办法。一是使用新一代的IP协议——IPv6,IPv6采用128位数字,所以地址的范围可以看作是无限的;另一种是使用NAT(Network Address Translation)——网络地址转换,允许内部网络上的多台 PC(使用内部地址段,如10.0.x.x、192.168.x.x、172.x.x.x)共享单个、全局路由的 IPv4 地址,这在一定程度上缓解了IP地址不足的问题。

“这下大家对IP地址没什么问题了吧。”老师问道。

“嗯,清楚了。”

“好,然后我们来看看Socket吧!虽然IP可以唯一的区分网络上的每台主机,但每台主机可能同时和多台机器通信,有可能某个软件就和多台机器通信,比如大家常用的QQ、IE浏览器。”

“所以仅仅依靠IP地址是无法区分一台机器的所有通信的。”老师继续说道,“为了解决这个问题,就引入了Socket(中文名称是套接字)。”

“对每一个通信,除了IP地址外,还用一个标识符Socket来标明每个通信程序(进程)。示意图如图3-4。”

“打个比方,我有一把钥匙,可以打开某个房间里的一把锁,但仅仅知道房间号还不够,还需要知道是具体那把锁。”

“如果把多个房间看成是多台计算机,那房间号就相当于IP地址,钥匙是数据,锁就是程序。数据和程序要通信,就像钥匙要找到所属的锁,仅凭所在的房间号是不够的。”

老师喝了一口水,继续说道:“所以我们可以在钥匙上贴一个标签,注明是哪个房间、哪把锁的钥匙。就像通信中的Socket,作为计算机通信的标记,标明通信是哪台机器的哪个程序的。这样就可准确分别出通信双方了。”

“哦,这样啊!”大家一下就明白了。

“套接字Socket在网络通信中非常重要,当年可是加利福尼亚大学伯克利分校(Berkeley)耗费了大量精力才设计出来的,所以也称为Berkeley套接字。”

“哦,那以后我也设计个吃的东西,拿我的名字命名。”玉波满怀信心的说。

“不用设计什么了。他高傲,但宅心人厚;他谦虚,但受万人敬仰!他就是来自天堂的使者、地狱的恶魔——食神玉波!”

“哈哈哈哈……”大家狂笑了起来。

“好了,”老师说道,“我相信在座的各位在不久之后一定会有所建树的,出名之后,可不要忘了我啊!”

“哈哈哈哈……”大家又大笑了起来,眼泪都快出来了。

“OK!玩笑开够了,我们继续,”课堂稍微安静后老师说道,“通过伯克利的成果,我们使用Socket实现网络通信编程就非常方便了。Socket其实就是一个整数,它标识了计算机上不同的通信端点。程序在通信前首先建立一个套接字,以后对设置IP、端口和传输数据,都通过此套接字来进行。”

小知识:端口port

是指TCP/IP协议中规定的端口,范围从0到65535。它可以标志某种服务,比如网页服务器一般是80端口,FTP服务器一般是21端口;在客户端连接中,也需要一个端口来通信,一般是比较高的动态端口号。

“大家理解了套接字Socket的概念,是不是想实际使用一下呢?”老师问道。

“是啊!想看看到底是怎样编程的。”

“好!使用Socket在两台计算机上实现通信,其实是件简单的事。首先我们看通信的流程。”

“通信双方一定有一台是服务端,一台是客户端。服务端首先启动,建立一个套接字Socket,并对相应的IP和端口进行绑定、监听;客户端也建立一个套接字,和服务端不同,它直接连接服务端监听的端口。双方建立连接后,服务端和客户端就可以互相传输数据了,当然都是通过Socket来完成的。”

“其工作流程图如3-5。”

“对图中的那些函数,我这里稍加解释一下。”

int  WSAStartup(WORD wVersionRequested, LPWSADATA  lpWSAData);

功能是初始化Windows Socket Dll,在Windows下必须使用它。

参数:

“wVersionRequested”表示版本,可以是1.1、2.2等;

“lpWSAData”指向WSADATA数据结构的指针。

int socket(int family, int type, int protocol);

功能是建立Socket,返回以后会用到的Socket值。如果错误,返回-1。

参数:

“int family”参数指定所要使用的通信协议,取以下几个值:AF_UNIX(Unix内部协议)、AF_INET(Internet协议)、AF_NS Xerox(NS协议)、AF_IMPLINK(IMP连接层),在Windows下只能把“AF”设为“AF_INET”;

“int type”参数指定套接字的类型,取以下几个值:SOCK_STREAM(流套接字)、SOCK_DGRAM (数据报套接字)、SOCK_RAW(未加工套接字)、SOCK_SEQPACKET(顺序包套接字);

“int protocol”参数通常设置为0。

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

功能是把套接字和机器上一定的端口关联起来。

参数:

“sockfd”是调用socket()返回的套接字值;

“my_addr”是指向数据结构struct sockaddr的指针,它保存你的地址,即端口和IP地址信息;

“addrlen”设置为sizeof(struct sockaddr)。

int listen(int sockfd, int backlog);

功能是服务端监听一个端口,直到accept()。在发生错误时返回-1。

参数:

“sockfd”是调用socket()返回的套接字值;

“backlog”是允许的连接数目。大多数系统的允许数目是20,也可以设置为5到10。

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

功能是客户端连接服务端监听的端口。

参数:

“sockfd”是调用socket()返回的套接字值;

“serv_addr”保存着目的地端口和IP 地址的数据结构struct sockaddr;

“addrlen”设置为sizeof(struct sockaddr)。

int accept(int sockfd, void *addr, int *addrlen);

功能是服务端接受客户端的连接请求,并返回一个新的套接字,以后服务端的数据传输就使用这个新的套接字。如果有错误,返回-1。

参数:

“sockfd”是和listen()中一样的套接字值;

“addr”是个指向局部的数据结构sockaddr_in的指针;

“addrlen”设置为sizeof(struct sockaddr_in)。

int send(int sockfd, const void *msg, int len, int flags);
int recv(int sockfd, void *buf, int len, unsigned int flags);

功能是用于流式套接字或数据报套接字的通讯,我们数据的真正传输就由它们完成。

参数:

“sockfd”是发/收数据的套接字值;

“msg”指向你想发送的数据的指针;

“buf”是指向接收数据存放的地址;

“len”是数据的长度;

“flags”设置为 0。

int sendto(int sockfd, const void *msg, int len, unsigned int flags,const struct sockaddr *to, int tolen);
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,  struct sockaddr *from, int *fromlen);

功能和send、recv类似,不过是用于无连接数据报套接字的传输。

int closesocket(int sockfd)

功能是关闭套接字。

参数“sockfd”为要关闭的套接字值。

“哇!好复杂的函数,好难记啊!”大家嚷道。

“NO,虽然函数有点多,每个函数又有很多参数,但大家完全没有必要去死记(其实也是记不清的)。大家要记的是流程和思路,编程要用具体函数时,查MSDN吧!非常详细的开发帮助资料,会找到你要用的。如图3-6。”

“有了上面的基础,我们一起来看一个具体程序。这个程序比较简单,服务端监听某个端口,如果有客户端连接,就向它发一字符串,客户端收到后,在屏幕上打出来。”

“但我们对编程还不大懂啊!”

“这里的目的是让大家对Socket编程有个整体了解。不用怕,程序我会详细解释的,首先是服务端的程序。其流程是:

socket()→bind()→listen→accept()→recv()/send()→closesocket()

具体代码如下:”

#include <stdio.h>
#include <winsock.h>
#pragma comment(lib,"Ws2_32")
#define MYPORT 830  /*定义用户连接端口*/ 
#define BACKLOG 10  /*多少等待连接控制*/ 
int main() 
{
         int sockfd, new_fd;                                  /*定义套接字*/
         struct sockaddr_in my_addr;          /*本地地址信息 */ 
         struct sockaddr_in their_addr;        /*连接者地址信息*/ 
         int sin_size;
         WSADATA ws;
         WSAStartup(MAKEWORD(2,2),&ws);           //初始化Windows Socket Dll
          //建立socket
         if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
         { 
                   //如果建立socket失败,退出程序
                   printf("socket error\n"); 
                   exit(1); 
         } 
         //bind本机的MYPORT端口
         my_addr.sin_family = AF_INET;                     /* 协议类型是INET  */ 
         my_addr.sin_port = htons(MYPORT);            /* 绑定MYPORT端口*/ 
         my_addr.sin_addr.s_addr = INADDR_ANY;   /* 本机IP*/ 
         if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))== -1)
         { 
                   //bind失败,退出程序
                   printf("bind error\n"); 
                   closesocket(sockfd);
                   exit(1); 
         }
         //listen,监听端口
         if (listen(sockfd, BACKLOG) == -1)
         { 
                   //listen失败,退出程序
                   printf("listen error\n"); 
                   closesocket(sockfd);
                   exit(1); 
         } 
         printf("listen...");
         //等待客户端连接
         sin_size = sizeof(struct sockaddr_in); 
         if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1)
         { 
                   printf("accept error\n"); 
                   closesocket(sockfd);
                   exit(1);
         } 
         printf("\naccept!\n");
         //有连接,发送ww0830字符串过去
         if (send(new_fd, "ww0830\n", 14, 0) == -1) 
         {
                   printf("send error");
                   closesocket(sockfd);
                   closesocket(new_fd); 
                   exit(1); 
         } 
         printf("send ok!\n");
         //成功,关闭套接字
         closesocket(sockfd);
         closesocket(new_fd);
         return 0;
}

老师说:“程序看起来比较长,是因为加了许多错误处理。如果去掉他们,就可以看出程序的本质。我再重复一遍,流程如下:”

对服务端程序的流程概括:

先是初始化Windows Socket Dll: WSAStartup(MAKEWORD(2,2),&ws);

然后建立Socket: sockfd = socket(AF_INET, SOCK_STREAM, 0)

再bind本机的MYPORT端口:

my_addr.sin_family = AF_INET;         /* 协议类型是INET   */ 
my_addr.sin_port = htons(MYPORT);       /* 绑定MYPORT端口  */ 
my_addr.sin_addr.s_addr = INADDR_ANY;   /* 本机IP           */ 
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))

接下来监听端口: listen(sockfd, BACKLOG)

如果有客户端的连接请求,接收它: new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)

最后发送ww0830字符串过去: send(new_fd, "ww0830\n", 14, 0)

收尾工作,关闭socket: closesocket(sockfd); closesocket(new_fd);

“哦,果然都是对Socket的操作。可不可以先看看服务端的效果呢?”大家问道。

“当然可以了,我们把server.cpp编译、执行,就会一直监听830端口,如图3-7。”

“测试它能否传输数据吧!在另一台机器上进入命令行界面,输入 telnet 服务器IP 830 ,我的服务器IP是211.83.154.120,如图3-8。”

“敲回车,执行效果如图3-9,客户端成功收到了服务端发的‘ww0830’字符串,并打了出来;服务端也显示传输成功(图3-10)。”

“乌拉!是啊!”

“刚才使用的Telnet是Windows系统自带的程序。我们既然可以实现服务端程序,那当然也可以实现客户端程序了。其流程是:

socket()→connect()→send()/recv()→closesocket()

比服务端更简单吧!其实现代码如下:”

#include <stdio.h>
#include <stdio.h>
#include <winsock.h>
#pragma comment(lib,"Ws2_32")
#define PORT 830                            /* 客户机连接远程主机的端口 */ 
#define MAXDATASIZE 100                     /* 每次可以接收的最大字节 */ 
int main(int argc, char *argv[]) 
{ 
int sockfd, numbytes; 
char buf[MAXDATASIZE]; 
struct sockaddr_in their_addr;        /* 对方的地址端口信息 */ 
if (argc != 2) 
{ 
           //需要有服务端ip参数
           fprintf(stderr,"usage: client hostname\n"); 
           exit(1); 
} 
 WSADATA ws;
WSAStartup(MAKEWORD(2,2),&ws);         //初始化Windows Socket Dll
 if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{ 
           //如果建立socket失败,退出程序
           printf("socket error\n"); 
           exit(1); 
} 
//连接对方
their_addr.sin_family = AF_INET;                         /* 协议类型是INET  */ 
their_addr.sin_port = htons(PORT);                       /* 连接对方PORT端口 */ 
their_addr.sin_addr.s_addr = inet_addr(argv[1]);        /* 连接对方的IP */ 
if (connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct sockaddr)) == -1)
{ 
           //如果连接失败,退出程序
           printf("connet error\n"); 
           closesocket(sockfd); 
           exit(1); 
} 
 //接收数据,并打印出来
if ((numbytes=recv(sockfd, buf, MAXDATASIZE, 0)) == -1) 
{ 
           //接收数据失败,退出程序
           printf("recv error\n"); 
           closesocket(sockfd); 
           exit(1); 
} 
buf[numbytes] = '\0'; 
printf("Received: %s",buf); 
closesocket(sockfd); 
return 0; 
}

“仍然是流程最关键,”老师又强调了一遍,“我们也把脉络提出来过一遍吧!”

对客户端程序的流程概括:

首先是初始化Windows Socket Dll: WSAStartup(MAKEWORD(2,2),&ws);

然后建立Socket: sockfd = socket(AF_INET, SOCK_STREAM, 0)

接着连接服务器方:

their_addr.sin_family = AF_INET;                                 /* 协议类型是INET    */ 
their_addr.sin_port = htons(PORT);                           /* 连接对方PORT端口      */ 
their_addr.sin_addr.s_addr = inet_addr(argv[1]);         /* 连接对方的IP  */ 
connect(sockfd, (struct sockaddr *)&their_addr,sizeof(struct sockaddr))

连接成功就接收数据: recv(sockfd, buf, MAXDATASIZE, 0)

最后把收到的数据打印出来并关闭套接字:

printf("Received: %s",buf);      closesocket(sockfd);

“这个程序的执行要带对方服务器的IP地址为参数,所以要这样执行,如图3-11。”

“我们还是先打开服务端的程序,让它监听端口,再运行客户端的程序去连接它,其效果如图3-12。”

“哦!又收到服务器发的数据了。”同学们高兴的说。

“我有点明白了!编程就是用一系列的函数完成构思的流程图中的每个部分。”宇强说道,“所以思路才是关键。”

“你总结得很好!”老师赞扬的说:“程序的本质是算法加数据结构。算法就是流程图,数据结构就是定义适当的变量来调用适合的函数。”

“当然,如果作为一个软件,就不局限于此了。软件追求的是满足用户提出的需求,并提供给用户人性化的界面。”

“我们知道了如何编程实现数据在网络上的传输,就可以给目标机发送命令和接收执行的结果了。”

“传输是可以了,但对方的计算机怎样执行我们传过去的命令呢?”宇强问道。

“问得好!这就要涉及到进程通信以及管道了。”