我们知道许多应用程序,例如E-mail、Web和即时通信都依靠网络才能实现。这些应用程序中的每一个都依赖一种特定的网络协议,但每个协议都使用相同的常规网络传输方法。许多人都没有意识到网络协议本身存在漏洞。本文将会学习如何使用套接字使应用程序访问网络以及如何处理常见的网络漏洞。
图1 OSI模型
1.套接字
套接字是通过操作系统(OS)完成网络通信的一种标准方法。可以将套接字看作是与连接相连的一个终端,就像是操作员配电盘上的一个插座一样。但是这些套接字只是程序员的抽象称呼,它们负责有文描述的OSI模型的所有基本细节。对程序员来说,可以使用一个套接字通过网络发送或接收数据。这些数据在较低的层(由操作系统处理)之上的会话层(5)传输,该层负责路由。有几种不同的套接字,它们决定了传输层的结构。最常见的类型是流套接字和数据报套接字。
流套接字提供了可靠的双向通信,这类似于您和他人打电话。一方向另一方发起连接,建立连接之后,任何一方都可以和另一方通信。此外,您所说的话实际上是否到达目的地能够得到快速证实。流套接字使用一种称为传输控制协议(Transmission Control Protocol,TCP)的标准通信协议,这个协议存在于OSI模型的传输层(4)。在计算机网络上,数据通常以我们称之为包的大数据块的形式传输。TCP被设计为数据包按顺序到达目的地并且无差错,就像在电话中讲话时,单词以它们被说出的顺序到达另一端一样。Web服务器、邮件服务器以及它们各自的客户应用程序都使用TCP和流套接字进行通信。
另一种常见的套接字类型是数据报套接字。使用数据报套接字通信更像是邮寄一封信而不是打电话。连接是单向的并且不可靠。如果您寄了几封信,您将不能确定它们是否按照和邮寄时相同的顺序到达目的地,甚至连能否被送达目的地也不能保证。邮政服务相当可靠,但Intemet并不可靠。数据报套接字在传输层(4)上使用另一种称为UDP的标准协议来代替TCP。UDP代表用户数据报协议(User Datagram Protocol),意味着可以用它来创建自定义协议。这个协议非常基本并且是轻量级的,它只内置了很少的保护措施。它并不是一种真正的连接,只是一种从一端向另一端发送数据的基本方法。使用数据报套接字时,协议中的系统开销非常少,但协议完成的功能也不多。如果程序需要证实另一方接收到了数据包,必须编程使另一方回送一个确认包。有些情况下,可以接受数据包的丢失。数据报套接字和UDP普遍用于网络游戏和流媒体,因为开发人员可以根据需要精确地修整他们的通信,而不会存在像TCP那样的固有系统开销。
2.套接字函数
在C语言中,套接字的行为类似于文件,因为它们使用文件描述符来标识它们自己。套接字的行为与文件非常相似,实际上利用套接字文件描述符,可以使用read()和write()函数接收和发送数据。但是,有几个函数是专门设计用来处理套接字的。在/usr/include/sys/socket.h文件中有这些函数原型的定义。
extern int socket (int __domain, int __type, int __protocol) __THROW;
用于创建一个新套接字,返回一个表牙示套接字后的文件描述符,错误时遣返回-1。extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
将一个套接字(由文件描述符fd指定)连接到远程主机。成功返回0,错误返回-1。
Listen(int fd,int backlog_queue_size)
侦听传入的连接并将连接请求排队,:直到数量达到backlog_queue_size。成功返回0,错误返回-1。
extern int accept (int __fd, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);
一个绑定的端口上接受一个传入连接。远程主机的地址信息写入remote_host结构中,地址结构的实际大小写入到addr_len中。这个函数返回一个新套接字文件描述符来标识已经连接的套接字,错误返回-1。
extern ssize_t send (int __fd, __const void *__buf, size_t __n, int __flags);
从*__buf向套接字fd发送n个字节,返回值为发送的字节数,错误返回-1。
extern ssize_t recv (int __fd, void *__buf, size_t __n, int __flags);
从套接字fd接收n个字节到*__buf中,返回值为收到的字节数,错误返回-1。
使用socket()函数创建套接字时,必须指定套接字的域(domain)、类型(type)和切协议( protocol)。域指的是套接字的协议族。套接字可以使用各种协议进行通信,从浏览Web时使用的标准Internet协议到诸如AX.25,这样的业余无线电协议(如果您是一个无线电发烧友)。这些协议族在bits/socket.h中定义,它自动包含在sys/socket.h中。
/usr/include/bits/socket.h 片段1
/* Protocol families. */
#define PF_UNSPEC 0 /* Unspecified. */
#define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
#define PF_UNIX PF_LOCAL /* Old BSD name for PF_LOCAL. */
#define PF_FILE PF_LOCAL /* Another non-standard name for PF_LOCAL. */
#define PF_INET 2 /* IP protocol family. */
#define PF_AX25 3 /* Amateur Radio AX.25. */
#define PF_IPX 4 /* Novell Internet Protocol. */
#define PF_APPLETALK 5 /* Appletalk DDP. */
#define PF_NETROM 6 /* Amateur radio NetROM. */
#define PF_BRIDGE 7 /* Multiprotocol bridge. */
#define PF_ATMPVC 8 /* ATM PVCs. */
#define PF_X25 9 /* Reserved for X.25 project. */
#define PF_INET6 10 /* IP version 6. */
如前所述,虽然流套接霉字和数据报套接字最常使用,但还有其他几种类型的套接字。套接字的类型也定义在bits/socket.h中(上面代码中的/*comment*/是另一种形式的注释,它将所有处于星号之间的内容作为注释)。/usr/include/bits/socket.h 片段2
/* Types of sockets. */
enum __socket_type
{
SOCK_STREAM = 1, /* Sequenced, reliable, connection-based
byte streams. */
#define SOCK_STREAM SOCK_STREAM
SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams
of fixed maximum length. */
#define SOCK_DGRAM SOCK_DGRAM
SOCK_RAW = 3, /* Raw protocol interface. */
#define SOCK_RAW SOCK_RAW
SOCK_RDM = 4, /* Reliably-delivered messages. */
#define SOCK_RDM SOCK_RDM
SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based,
datagrams of fixed maximum length. */
#define SOCK_SEQPACKET SOCK_SEQPACKET
SOCK_PACKET = 10 /* Linux specific way of getting packets
at the dev level. For writing rarp and
other similar things on the user level. */
#define SOCK_PACKET SOCK_PACKET
};
Socket()函数的最后一个参数是协议,该参数通常为0。该函数的详细说明书允许使用一个协议族中的多个协议,因此这个参数用来从协议族中选择一个协议。但是,实际上大多数协议族中仅有一个协议,这意味着这个参数通常被设置为0,即选用协议族列表中第一个也是唯一的一个协议。本文使用套接字所做的事情也是这种情况,所以在我们的例子中这个参数总是0。
3.套接字地址
许多套接字函数引用一个sockad出结构来传递定义了一个主机的地址信息。这个结构也定义在bits/socket.h中,如后面所示。
/usr/include/bits/socket.h 片段3
/* Get the definition of the macro to define the common sockaddr members. */
#include <bits/sockaddr.h>
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
宏SOCKADDR_COMMON在文件bits/sockaddr.h中定义,其本质是将参数转换为一个无符号短整型数。这个值定义了地址的地址族,结构的其余部分用于保存地址数据。因为套接字可以使用各种协议族进行通信,根据地址族的不同,每个协议族都有自己的定义终端地址的方法,所以必须将地址定义为变量。可用的地址族也在bits/socket.h中定义,它们通常直接转换成相应的协议族。
/usr/include/bits/socket.h 片段4
/* Address families. */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6
#define AF_ROSE PF_ROSE
#define AF_DECnet PF_DECnet
#define AF_NETBEUI PF_NETBEUI
#define AF_SECURITY PF_SECURITY
#define AF_KEY PF_KEY
#define AF_NETLINK PF_NETLINK
#define AF_ROUTE PF_ROUTE
#define AF_PACKET PF_PACKET
#define AF_ASH PF_ASH
#define AF_ECONET PF_ECONET
#define AF_ATMSVC PF_ATMSVC
#define AF_SNA PF_SNA
#define AF_IRDA PF_IRDA
#define AF_PPPOX PF_PPPOX
#define AF_WANPIPE PF_WANPIPE
#define AF_BLUETOOTH PF_BLUETOOTH
#define AF_MAX PF_MAX
因为地址可以包含不同类型的信息(这取决于地址族),所以有其他几种地址结构,它们在地址数据部分包含了来自于sockaddr结构的公共元素以及地址族的特殊信息。这些结构大小相同,因此可以将它们从一种类型强制转换到另一种类型。这意味着socket()函数会简单地接受一个指向sockaddr结构的指针,而这个指针事实上可以指向一个IPv4、IPv6或X.25地址。这样就允许套接字函数操作各种协议了。