网络扫描技术揭秘学习笔记《一》TCP/IP协议编程

时间:2022-02-25 01:05:50

《一》TCP/IP协议编程

1. Socket地址=(IP地址:端口号)
服务器要想让客户端访问自身,必须同时将二者公之于众,缺一不可;否则客户端将不知道该到哪台主机或者某台主机的端口访问服务,即:

连接=(Socket地址1Socket地址2)=(IP地址1:端口地址1,IP地址2:端口地址2

2. 手工扫描

  1. 想扫描192.168.1.1~192.168.1.254这段IP中哪些主机是开的,则可以通过如下一个批处理来完成,系统会按顺序依次不停地ping每一台主机。
    for /1 %%a in ( 1 1 254 ) do ( ping 192.168.1.%%a)
  1. 要检测对方主机80端口是否提供WWW服务,只需要在命令行中输入:
    Talnet <对方IP地址> 80
    如果失败,会提示无法连接远程主机
    如果成功,则对方主机进入到等待客户端输入命令的状态,这个时候可以随便输入一些字符,并连续回车,五六个回车后,对方就会返回如下内容:
HTTP/1.1 400 Bad Request
Server:Microsoft-IIS/5.1
Date:Sun, 19 Jun 2011 14:43:50 GMT
Connect-Type: test/html
Connect-Length: 87
<html><head><title>Error</title></head>
<body>The parameter is incorrect.</body></html>

失去了跟主机的连接。
C:>

  1. 测试21端口是否提供FTP服务,在命令行输入:
    Telnet <对方IP地址> 21
    如果连接失败,提示Telnet无法连接到远程主机
    如果连接成功,对方会显示以下内容:
    220-Micrsoft FTP Service
    220 欢迎访问LRM服务器

3.端口和套接字

  1. 一个二元组(IP地址,端口地址)组成一个套接字,
    一个五元组(本地IP,本地端口,使用协议,远程IP,远程端口)组成了一个通信过程。
    一个IPv4的基本数据结构主要有in_addr和socketaddr_in两个,前者表示32位的地址,后者是通用的套接口地址结构,它们的结构如下:
struct in_addr
{
in_addr_t s_addr;
};

struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
  1. 地址表示顺序
    不同的系统在内存存储多字节数据的方式有所不同,而网络传输中,数据存储顺序不一定和系统存储顺序一样,因此为保证系统正确性和可移植性,需要利用系统的转换函数进行转换。
    以IPv4的地址为例,一个IP地址的四个字节”192.168.1.100“,在PC架构的计算机中,数据的表示是低位优先,由前至后是100、1、168、192;而在网络Socket协议所表示的网络传输中,则是高位优先,由前至后是192.168.1.100,这需要在处理时通过函数转化。

4.Windows Socket结构

  1. sockaddr结构

在Socket的函数中,有几个函数使用了socketaddr结构,该结构用于保存一个IP地址,其结构如下:

struct sockaddr
{
unsigned short sa_family;
char sa_data[14];
};

结构成员如下:
sa_family:协议族,一般是AF_INET。
sa_data:数据,14是其最大长度。
sockaddr结构通常只是为了在各操作系统中保持兼容使用,平时很少有人直接使用该结构格式,而是使用一个一直兼容的sockaddr_in结构,其结构如下:

struct sockaddr_in
{
short sin_family;
unsigned short sin_port;
struct in_addr;
char sin_zero[8];
};

该结构表示一个TCP/IP通信中,某个主机IP地址和端口的完整表示,其结构成员如下:
sin_family:协议族,一般是AF_INET。
sin_port:端口地址。需要注意的是该端口在内存中保存格式是低位优先,因此在复制的时候,应该调用htons函数做一下转换。
sin_addr:一个存储IP的结构。
sin_zero:为了和sockaddr结构保持同样的长度二补足8个字节,一般情况下,用户应该全部填成0.
其中真正存储IP结构的sin_addr变量有事一个结构,该结构如下:

struct in_addr
{
union
{
sturct{
unsigned char s_b1,s_b2,s_b3,s_b4;
}S_un_b;
struct{
unsigned short s_w1,s_w2;
}S_un_w;
}S_un;
}
  1. hostent结构
    hostent结构的定义为:
 struct hostent {
cahr FAR* h_name;
char FAR * FAR *h_aliases;
short h_addrtype;
short h_length;
cahr FAR * FAR *h_addr_list;
};

hostent结构用于存储给定主机的信息,例如主机名、IP地址等属性。
结构成员如下:
h_name:主机名
h_aliases:主机的别名
h_addrtype:地址的类型
h_length:每个地址的长度,以字节为单位

  1. servent结构
struct servent{
char FAR* s_name;
char FAR *FAR s_aliases;
short s_port;
char FAR * s_proto'
};

servent结构用于保存或返回给指定服务器的名称和服务器数。
成员函数如下:
s_name:服务名
s_aliases:服务的别名
s_port:服务的端口。端口号以网络顺序存储
s_proto:协议的名称

5. Windows socket转换类函数

  1. htons函数
u_short htond(
u_short hostshort
)
;

htons函数将计算机存储的USHORT格式转换为网络存储的USHORT格式。该函数名可以理解为:Host TO Net usigned Short。
返回值:返回一个网络顺序的USHORT格式整数。
参数:hostshort,一个16位的以计算机存储格式的USHORT整数。

  1. ntohs函数
u_short ntohs(
u_short netshort
)
;

ntohs函数将网络存储的USHORT格式转换为计算机存储的USHORT格式。该函数名可以理解为:Net TO Host unsigned Short。
返回值:返回一个网络顺序的USHORT格式整数
参数:netshort,一个16位的以网络存储格式的USHORT整数。

  1. htonl函数
u_long htonl(
u_long hostlong
)
;

htonl函数将计算机存储的ULONG格式转换为网络存储USHORT格式。该函数名可以理解为:Host TO Net unsigned Long。
返回值:返回一个网络顺序的ULONG格式函数。
参数:hostlong,一个32位的以计算机存储格式发的ULONG整数

  1. ntohl函数
u_long ntoh1(
u_long netlong
)
;

ntohl函数将网络存储的ULONG格式转换为计算机存储的ULING格式。该函数名可以理解为:Net TO Host unsigned Long
返回值:返回一个32位的以网络格式的ULONG整数
参数:netlong,一个32位的以网络存储格式的ULONG整数。

  1. inet_ntoa函数
char FAR * inet_ntoa(
struct in_addr in
);

inet_ntoa函数将由in_addr结构所表示的网络地址,转换成由字符串表示的IP地址,在本书中的扫描器,有大量的ip字符串和ULONG之间的互换操作,由于该函数还要使用一个in_addr结构,所以比较麻烦,并且出错时,不能正确指出出错位置,因此本书中大部分的转换采用的是自编的一个函数,详细内容见“2.2.5节IP格式的互换”。
返回值:如果没有错误,则返回一个字符串型的IPv4地址串(形如“a.b.c.d”),否则返回NULL。
参数:in_addr,是一个in_addr类型的主机地址结构。该结构在本节前面内容中已详细说明了。

  1. inrt_addr
unsigned long innt_addr(
const char FAR *cp
};

inet_addr函数将字符串组成的IP地址串转换成一个ULNG的整数,该整数可用于in_sddr结构中,是按网络格式存储的。在本书中的扫描器,有大量的IP字符串和ULONG之间的互换操作,由于该函数还要使用一个in_addr结构,所以比较麻烦,并且出错时,不能正确指出出错位置。因此本书中大部分的转换采用的是自编的一个函数,详细内容见“2.2.5节IP格式的互换”。
返回值:如果没有错误,则返回一个ULONG型整数,否则返回INADDR_NONE,此时例如IP字符好惨的某一项值大于255或为负值
参数:cp,字符串指针,指向一个IP字符串

  1. gethostbyname函数
struct hostent FAR * gethostbyname(
const char FAR *name
);

gethostbyname函数根据主机名读取主机信息(主要是IP地址)。
返回值:如果调用成功,则返回一个指向hostent结构的指针,hostent结构的定义参见前面小节;否则返回NULL,可以通过调用WSAGetLastError获得对错误信息的进一步描述
参数:name:指向一个以NULL结尾的,表示主机名的字符串。

  1. gethostbyaddr函数
struct HOSTENT FAR * gethostbyaddr(
const char FAR *addr,
int len,
int type
);

gethostbyaddr函数通过网络地址读取主机信息。
返回值:如果调用成功,则返回一个指向hostent结构的指针;否则返回NULL。可以通过调用WSAGetLastError获得对错误信息的进一步描述。
参数:
addr:指向一个网络字节顺序的IP地址
len:地址的长度
type:协议类型,通常是AF_INET。

  1. gethostname函数
int gethostname(
char FAR *name,
int namelen
);

gethostname函数读取本地主机的主机名
返回值:如果调用成功,则返回
,否则返回SOCKET_ERROR,可以通过调用WSAGetLastError获得对错误信息的进一步描述。
参数:
name:指向一个一NULL结尾的,表示主机名的字符串
namelen:name的长度,以字节为单位

  1. getservbyname函数
struct servent FAR *getservbyname(
int char FAR *name,
const char FAR *proto
);

getservbyname函数根据服务名和协议读取服务信息
返回值:如果调用成功,则返回servent结构,该结构的定义参见前面第七小节。
参数:
name:指向一个以NULL结尾的,表示服务名的字符串
proto:指向一个以NULL结尾的协议字符串,如果是NULL,则返回第一个服务

  1. getservbyport函数
struct servent FAR *getservbyport(
int port,
const char FAR *proto
);

getservbyport函数根据端口和协议读取服务信息
返回值:如果调用成功,则返回servent结构,该结构的定义参见前面结构说明;否则返回NULL。
可以通过调用WSAGetLastError获得对错误信息的进一步描述,参见前面第七小节。
参数:
port:要查询的端口值
proto:指向一个以NULL结尾的协议字符串,如果是NULL,则返回第一个服务。

6. Windows Socket通信类函数

需要说明的是,这些函数至少时1.1版本,如果要是使用这些函数,需要在文件前面包含头文件,已经静态链接库

#include <Winsock2.h>
#program comment(lib,"Ws2_32.lib")
  1. WSAStartup函数
int WSASartup(
WORD wVersionRequested,
LPWSADATA IpWSAData
);

WSAStartup函数首先查询当前操作系统是否支持所要求的版本号,完成对Windows Socket的初始化工作。要使用Socket2通信,必须首先使用该函数,因此该函数应在逻辑上,处于Socket2所有函数的第一位
返回值:函数如果调用成功,则返回0;否则返回错误码
此时由于Socket机制还没有完全建立,所以还不能通过WSAGetLastError函数获得错误的详细信息。只有该函数返回成功了,之后的操作才能使用WSAGetLastError函数。
参数:
wVersionRequrested:一个WORD类型的变量可以看成两个由BYTE拼成,其中高位字节表示要使用功能的最低副版本号(修订版本号),低位字节表示的是要使用功能的最低主版本号
IpWSAData:指向一个WSADATA结构的指针,该指针列出了当前Windows Sockets的各项实现细节。

  1. WSACleanup函数
intWSACleanup (void)

WSACleanup函数完成与socket库绑定的解除,并释放socket库所占用的系统资源。
该函数应该作为某次socket操作的左后一个函数,否则之后任何socket操作都会导致出错。
返回值:如果调用正确,返回0;佛足额会返回SOCKET_ERROR。可以通过WSAGetLastError函数读取对错误信息的进一步描述。

  1. socket函数
SOCKET socket(
int af,
int type,
int protocol
);

socket函数创建一个sockert套接字,该函数相对简单,但却非常重要,因为这个函数是后面各种操作的基础,还有一点必须提出的是,该协议中type字段将直接决定后面所建立的的连接是面向连接的,还是面向非连接的,这将直接影响后续所有该ssocket的操作:
返回值:如果创建成功,则返回一个socket套接字;否则返回INVALID_SOCKET。可以通过WSAGetLastError函数获得详细错误信息。
参数:
af:网络通信协议族,一般情况下用AF_INET,表示IPv4协议
type:指定协议的采用连接类型,在Windows Docket1.1中只支持以下几种:SOCK_STREAM, SOCK_DGRAM, SOCK_RAW,Winsows Socket 2 及以后,还支持多种类型,可以通过WSAEnumProtocils函数读取
protocol:指定要用的协议。如果第二个参数type不是SOCK_RAW ,则此函数一般是0,表示采用默认协议。如果type是SOCK_RAW ,则此函数就可以指定相应的协议,比如IPPROTO_IP表示采用IP协议,IPPROTO_ICMP表示采用ICMP协议。

  1. closesocket函数
int closesocket(
SOCKET s
);

closesocket关闭之前打开的socket套接字,在进行关闭之前,一般要通过shutdown函数,通知对方自己要关闭套接字。
返回值:如果关闭成功,则返回0;否则返回SOCKET_ERROR。如果想知道详细错误,可以调用WSAGetLasetError函数获得进一步解释
参数:
s:之前打开的套接字

  1. setsocketopt函数
int setsocketopt(
SOCKET s,
int level,
const optname,
const char FAR *optval,
int optlen
);

setsocketopt函数设置一个socket的参数选项。通常情况下,默认的选项就够用,但扫描器本身的特点是,几乎每一个程序都需要修改器默认的选项,所以该函数也是socket中的一个重要的函数,在调用顺序上,如果setsocketopt函数在bind函数之前,则设置的项会在会直到bind函数时才有效。即使setsocketopt功能成功,bind函数也会因为setsocketopt函数过早调用而失败。
与setsocketopt作用相反的一个函数是getsockopt,getsockopt函数的功能是读取一个socket的参数选项,由于其函数参数项数和各项的意义与setsockopt一样,所以此处不再重复。
返回值:如果调用成功,则返回0;否则返回SOCKET_ERROR。可以通过调用WSAGetLastError函数获得对错误信息的进一步解释。
参数:
s:由socket函数创建的socket套接字
level:设置选项所定义的级别,当前支持的级别主要有SOL_SOCKET,IPPROTO_TCP和IPPROTO_IP,分别对应于应用层、传输层、网络层的设置
optname:要设置的选项,这些选项参数一次只能设置一个,所以要同时设置两个或两个以上参数时,需多次重复调用setsockopt函数,并在每次调用时设置一个参数,如果连续重复设置同一个参数,则以最后一次的设置为有效设置。
optval:一个指向选项值的指针,集体的值由optname决定
aptlen:一个指向选项值长度的指针。

  1. select函数
int select(
int nfds,
fd_set FAR *readfds,
fd_set FAR *writefds,
fd_set FAR *exceptfds,
const struct timeval FAR *timeout
);

socket函数的作用是监视阻塞状态下端口的状态,如当前是否有数据到达,从而进入读端口的状态,需要说明的是select函数在windows下和在linux下使用方法有一定差别,select函数最重要的作用是通过readfds参数判断当前socket是否有数据到达,如果到达则转入读状态,否则继续空转或处理其他事务。本书较少采用这一功能来判断是否要数据到达,而是采用阻塞读取,直至超时后退出阻塞的方式,考虑到本书只有少数地方是用到此函数,所以在此只是稍作介绍。
返回值:如果调用成功,则返回所监视的端口处于”准备”状态的socket句柄个数,并且将这些句柄保存在一个fd_set结构中,如果超时,则返回0,否则返回SOCKET_ERROR,可以通过WSAGetLastError函数获得对错误信息的进一步解释。

  1. bind函数
int bind(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);

bind函数可以将一个本地地址昱socket套接字进行绑定。一旦绑定成功,则此后该socket操作将与该地址有关。该函数既可用于面向对象的TVP通信,也可以用于面向非连接的UDP通信中。
返回值:如果调用成功,则返回0,否则返回SOCKET_ERROR,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
参数如下:
s:由socket函数创建的socket套接字
name:分配给该socket套接字的一个地址SOCKADDR结构
namelen:SOCKADDR结构的长度

  1. listen函数
int listen(
SOCKET S,
int backlog
);

listen函数使用socket状态监听状态,并等待其他socket的连接。该函数仅用于面向连接的TCP通信中,UDP通信是不需要listen函数的
返回值:如果调用成功,则返回0,否则返回SOCKET_ERROR,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
参数如下:
s:指向一个由socket的函数创建的socket套接字
backlog:能连接的最大客户端数。如果设置成SOMAXCONN,则服务提供者尽可能地创建最大的值

  1. accept函数
SOCKET accept(
SOCKET s,
struct sockaddr FAR *addr,
int FAR *addrlen
);

函数允许和接收一个远端的连接,该函数仅用于面向连接的TCP通信中,并且只用于服务器端,用于接收客户端通过connect函数发来的连接申请;面向非连接的UDP通信是不需要处理此函数的
调用成功后,该函数将会处于阻塞状态,直到有远端的连接,才会返回,从外表上看程序很像是死掉了,因此除非程序本身没有要求,否则一般建议将此函数放入线程中使用,以避免整个函数像“僵死”一样。
该函数看似简单,其实比较复杂,也是多线程处理效果的关键,首先调用此函数之前程序已成功地调用了listen函数,然后再调用该函数时,如果调用成功,则返回一个新的socket,所以如果后面服务端的处理很简单,可以在当前线程中用这个新创建的,进行处理,俗称“短连接”;如果处理很复杂,并且仍在当前线程中处理,则会影响到accept函数对其他线程通过connect进行连接,此时就需要再创建一个线程,有新建的线程,并使用返回的一个socket专门处理此次连接后的各项操作,俗称“长连接”。
返回值:如果调用成功,则返回接收远端通过connect连接后,新创建的一个socket,否则返回INVALID_SOCKET,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
需要说明的是,新创建的语言有的具有相同的属性,因此不必再初始化。
参数:
s:指向一个有socket函数创建的socket套接字
addr:连接一个socketaddr结构的指针,改制真中保存着远端socket的一些信息。如果该值置成NULL,则表示用户对谁建立的连接并不感兴趣,而只处理内容,其中的socketaddr结构详见前面内容。
addrlen:sockadddr结构的长度,调用之后,函数会返回实际需要的长度,如果该值置成NULL,则表示用户对谁建立的连接并不感兴趣,而只处理内容。

  1. connect函数
int connect(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);

connect函数与客户端的身份与远端主机建立连接。在扫描器的应用中,connect是一种简单而有效的连接方式,连接成功,则可以认为对方的端口是打开的,该函数仅用于面向连接的TCP通信中,并且只用于服务器端,用来接收客户端通过connect函数发来的连接请求,面向非连接的UDP通常是不需要处理参数的。
返回值:如果调用成功,则返回0,否则返回INVALID_SOCKET,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
参数如下:
s:指向一个有socket函数创建的socket套接字
name:指向一个sockaddr的结构指针,该结构中保存了要连接的远端主机的IP地址和端口地址,其中一个sockaddr结构详见前面内容
addrlen:sockadddr结构的长度,调用之后,函数会返回实际需要的长度,如果该值置成NULL,则表示用户对谁建立的连接并不感兴趣,而只处理内容。

  1. send函数
int send(
SOCKET s,
const cahr FAR *buf,
int len,
int flags
);

send函数发送数据到已建立连接的socket上,该函数即可以用于服务器,也可以用于客户端,但双方都必须是采用TCP连接
返回值:如果调用成功,则返回0,否则返回INVALID_SOCKET,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
参数如下:
s:指向一个有socket函数创建的socket套接字
buff:指向一个发送缓冲区的指针
len:buff缓冲区的长度,以字节为单位
flags:发送的方法,一般置0

  1. recv函数
int recv(
SOCKET s,
char FAR *buf,
int len,.
int flags
);

recv函数用于接收从已建立连接的socket上的数据,该函数即可以用于服务器,也可以用于客户端,但双方都必须是采用TCP连接.
返回值:如果调用成功,则返回0,否则返回INVALID_SOCKET,可以通过WSAGetLastError函数获得对错误信息的进一步解释。

  1. shutdown函数
int shutdown(
SOCKET s,
int how
);

shutdown函数禁止当前的发送或接收。对该函数关注的不多,所以可以看到很多程序在关闭的时候在收发完成后,直接就调用函数了,这样做有的时候会使对方仍处于连接中,而己方已断开。
返回值:如果调用成功,则返回0,否则返回INVALID_SOCKET,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
参数如下:
s:要停止发送或接受的socket
how:要关闭的类型,其中how的取值可以是:SD_RECEIVE,SD_SEND,SD_BOTH

  1. sendto函数
int sendto(
SOCKET s,
const char FAR *buf,
int len,
int flags,
xonst struct sockaddr FAR *to,
int tolen
);

sendto函数发送数据报道远端的主机指定的端口上。该函数只能用于面向非连接的通信中。
返回值:如果调用成功,则返回0,否则返回INVALID_SOCKET,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
参数如下:
s:指向一个有socket函数创建的socket套接字
buff:指向一个发送缓冲区的指针
len:buff缓冲区的长度,以字节为单位
flags:发送的方法,一般置0
to:要发送的目标主机IP和端口,该指针指向一个sockaddr结构,该结构详见前面内容。

  1. recvfrom函数
int recvfrom(
SOCKET s,
char FAR *buf,
int len,
int flags,
struct sockaddr FAR *from,
int FAR *fromlen
);

recvfrom函数接收远端发过来的数据报。该函数只能用于面向连接的通信中。
返回值:如果调用成功,则返回实际接收的字节数;否则返回SOCKET_ERROR,可以通过WSAGetLastError函数获得对错误信息的进一步解释。
参数如下:
s:指向一个有socket函数创建的socket套接字
buff:指向一个发送缓冲区的指针
len:buff缓冲区的长度,以字节为单位
flags:发送的方法,一般置0
from:向本身发送数据包的源主机IP和端口,该指针指向一个socketaddr结构,该结构的详细说明在前面结构说明中。
fromlen:sockaddr结构的长度。

7.原始套接字

上述函数介绍中提到一个原始套接字(Raw Socket),如果不使用原始套接字,则无论是发送和接收,系统都会自动处理IP包头,TCP/UDP包头的数据,这时用户最需要关心发送和接收的数据本身即可,这种自动处理虽然方便,但也是系统失去了灵活性。而当使用原始套接字时,如果发送数据系统会将要发送的数据包的前面若干字节数据IP头、TCP/UDP头;如果接收数据,系统会将接收到的数据包前面加上数据IP头、TCP/UDP头。
在所有扫描实例中,绝大多数都是扫描器主动发起探测,但也有少部分扫描器是坐等接收信息,然后对被动接收的数据进行分析,从而得出结论,而这些应用在后面会广泛使用,故在这些专门列出。
该功能只能用于Windows 2000/XP以及以后的版本中,在Windows 95/98/Me/NT中无法使用,如果确实要使用,只能使用钩子(hook)技术,直接从网卡驱动程序中进行截取。
网络扫描技术揭秘学习笔记《一》TCP/IP协议编程
1. 原始套接字的发送
原始套接字的发送很简单,但实际编写却很麻烦,这主要是因为需自己填写IP头和TCP头的数据内容,并分别计算IP头和TCP头的校验和。由于不再使用Socket提供的IP和TCP头,所以需要通过setsocketopt函数告诉系统使用自己定义的IP和TCP头,并且虽然所填的是面向连接的TCP头,仍然要使用UDP所专用得sendto函数,而不是使用send函数。

  1. 原始套接字的接收
    原始套接字的接收相对复杂,步骤较多,但通常情况下,只要按图所示的步骤操作即可,每一个步骤只有一两行语句,不像“原始套接字的发送”中的填充IP头和TCP头那样需要很多行,其中的WSAIoctl函数的SIO_CVALL参数表示接收经过本机网卡的所有数据包。
    网络扫描技术揭秘学习笔记《一》TCP/IP协议编程