4.3 Winsock 2.0
开始编写下面的程序前,需要首先打开Visual Studio 6.0。几乎所有的exploit程序都编写成简单的控制台程序。控制台程序在Windows命令提示符下面运行,类似于UNIX的终端。与UNIX的终端程序一样,控制台程序需要少量参数运行简单的应用程序。要新建一个空的Win32控制台应用程序的工作区,可按以下步骤进行。
(1) 在File菜单下,选择New菜单项。
(2) 选择Win32 Console Application,指定一个合适的项目名称,然后单击OK按钮。
(3) 选择An empty project,单击Finish按钮。
(4) 在File菜单下,选择New菜单项。
(5) 选择C/C++ Source File,指定合适的文件名,然后单击OK按钮。
以上各步完成以后,将得到一个空源文件。
程序必须使用#include <winsock2.h>载入Winsock 2相关的头文件。使用Winsock 2还需要链接到它的库文件,如果没有链接到合适的库文件,编译器或链接器将会报错,显示无法识别Winsock 2的函数。在Visual Studio 6.0中,链接到库文件有以下两种方式。
● 在.c或.cpp文件中链接库文件。这是首选的方法,比较简单,尤其是与别人共享源文件的时候。
● 通过Visual Studio工作区链接库文件,不过这样会使代码共享麻烦一些。如果发现代码无法通过编译,则应当检查一下库文件是否正确链接。下面将一步一步地对这两种链接方式进行说明。
4.3.1 通过Visual Studio 6.0链接
(1) 按Alt+F7键或选择Project菜单下的Settings菜单项。
(2) 在Project Settings对话框中,选择Link选项卡,然后在Object/library modules:文本框中加入ws2_32.lib,然后单击OK按钮。
(3) 完成上面各步以后,就可以正确链接到ws2_32.dll(如图4-1所示)。

