winsock 网络安全编程:Winsock编程基础
网络攻防是一个大课题,比如端口扫描、SQL Injection扫描、数据包嗅探、网络密码猜测、后门、木马等基础技术。这些技术在入侵分析中很常见。
在学习扫描器、嗅探器、木马等知识之前,首先要学习网络编程的基础知识。网络编程的基础是深入学习网络的开端。没有基础知识,扫描和嗅探都是空。
01网络基础知识
计算机之间通过互联网的通信主要依靠TCP/IP。该协议是一个4层协议,自上而下由应用层、传输层、互联网层和链路层组成。TCP/IP的下层协议始终服务于上层协议,下层协议的细节对上层协议是透明的。分层设计的优点是每一层的功能明确,修改一层的实现不会影响其他层。TCP/IP在每一层都定义了许多不同的协议,如互联网层的ICMP和IGMP,传输层的TCP和UDP。在众多协议中,最具代表性的协议是TCP和IP,所以互联网协议被称为TCP/IP族。
IP协议是“互联网协议”的缩写,是为计算机网络相互通信而设计的。IP协议中最重要的是IP地址,用来唯一标识网络上的计算机主机。互联网上没有两台机器有相同的IP地址,所以用它来识别网络主机。所有的IP地址都是32位长,用点分十进制表示,如“10.10.30.12”。IP地址指定的不是主机,而是网络接口设备。因此,如果一台主机有两个网络接口,就会有两个IP地址。通常一台普通主机只有一个网络接口设备,IP地址也只有一个。比如个人使用的PC通常只有一个IP地址;用于服务器或网络设备。),会有多个网络接口设备,每个网络接口设备都会有一个IP地址,所以路由器等网络设备也会有多个IP地址。
IP地址分为五类,即a类、b类、c类、d类和e类..各种IP地址的范围如表1所示。
表1各种IP地址的范围
IP工作在TCP/IP第4层协议的“互联网层”,互联网层最重要的工作就是路由数据包。IP是一种路由协议,是指路由协议在路由过程中会用到IP协议。真正的路由协议叫做路由协议,具体的路由协议有RIP、OSPF、BGP等等。首先,你只需要知道一个IP地址是什么,它的功能是什么。
传输层主要有两种协议,分别是TCP和UDP。
TCP是“传输控制协议”的缩写,意思是传输控制协议。TCP是一种面向连接的可靠通信协议。TCP协议是IP协议的上层协议,IP服务于TCP。
UDP是“用户数据报协议”的缩写,意思是用户数据报协议。UDP是一种无连接的传输层协议,提供面向事务的简单、不可靠的信息传输服务。
传输层为应用层提供服务,应用层的协议部分基于TCP,如FTP、HTTP,部分基于UDP,如DNS。IP层提供IP地址来识别网络主机,而传输层提供端口来识别主机中的进程。通过确定IP地址和端口号,可以确定网络上的主机和主机上的通信过程。
传输层提供标识通信过程的端口号。按照协议,端口分为TCP端口和UDP端口,分别有65536个TCP端口和UDP端口。对于应用程序,通常使用大于1024的端口号,因为小于1024的端口号属于保留端口。互联网上的许多服务使用的端口号小于1024。为了避免冲突,程序员不应该在自己的应用程序中使用小于1024的端口号。相同协议的端口不能冲突。比如Web服务器占用了主机TCP的端口80,其他程序就不能再使用TCP的端口80。常用端口号如表2所示。
表2常见端口号示例
除了端口号小于1024之外,还有一些众所周知的端口号,比如MS SQL Server的端口号为1433,Windows Remote Desktop的端口号为3389。程序员在编写自己的网络应用程序时,应该避免与这些常用端口发生冲突。
面向连接的协议和非面向连接的协议使用的功能
1.面向连接的协议
面向连接的协议使用TCP,服务器与客户端建立通信通道所需的基本Winsock功能如下。
服务器端功能:
socket->bind->listen->accept->send/recv->closesocket客户端功能:
socket->connet->send/recv->closesocket2.非面向连接的协议
在非面向连接的协议中,发送方只需要将要直接发送的数据发送出去,而不考虑接收方能否接收到数据。接收端收到数据后,不会响应信息通知,发送给发送端。这种方法相当于写了一封信,把写好的信放在邮箱里,但不能保证收件人真的能收到信。
在非面向连接的协议中使用了UDP,服务器和客户端通信所需的基本Winsock功能如下:
服务器端功能:
socket->bind->sendto/recvfrom->closesocket客户端功能:
socket->sendto/recvfrom->closesocket03Winsock网络编程知识
Winsock是Windows下网络编程的基础,下面介绍Winsock的常用功能。
1.Winsock初始化和发布
使用Winsock相关函数时需要初始化Winsock库,使用完成后需要释放。初始化和释放Winsock库的功能如下。
Winsock库初始化函数的定义;
int WSAStartup;这个函数的第一个参数wVersionRequested是要初始化的Winsock库的版本号。第二个参数lpWSAData是指向WSAData的指针。该函数的返回值为0,表示函数调用成功。如果函数调用失败,则返回其他值。通过在程序开始时调用初始化函数,所有与Winsock相关的API函数都可以在程序中使用。
Winsock库发布函数的定义;
int WSACleanup ;函数没有参数,直接在程序末尾调用函数就可以释放Winsock库。
初始化和释放Winsock库的代码示例如下:
WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD; err = WSAStartup; if { return -1; } if != 2 || HIBYTE != 2 ) { WSACleanup; return -1; } // …… WSACleanup;2.创建和关闭套接字
套接字用于根据指定的协议类型分配套接字描述符。该描述符主要用于客户端和服务器之间的通信。当套接字用完时,应该关闭套接字以释放资源。创建套接字和关闭套接字的功能是socket和closesocket。
创建套接字的函数定义如下:
SOCKET socket;套接字函数中有三个参数。第一个参数af用于指定地址族。Windows下可以使用的参数值很多,但只能使用两个,即AF_INET和PF_INET。这两个宏在Winsock2.h下的定义相同,如下所示:
#define AF_INET 2 /* internetwork: UDP, TCP, etc. */ /* * Protocol families, same as address families for now. */ #define PF_INET AF_INET以上两个定义取自Winsock2.h头文件。从定义可以看出,PF_INET和AF_INET是一样的。看PF_INET宏定义上面的注解,AF代表地址族,PF代表协议族。对于Windows,两者是一样的;对Unix/Linux来说,它们是不同的。一般调用套接字函数时要用到PF_INET,设置地址时要用到AF_INET。上面的备注FP_INET也是来自Winsock2.h头文件。协议系列,目前与地址系列相同。也就是说,目前PF和AF是一样的。说明中说目前是一样的,但是这个定义可能会留到以后,为了保持良好的兼容性。调用套接字函数时,要使用PF_INET宏,尽量避免或不使用AF_INET宏。
套接字函数的第二个参数类型指定了新套接字描述符的类型。这里通常可以使用三个值,分别是SOCK_STREAM、SOCK_DGRAM和SOCK_RAW,分别代表流套接字、包套接字和原始协议接口。
套接字函数的第三个参数协议用于指定应用程序使用的通信协议。这里可以选择IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP等协议。根据第二个参数的值选择该参数的值。如果第二个参数使用了SOCK_STREAM,则为IPPROTO _ TCP应该用于第三个参数。如果第三个参数使用SOCK_DGRAM,那么第三个参数应该使用IPPROTO_UDP。为了编写简单,如果第二个参数是SOCK_STREAM或SOCK_DGRAM,那么第三个参数可以默认为0。如果第二个参数指定了SOCK_RAW,那么必须指定第三个参数,而不是使用值0。
如果成功调用socket函数,它将返回一个新的socket描述符,如果调用失败,它将返回INVALID_SOCKET。调用失败后,如果想知道调用失败的原因,那么就调用WSAGetLastError函数获取错误码。
所有Winsock函数出错后,可以调用WSA GetLasError获取错误代码,但是WSAStartup无法通过WSA GetLasError获取错误代码,因为WSAStartup还没有被成功调用,所以无法调用WSA GetLasError。
关闭套接字的功能定义如下:
int closesocket;closesocket函数的参数是套接字函数创建的套接字描述符。
对于WSAStartup/WSACleanup和socket/closesocket等函数,最好保持成对。也就是说,写一个函数的时候,马上写另一个函数的调用,以免忘记资源的释放。
3.面向连接协议的功能
Bind、listen、accept、connect、send和recv是常用的面向连接的函数,是最基本的Winsock面向连接的函数。下面描述了几个函数的使用。
可以通过socket函数创建一个新的套接字描述符,但它只是一个描述符,为网络的一些资源做准备。要真正在网络上进行通信,您需要本地地址和本地端口号信息。当然,本地地址和端口号信息应该与用于绑定的套接字描述符相关联。在Winsock函数中,使用bind函数完成套接字和地址端口信息的绑定。绑定函数的定义如下:
int bind;该函数有三个参数,第一个参数s是新创建的socket描述符,即socket函数创建的描述符,第二个参数名是sockaddr结构,提供socket的地址和端口信息,第三个参数namelen是sockaddr结构的大小。
第二个参数中的sockaddr结构定义如下:
struct sockaddr { u_short sa_family; char sa_data; };这个结构中有16个字节,在这个结构之前使用的结构是sockaddr_in,定义如下:
struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero; };Sockaddr结构旨在保持特定协议之间结构的兼容性。在为bind函数指定地址和端口时,sockaddr_in结构应该填充相应的内容,调用函数时应该使用sockaddr结构。
在sockaddr_in结构中,还有一个in_addr结构,在winsock2.h中定义如下:
struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; };在这个结构中有一个公共体S_un,它包含两个结构变量和一个u_long类型变量。常用的IP地址用点分十进制表示,但in_addr结构不提供存储用点分十进制表示的IP地址的数据类型。此时,需要使用转换函数将点分十进制表示的IP地址转换为可接受的in_addr结构类型。这里使用的转换函数是inet_addr,定义如下:
unsigned long inet_addr;功能是将点分十进制表示的IP地址转换成无符号长类型的数值。该函数的参数cp是指向点分十进制IP地址的字符指针。同时这个函数还有一个反函数,把无符号长的数字IP地址转换成点分十进制的IP地址串。该功能的定义如下:
char FAR * inet_ntoa;sockaddr_in结构中的Sin_port表示一个端口,在大尾模式下需要按字节顺序存储。在英特尔X86架构下,数值存储方式默认为小尾字节顺序,而TCP/IP的数值存储方式为大尾字节顺序。为了实现方便的转换,winsock2.h提供了方便的函数,即htons和htonl,以及它们的反函数ntohs和ntohl。
函数hton和htonl的定义如下:
u_short htons; u_long htonl;Ntohs和ntohl函数的定义如下:
u_short ntohs; u_long ntohl;这四个功能中,前两个功能是将主机字节顺序转换为主机到网络,后两个功能是将网络字节顺序转换为主机字节顺序。在一些架构系统中,主机字节顺序和网络字节顺序是相同的,所以转换函数不执行任何转换,但是为了代码的可移植性,仍然会调用转换函数。
绑定功能的使用如下:
// 创建套接字 SOCKET sLisent = socket; // 对 sockaddr_in 结构体填充地址、端口等信息 structsockaddr_inServerAddr;ServerAddr.sin_family = AF_INET; ServerAddr.sin_addr.S_un.S_addr = inet_addr;ServerAddr.sin_port = htons;// 绑定套接字与地址信息 bind&ServerAddr, sizeof);服务器端的地址可以指定为INADDR_ANY宏,意思是“任意地址”或“所有地址”。当客户端发起连接时,服务器操作系统从客户端接收连接,并根据网络的配置自动选择一个IP地址与客户端通信。
当套接字与地址和端口信息绑定时,端口必须监听。当端口处于监听状态时,它可以接受其他主机的连接。监听端口和接受连接请求的功能分别是监听和接受。
监听端口的功能定义如下:
int listen;该函数有两个参数,第一个参数s是要监控的套接字描述符,第二个参数backlog是允许进入请求连接队列的数量。积压的最大值由系统指定。在winsock2.h中,其最大值由SOMAXCONN表示,定义如下:
#define SOMAXCONN 0x7fffffff接受连接请求的函数定义如下:
SOCKET accept;这个函数从连接请求队列中获取连接信息,创建一个新的套接字描述符,并获取客户端地址。新创建的套接字用于与客户端通信。服务器和客户端的通信完成后,需要使用closesocket函数关闭套接字,释放相应的资源。该函数有三个参数,第一个参数s是监听下的套接字描述符,第二个参数addr是sockaddr结构的指针,用于返回客户端的地址信息,第三个参数addrlen是指向int类型的指针变量,用于传入sockaddr结构的大小。
上面介绍的是面向连接的服务器端功能,它完成了服务器应该具有的一系列基本动作,如下所示。
;
该函数连接插座。该函数有三个参数,第一个参数s表示创建的套接字描述符,第二个参数名称是sockaddr结构的指针,它存储服务器的IP地址和端口号,第三个参数namelen指定sockaddr结构的长度。
当客户端使用connect函数与服务器连接时,客户端和服务器可以进行通信。沟通主要是发送和接收信息。这里介绍了两个功能,即send和recv。
发送功能发送定义如下:
int send;该函数有四个参数,第一个参数s是套接字描述符。对于服务器端,使用accept函数返回的套接字描述符;对于客户端,使用套接字函数创建的套接字描述符。第二个参数buf是发送消息的缓冲区,第三个参数len是缓冲区的长度,第四个参数flags通常赋值为0。
接收功能recv定义如下:
int recv;该函数有四个参数。该功能的使用与发送功能相同,在此不再赘述。
send和recv的名称分别表示发送和接收,但实际上,数据的发送和接收取决于网络协议,send和recv只完成从网络协议使用的缓冲区复制数据的动作。
4.面向非连接的协议功能
在面向连接的TCP中,服务器将套接字描述符绑定到地址后,需要监听端口,等待客户端的连接请求,而客户端需要连接到服务器。通过完成这些步骤,可以保证面向连接的TCP可靠传输,在调用Connect函数的过程中完成TCP的“三次握手”。非面向连接的UDP协议在开发上与面向连接的TCP协议基本相同。在非面向连接的UDP开发中,服务器不需要监控端口,所以不需要等待客户端的连接请求,客户端也不需要完成与服务器的连接。中间的“三次握手”过程被省略了,所以UDP相比TCP不可靠,但在效率上比TCP更快。
在非面向连接的协议开发中,服务器不再需要调用监听和接受功能,客户端也不再需要调用连接功能。服务器和客户端之间的通信功能可以使用sendto和recvfrom功能。
Sendto函数的定义如下:
int sendto;此功能用于在UDP通信方之间发送数据。这个函数有六个参数,第一个参数S是套接字描述符,第二个参数buf是发送数据的缓冲区,第三个参数len指定第二个参数的长度,第四个参数通常赋值为0,第五个参数to是sockaddr结构的指针,这里给出了接收消息的地址信息,第六个参数tolen指定第五个参数的长度。
recvfrom功能定义如下:
int recvfrom;该功能用于接收UDP通信方之间的数据。这个函数的用法和sendto一样,这里就不介绍了。
sendto函数和recvfrom函数的功能与send函数和recv函数的功能相似。它们都是将数据复制到网络协议缓冲区的功能,并不真正用于发送和接收数据。
04字节顺序
字节顺序的存在是由于不同架构的CPU在访问数据时采用的顺序不同。在计算机内存中存储数值有一定的标准,不同的系统架构会有所不同。理解字节存储顺序是逆向工程的基础知识。在动态分析程序时,经常需要观察内存数据的变化。如果不理解字节存储顺序,可能会迷失在浩瀚的记忆海洋中,无法继续逆水行舟。
1.字节顺序基础
通常在内存中存储值有两种方式,一种是大尾方式,另一种是小尾方式。
表3字节顺序对照表
从表中可以得出以下结论。
大尾存储方式:内存高地址存储数据低字节数据,内存低地址存储数据高字节数据;
小尾存储方式:内存高地址存储高字节数据,内存低地址存储低字节数据。
2.主机字节顺序和网络字节顺序
主机字节顺序和网络字节顺序是相对的概念。
所谓主机字节顺序是指主机存储数据时的字节顺序,主机字节顺序根据不同的系统架构而不同。一般来说,兼容Windows操作系统的CPU是小尾模式,而兼容UNIX操作系统的CPU大多是大尾模式。因此,主机字节顺序不是固定的字节顺序,需要根据不同的系统架构来确定。
所谓网络字节顺序是指网络传输相关协议规定的字节传输顺序,TCP/IP使用的字节顺序是大尾巴模式。
3.字节顺序相关函数
涉及字节顺序的常用相关函数有hton、htonl、ntohs和ntohl。四种功能定义如下:
u_short htons; u_long htonl; u_short ntohs; u_long ntohl;在Windows下,使用以上四个转换函数会改变值的大小,因为它在内存中的存储模式已经改变。如果在UNIX系统下使用以上四个转换函数,什么都不会改变。无论是什么样的系统,都需要在网络开始时调用这些函数进行转换,因为这样做可以有效地保证网络中传输的确实是网络字节顺序。
4.判断主机字节顺序的编程
“编程判断主机字节顺序”是很多杀毒软件公司或安全开发岗位的面试问题,因为比较基础。这里给出了这个主题的实现方法。完成本课题有两种方法,第一种方法是“价值比较法”,第二种方法是“直接转换比较法”。
方法1:价值比较法
所谓的值比较方法首先定义一个4字节的十六进制数。因为使用调试器查看内存最直观的方式是十六进制值,所以定义十六进制数是一种直观的操作方式。然后通过指针的方式在“内存”中取出这个十六进制数的某个字节,最后与实际值中对应的数字进行比较。由于字节顺序问题,内存中的一个字节可能与实际值中的对应字节不同,因此可以确定字节顺序。
代码如下:
int main { DWORD dwSmallNum = 0x01020304; if &dwSmallNum == 0x04 ) { printf; } else { printf; } return 0; }方法二:直接转换比较法
所谓直接转换比较法,是利用字节顺序转换函数将定义的值进行转换,然后将转换后的值与原值进行比较。如果原值与转换值相同,则表示大尾模式,否则表示小尾模式。
代码如下:
int main { DWORD dwSmallNum = 0x01020304; if ) { printf; } else { printf; } return 0; }这种方法相对简单。如果转换结果等于原值,说明是大尾巴模式,因为转换结果是网络字节顺序,相当于大尾巴模式。
字节顺序的内容一定要自己调试理解,因为在网络开发中,只需要进行简单的转换,不需要太在意它的细节。而且如果是逆向工程,在内存中搜索数据时,会用到字节顺序的知识。