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。”
“哦!又收到服务器发的数据了。”同学们高兴的说。
“我有点明白了!编程就是用一系列的函数完成构思的流程图中的每个部分。”宇强说道,“所以思路才是关键。”
“你总结得很好!”老师赞扬的说:“程序的本质是算法加数据结构。算法就是流程图,数据结构就是定义适当的变量来调用适合的函数。”
“当然,如果作为一个软件,就不局限于此了。软件追求的是满足用户提出的需求,并提供给用户人性化的界面。”
“我们知道了如何编程实现数据在网络上的传输,就可以给目标机发送命令和接收执行的结果了。”
“传输是可以了,但对方的计算机怎样执行我们传过去的命令呢?”宇强问道。
“问得好!这就要涉及到进程通信以及管道了。”