图4-1 Visual Studio项目设置菜单
4.3.2 通过源代码链接
在源文件的include语句下面直接添加以下语句:
#progma comment(lib, "ws2_32.lib")
完成以后,程序即可正确链接。
使用Winsock 2 API之前,必须首先创建一个WSADATA对象;访问ws2_32.dll和调用Winsock 2 API时,都需要这个对象。WSADATA对象包含许多属性,但在例4-1中只用到了wVersion。MAKEWORD()函数将版本号转换成为标准形式,如示例中MAKEWORD(2,0)表示版本号为2.0。
例4-1 WSADATA对象
1 WSADATA wsaData;
2 WORD wVersionRequested;
3 wVersionRequested = MAKEWORD(2, 0);
4 WSAStartup(wVersionRequested, &wsaData);
5 If ( WSAStartup(wVersionRequested, &wsaData) < 0 )
6 {
7 printf("Wrong version");
8 exit(1);
9 }
10 SOCKET MySocket;
11 MySock = socket(AF_INET, SOCK_STREAM, 0);
12 MySock = socket(AF_INET, SOCK_DGRAM, 0);
13 struct hostent *target_ptr;
14 target_ptr = gethostbyname( targetip );
15 if( Target_ptr = gethostbyname( targetip ) == NULL )
16 {
17 printf("Can not resolve name.");
18 exit(1);
19 }
20 Struct sockaddr_in sock;
21 Memcpy( &sock.sin_addr.s_addr, target_ptr->h_addr,
target_ptr->h_length );
22 sock.sin_family = AF_INET;
23 sock.sin_port = htons( port );
24 connect (MySock, (struct sockaddr *)&sock, sizeof (sock) );
25 If ( connect (MySock, (struct sockaddr *)&sock, sizeof (sock) ) )
26 {
27 printf("Failed to connect.");
28 exit(1);
29 }
30 char *recv_string = new char [MAX];
31 int nret = 0;
32 nret = recv( MySock, recv_string, MAX -1, 0 );
33 if( (nret = recv( MySock, recv_string, MAX -1, 0 )) <= 0 )
34 {
35 printf("Did not recover any data.");
36 exit(1);
37 }
38 char send_string [ ] = "\n\r Hello World \n\r\n\r";
39 int nret = 0;
40 nret = send( MySock, send_string, sizeof( send_string ) -1, 0 );
41 if( (nret = send( MySock, send_string, sizeof( send_string ) -1, 0 ))
<= 0 )
42 {
43 printf("Could not send any data.");
44 exit(1);
45 }
46 socketaddr_in serverInfo;
47 serverInfo.sin_family = AF_INET;
48 serverInfo.sin_addr.s_addr = INADDR_ANY;
49 listen(MySock, 10);
50 SOCKET NewSock;
51 NewSock = accept(MySock, NULL, NULL);
52 closesocket(MySock);
53 WSACleanup();
分析
● 第1行到第4行,使用WSAStarup()函数启动Winsock 2 API,这个函数需要两个参数:版本号和需要启动的WSADATA对象。如果失败,函数将返回一个错误信息,其中最常见的错误就是由要求的版本号高于可用的版本导致的。
● 第10行到第12行,需要增加一个套接字对象并初始化。套接字对象包含3个参数:地址族、套接字类型和协议。在使用中,地址族总是指定为AF_INET,套接字类型为SOCK_STREAM或SOCK_DGRAM,其中SOCK_STREAM用于双向连接,它与AF_INET同时使用时表示用于TCP连接。SOCK_DGRAM为无连接的缓冲区,与AF_INET同时使用时表示用于用户数据报协议(User Datagram Protocol, UDP)。最后一个参数用于处理套接字上使用的某些特殊协议,它极少使用,所以置为0。
● 第13行到第19行,为套接字获取IP地址和端口号的信息。有可能IP地址并没有直接表示为一个地址,而是一个合法的域名,需要解析才能得到IP地址。将域名解析为易于转换的形式,可以通过hostent结构调用gethostbyname()函数。gethostbyname()函数需要一个字符串作为参数,表示想要连接到的目标机器。这个字符串可以是一个IP地址、域名、局域网主机名,或者任何可以通过nslookup解析的名字。如果无法找到指定的IP地址,gethostbyname()函数将返回NULL。
● 第20行到第21行,根据需要,hostent结构内部应该有一个格式正确的IP地址,它必须移到sockaddr_in结构中。首先,必须声明一个sockaddr变量,并填入IP地址,IP地址存放在target_ptr指针所指的位置。这些操作可以通过memcpy()函数实现,它与strcpy()函数的工作方式类似,不过它用于内存块的复制,并且需要3个参数:目标地址、源地址和数据长度。
● 第22行到第23行,sock结构成员的赋值还没有完成,还需要指定连接类型和端口号。连接类型为Internet(AF_INET),赋值给sin_family成员。需要连接的服务端口号赋值给sin_port成员;sin_port所表示的端口信息必须是网络字节顺序的16位整数。指定的端口号可能存放在一个整数中,所以需要转换成正确的形式。转换字节顺序调用htons()函数即可,它接受一个参数并将其按网络字节顺序返回。
● 第25行到第29行。现在可以建立连接了,建立连接可以通过调用connect()函数实现。connect()函数用于建立套接字对象的连接,这个函数接受3个参数并返回一个错误号。第1个参数表示要建立连接的套接字名称,即本例中的MySock;第2个参数是与套接字相关的信息,包括端口号、IP地址以及连接类型,这些信息已经存放在结构变量sock中,所以这里只需用一个指针指向它即可;最后一个参数表示第2个参数的大小,可以用sizeof()获得。如果执行成功,函数返回的错误号为0。与WSAStartup()函数一样,最好是进行一些错误检查,以确保连接成功。
● 第31行到第37行。现在可以向网络另一端的主机发送和接收数据了。recv()函数用于从其他机器接收数据,这个函数接收4个参数并返回一个整型值。第1个参数为接收数据所使用的套接字,也就是MySock;第2个参数为用来存储所接收数据的字符串;第3个参数表示希望接收的数据的最大长度,长度应为字符串的长度减1,留下一个字节用于存放结束符;最后一个参数是一个指定函数调用方式的标志。如果使用MSG_PEEK调用这个函数,可以只查看数据而不将数据从缓冲区清除。另一种方式是使用MSG_OOB标志,这个标志用于DECnet协议。该参数最常用的值是0,它将信息移到指定的字符串,并从缓冲区清除。函数的返回值为所接收数据的长度,如果执行失败,则会返回0或一个负数。
● 第38行到第45行,send()函数与recv()函数相似,区别在于它是用来发送数据的。这个函数同样接受4个参数:第1个参数与recv()函数一样,是一个套接字标识符;第2个参数是要发送的字符串;第3个参数为所发送字符串的长度,可以通过将sizeof()的结果减1得到,因为这里不打算将消息末尾的结束符也发送出去;最后一个参数与recv()函数一样,是一个标志参数。send()函数返回所发出数据的长度,如果发送的长度为0,则说明没有发出任何消息。这一点有助于确定消息是否成功发送出去了。
● 第46行到第49行,这里打算构建一个服务器程序,因此需要用一个套接字等待客户端的连接。这可以通过listen()函数实现,这个函数需要两个参数,并且返回值为整数。在调用listen()函数之前,必须首先创建一个在指定的IP地址及端口上进行监听的套接字。将sin_addr.s_addr设置为INADDR_ANY,表示将套接字指定为监听所有的本地地址。listen()函数的第1个参数是进行监听的套接字,第二个参数是套接字通信时最多的并发连接数量。
● 第50行到第51行,如果一个客户端试图与服务器建立连接,就需要用accept()函数接受客户端的连接。accept()函数具有3个参数,且返回值为一个套接字标识符。函数的第1个参数为等待连接的套接字;第2个参数是一个可选参数,为指向sockaddr结构的指针,用来获得客户端的信息;第3个参数也是一个可选参数,为addr参数的长度。accept()函数返回的套接字将用来与客户端继续进行通信。
● 第52行到第53行,进行一些清除工作,这是一项非常重要但却经常被忽略的工作。清除工作通过两个函数完成:closesocket()和WSACleanup()。closesocket()函数将关闭套接字,并释放套接字所占的内存空间。WSACleanup()函数将终止WSADATA对象,释放它所占的内存,并停止使用ws2_32.dll。程序越大越复杂,及时释放那些不再使用的对象就越重要,如果不这样做,将会导致程序占用更多的内存,运行效率更低。






