1. 网络中的四层
应用层 |
---|
传输层 |
网络层 |
数据链路层 |
数据链路层:解决点对点的通讯 (mac地址)
网络层:解决主机到主机的通讯 (ip地址)
传输层:解决一台主机的任意的进程和另一台主机的任意进程的通讯 (端口)
应用层:解决应用程序个各种业务问题
1.1 传输层
tcp套接字的使用
int fd = socket(AF_INET,SOCK_STREAM,0);
AF_INET/AF_INET6 ipv4/ipv6
这种套接字可以直接使用tcp协议,发送和接受都是只要tcp的数据区的内容,无需关注tcp报文的内容
udp套接字的使用
int fd = socket(AF_INET,SOCK_DGRAM,0);
AF_INET/AF_INET6 ipv4/ipv6
和tcp套接字一样只需要关注收发的数据
1.2 网络层
网络层的原始套接字使用
int sfd = socket(AF_INET, SOCK_RAW, IPPROTO_xxx);
AF_INET/AF_INET6 ipv4/ipv6
AF_INET和SOCK_RAW组合表示网络层的原始套接字
IPPROTO_xxx 协议
这里的协议表示封装在ip报文中的协议
协议可选定义在 /usr/include/netinet/in.h 头文件中
enum
{
IPPROTO_IP = 0, /* Dummy protocol for TCP. */
#define IPPROTO_IP IPPROTO_IP
IPPROTO_ICMP = 1, /* Internet Control Message Protocol. */
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IGMP = 2, /* Internet Group Management Protocol. */
#define IPPROTO_IGMP IPPROTO_IGMP
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94). */
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6, /* Transmission Control Protocol. */
#define IPPROTO_TCP IPPROTO_TCP
IPPROTO_EGP = 8, /* Exterior Gateway Protocol. */
#define IPPROTO_EGP IPPROTO_EGP
IPPROTO_PUP = 12, /* PUP protocol. */
#define IPPROTO_PUP IPPROTO_PUP
IPPROTO_UDP = 17, /* User Datagram Protocol. */
#define IPPROTO_UDP IPPROTO_UDP
IPPROTO_IDP = 22, /* XNS IDP protocol. */
#define IPPROTO_IDP IPPROTO_IDP
IPPROTO_TP = 29, /* SO Transport Protocol Class 4. */
#define IPPROTO_TP IPPROTO_TP
IPPROTO_DCCP = 33, /* Datagram Congestion Control Protocol. */
#define IPPROTO_DCCP IPPROTO_DCCP
IPPROTO_IPV6 = 41, /* IPv6 header. */
#define IPPROTO_IPV6 IPPROTO_IPV6
IPPROTO_RSVP = 46, /* Reservation Protocol. */
#define IPPROTO_RSVP IPPROTO_RSVP
IPPROTO_GRE = 47, /* General Routing Encapsulation. */
#define IPPROTO_GRE IPPROTO_GRE
IPPROTO_ESP = 50, /* encapsulating security payload. */
#define IPPROTO_ESP IPPROTO_ESP
IPPROTO_AH = 51, /* authentication header. */
#define IPPROTO_AH IPPROTO_AH
IPPROTO_MTP = 92, /* Multicast Transport Protocol. */
#define IPPROTO_MTP IPPROTO_MTP
IPPROTO_BEETPH = 94, /* IP option pseudo header for BEET. */
#define IPPROTO_BEETPH IPPROTO_BEETPH
IPPROTO_ENCAP = 98, /* Encapsulation Header. */
#define IPPROTO_ENCAP IPPROTO_ENCAP
IPPROTO_PIM = 103, /* Protocol Independent Multicast. */
#define IPPROTO_PIM IPPROTO_PIM
IPPROTO_COMP = 108, /* Compression Header Protocol. */
#define IPPROTO_COMP IPPROTO_COMP
IPPROTO_SCTP = 132, /* Stream Control Transmission Protocol. */
#define IPPROTO_SCTP IPPROTO_SCTP
IPPROTO_UDPLITE = 136, /* UDP-Lite protocol. */
#define IPPROTO_UDPLITE IPPROTO_UDPLITE
IPPROTO_MPLS = 137, /* MPLS in IP. */
#define IPPROTO_MPLS IPPROTO_MPLS
IPPROTO_RAW = 255, /* Raw IP packets. */
#define IPPROTO_RAW IPPROTO_RAW
IPPROTO_MAX
};
比较常用的是IPPROTO_TCP/IPPROTO_UDP/IPPROTO_ICMP,tcp/udp/icmp协议
这些字段填入第三个参数,这个原始套接字就会接受协议的数据,数据是从ip协议开始的。默认的发送数据不需要填充ip协议
如果要自己构造ip协议需要开启IP_HDRINCL选项
int ip_on = 1;
setsockopt(sfd, IPPROTO_IP, IP_HDRINCL, &ip_on, sizeof(int));
这一层中常用的结构体都在 /usr/include/netinet目录下
ip协议结构体,tcp协议结构体
//ip协议结构体
struct ip
{
#if __BYTE_ORDER == __LITTLE_ENDIAN //大小端影响
unsigned int ip_hl:4; //ip头的长度 ip_hl *4
unsigned int ip_v:4; //版本
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
unsigned int ip_v:4; /* version */
unsigned int ip_hl:4; /* header length */
#endif
uint8_t ip_tos; //tos服务类型
unsigned short ip_len; //整个ip包的总长度
unsigned short ip_id; //标识 每发送一个数据报,其值就加1
unsigned short ip_off; //ip分片相关
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
uint8_t ip_ttl; //生命周期
uint8_t ip_p; //协议
unsigned short ip_sum; //src校验和
struct in_addr ip_src, ip_dst; //源ip和目的ip
};
//tcp协议结构体
struct tcphdr
{
__extension__ union
{
struct
{
uint16_t th_sport; //源端口号
uint16_t th_dport; //目的端口号
tcp_seq th_seq; //发送序列化
tcp_seq th_ack; //确认序列化
# if __BYTE_ORDER == __LITTLE_ENDIAN
uint8_t th_x2:4; //保留
uint8_t th_off:4; //tcp数据区的偏移(可以理解为tcp头的大小) 值是th_off * 4
# endif
# if __BYTE_ORDER == __BIG_ENDIAN
uint8_t th_off:4; /* data offset */
uint8_t th_x2:4; /* (unused) */
# endif
uint8_t th_flags; //tcp flasg syn ack等标识,对应的值赋值推荐下面的结构体定义
# define TH_FIN 0x01
# define TH_SYN 0x02
# define TH_RST 0x04
# define TH_PUSH 0x08
# define TH_ACK 0x10
# define TH_URG 0x20
uint16_t th_win; //窗口大小
uint16_t th_sum; //crc校验和
uint16_t th_urp; //紧急指针
};
struct //和上面结构体类似 但是标志位赋值更为方便
{
uint16_t source;
uint16_t dest;
uint32_t seq;
uint32_t ack_seq;
# if __BYTE_ORDER == __LITTLE_ENDIAN
uint16_t res1:4;
uint16_t doff:4;
uint16_t fin:1;
uint16_t syn:1;
uint16_t rst:1;
uint16_t psh:1;
uint16_t ack:1;
uint16_t urg:1;
uint16_t res2:2;
# elif __BYTE_ORDER == __BIG_ENDIAN
uint16_t doff:4;
uint16_t res1:4;
uint16_t res2:2;
uint16_t urg:1;
uint16_t ack:1;
uint16_t psh:1;
uint16_t rst:1;
uint16_t syn:1;
uint16_t fin:1;
# else
# error "Adjust your <bits/endian.h> defines"
# endif
uint16_t window;
uint16_t check;
uint16_t urg_ptr;
};
};
};
1.2.1 校验和算法
unsigned short crcsum(unsigned short *addr,int len)
{
int nleft=len;
unsigned int sum=0;
unsigned short *w=addr;
unsigned short answer=0;
while (nleft > 1) {
sum+=*w++;
nleft-=2;
}
if (nleft == 1) {
*(unsigned char *)(&answer)=*(unsigned char *)w;
sum+=answer;
}
sum=(sum>>16)+(sum&0xffff);
sum+=(sum>>16);
answer=~sum;
return answer;
}
1.2.2 ip封包
ip报文的校验和计算
iphead->ip_sum = htons(crcsum((unsigned short *)iphead,sizeof(struct ip)));
ip数据包的检验和只需要把ip头进行计算
ip封包函数
int ip_packet(struct ip *iphead,char *src_ip,char *dst_ip,uint16_t len)
{
if (!iphead) return -1;
iphead->ip_v = 4; //版本
iphead->ip_hl = 5; //ip头的长度 ip_hl *4
iphead->ip_tos = 0; //tos服务类型
iphead->ip_len = htons(len); //整个ip数据包的总长度
iphead->ip_id = htons(54050); //标识 每发送一个数据报,其值就加1 (随机给)
//3位标志字段地第一位保留。第二位表示"禁止分片"。如果设置了这个位,IP模块将不对数据报进行分片。在这种情况下,如果IP数据报长度超过MTU的话,IP模块将丢弃该数据报并返回一个ICMP差错报文。第三位表示“更多分片”。除了数据报的最后一个分片外,其他分片都要把它置1
iphead->ip_off = 0;
iphead->ip_off = htons(iphead->ip_off |= IP_DF);
iphead->ip_ttl = 64; //生命周期
iphead->ip_p = IPPROTO_TCP; //协议
iphead->ip_sum = 0; //src校验和 等整个数据包填充完成后计算
inet_aton(src_ip, &iphead->ip_src); //源ip
inet_aton(dst_ip, &iphead->ip_dst); //目的ip
iphead->ip_sum = htons(crcsum((unsigned short *)iphead,sizeof(struct ip)));
return 0;
}
简单的封装一下ip数据包。大部分字段的值可以使用抓包工具去查看
1.2.3 tcp封包
tcp报文的校验和计算
不同于ip校验和,tcp计算校验和需要整个tcp报文加上伪首部
伪首部的定义
struct tcp_checksum
{
struct in_addr ip_src; //源地址
struct in_addr ip_dst; //目的地址
uint8_t reserve; //保留
uint8_t protocol; //协议 tcps是6 IPPROTO_TCP
uint16_t len; //tcp报文的大小
uint8_t data[0]; //可以是tcp报文 / udp报文
};
校验函数
#define CTCPHEADLEN 12
int tcp_checksum(struct tcphdr *tcphead,struct in_addr ip_src,struct in_addr ip_dst,int len)
{
//tcp + 伪首部长度
struct tcp_checksum *tcs = calloc(1,len + CTCPHEADLEN);
tcs->ip_src = ip_src;
tcs->ip_dst = ip_dst;
tcs->protocol = IPPROTO_TCP;
tcs->reserve = 0;
tcs->len = htons(len);
memcpy(&(tcs->data),tcphead,len);
tcphead->th_sum = crcsum((unsigned short *)tcs,len + CTCPHEADLEN);
free(tcs);
return 0;
}
tcp封包函数
int tcp_packet(struct tcphdr *tcphead,uint16_t sport,uint16_t dport,uint8_t headlen)
{
if (!tcphead) return -1;
tcphead->th_sport = htons(sport); //源端口号
tcphead->th_dport = htons(dport); //目的端口号
tcphead->th_seq = htonl(12345); //发送序列化
tcphead->th_ack = 0; //确认序列化
tcphead->th_off = headlen >> 2; //tcp数据区的偏移 th_off * 4
tcphead->th_flags = 0; //清空flags位 这个地方是tcp中比较重要的地方 syn ack等标志位这个函数不做封装
tcphead->th_win = htons(64240); //窗口大小
tcphead->th_sum = 0; //crc校验和
tcphead->th_urp = 0; //紧急指针
return 0;
}
这个函数没有算校验和 和 tcp标志位的封装,函数可用于二次封装
1.2.4 发包
使用sendto函数
//填充地址
struct sockaddr_in dest_addr = {0};
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(DST_PORT);
inet_aton(DST_IP, &dest_addr.sin_addr);
int ret = sendto(sfd,buf,ntohs(iphead->ip_len),0,(struct sockaddr *)&dest_addr,sizeof(struct sockaddr_in));
必须使用sendto函数。因为我踩过坑之前我构造了ip+tcp包去使用send函数去发送,结构报了 “Destination address required” 的错误
不要觉得构造了ip头,应该不需要发送ip。切记切记大坑
1.3 数据链路层
数据链路层的原始套接字使用
int sfd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_xxx));
AF_PACKET 表示使用数据链路层的原始套
SOCK_DGRAM 表示数据是从网络层开始的 SOCK_RAW表示数据是包含mac头的
ETH_P_xxx 协议,可以是ip arp等
第三个参数的取值在 /usr/include/linux/if_ether.h
#define ETH_P_LOOP 0x0060 /* Ethernet Loopback packet */
#define ETH_P_PUP 0x0200 /* Xerox PUP packet */
#define ETH_P_PUPAT 0x0201 /* Xerox PUP Addr Trans packet */
#define ETH_P_TSN 0x22F0 /* TSN (IEEE 1722) packet */
#define ETH_P_ERSPAN2 0x22EB /* ERSPAN version 2 (type III) */
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
#define ETH_P_X25 0x0805 /* CCITT X.25 */
#define ETH_P_ARP 0x0806 /* Address Resolution packet */
...
比较常用的是
ETH_P_IP 0x800 只接收发往本机mac的ip类型的数据帧
ETH_P_ARP 0x806 只接受发往本机mac的arp类型的数据帧
ETH_P_RARP 0x8035 只接受发往本机mac的rarp类型的数据帧
ETH_P_ALL 0x3 接收发往本机mac的所有类型的数据帧, 接收从本机发出的所有类型的数据帧.
在第三个参数如果只写一种协议,那么发往接受只能是这个协议,并且无法接受发往别的地方的数据包,只能接受
如果是ETH_P_ALL那么是可以接受发往别的地方的数据包,这个可以用于做抓包
1.3.1发包
使用sendto函数
#include <linux/if_packet.h>
//填充这个结构
struct sockaddr_ll {
unsigned short sll_family; /* Always AF_PACKET */
unsigned short sll_protocol; /* Physical-layer protocol */
int sll_ifindex; /* Interface number */
unsigned short sll_hatype; /* ARP hardware type */
unsigned char sll_pkttype; /* Packet type */
unsigned char sll_halen; /* Length of address */
unsigned char sll_addr[8]; /* Physical-layer address */
};
和sockaddr_in类似。
sockaddr_ll addr;
sendto(fd,buf,len,0,(struct sockaddr *)&addr,sizoef(addr));
没发过链路层的包给不了填充的代码。
2.原始套接字案例
2.1 syn tcp包发送
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#define SET_U_1(N,D) (*((uint8_t *)(N++)) = D)
#define SET_U_2(N,D) do{ \
(*((uint16_t *)(N)) = htons(D));N+=2;\
}while(0)
int tcp_syn_opt(struct tcphdr *tcphead)
{
uint8_t *p = (uint8_t *)(tcphead + 1);
//最大报文段长度(MSS)选项 kind = 2
SET_U_1(p,TCPOPT_MAXSEG);
SET_U_1(p,4);
SET_U_2(p,1460);
//窗口扩大因子选项 kind = 2
SET_U_1(p,TCPOPT_NOP);
SET_U_1(p,TCPOPT_WINDOW);
SET_U_1(p,3);
SET_U_1(p,8);
//选择性确认(Selective Acknowledgment,SACK)选项 kind=4
SET_U_1(p,TCPOPT_NOP);
SET_U_1(p,TCPOPT_NOP);
SET_U_1(p,TCPOPT_SACK_PERMITTED);
SET_U_1(p,2);
return 0;
}
#define DST_IP "192.168.224.132"
#define DST_PORT 80
int main(int argc,char *argv[])
{
//接受tcp报文
int sfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (sfd == -1) {
perror("create socket err");
return -1;
}
//开启自己构造ip包
int ip_on = 1;
setsockopt(sfd, IPPROTO_IP, IP_HDRINCL, &ip_on, sizeof(int));
//缓冲器定义
char buf[BUFFSIZE] = {0};
struct ip *iphead = (struct ip *)buf;
struct tcphdr *tcphead = (struct tcphdr *)(iphead + 1);
//封装ip包
ip_packet(iphead,"172.30.201.131",DST_IP,IPPACKSIZE + TCPPACKSIZE + TCPSYNOPTSIZE);
//封装tcp包
tcp_packet(tcphead,60334,DST_PORT,32);
//tcp可选字段 (抓一个syn跟着填充)
tcp_syn_opt(tcphead);
//tcp数据包的长度
uint16_t len = ntohs(iphead->ip_len) - sizeof(struct ip);
tcp_checksum(tcphead,iphead->ip_src,iphead->ip_dst,len);
//构造地址,发包
struct sockaddr_in dest_addr = {0};
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(DST_PORT);
inet_aton(DST_IP, &dest_addr.sin_addr);
int ret = sendto(sfd,buf,ntohs(iphead->ip_len),0,(struct sockaddr *)&dest_addr,sizeof(struct sockaddr_in));
//int ret = send(sfd,buf,ntohs(iphead->ip_len),0);
if (ret == -1) {
perror("send pack error");
return -1;
}
char ip_src[IPLEN] = {0};
char ip_dst[IPLEN] = {0};
while (1) {
memset(buf,0,BUFFSIZE);
recv(sfd,buf,BUFFSIZE,0);
memcpy(ip_src,inet_ntoa(iphead->ip_src),IPLEN);
memcpy(ip_dst,inet_ntoa(iphead->ip_dst),IPLEN);
uint16_t sport = ntohs(tcphead->source);
uint16_t dport = ntohs(tcphead->dest);
//过滤输出
if (sport == DST_PORT) {
printf("src ip %s port %d - dst ip %s port:%d\n",ip_src,sport,ip_dst,dport);
printf("ack flag = %d\n",tcphead->ack);
printf("syn flag = %d\n",tcphead->syn);
printf("seq num = %u\n",ntohl(tcphead->th_seq));
printf("ack num = %u\n",ntohl(tcphead->th_ack));
}
}
return 0;
}
-
“172.30.201.131” 是我本地WSL的地址
-
“192.168.224.132” 是虚拟机的地址
-
重复的代码不再展现
看效果:
1.在"192.168.224.132" 是虚拟机启动一个80tcp服务,打开Wireshark
2.在windows打开抓包工具(切记选对网卡),在wsl上编译完代码
3.查看结果
windows
虚拟机
可以看到回的包中的ack数字,和我们一开始填充tcp seq刚好是+1的关系。说明我们正确的收到了回应包。
当然先声明代码仅供学习参考。如果你只是想实验syn泛洪攻击可以使用虚拟机试用。
这段代码可以去判断对端主机的端口有没有开,如果你收到对应的ack包那么可以确认对端端口是看。当然要设置一个超时时间。
多端口扫描可以创建多个这样的原始套接字,然后开这些套接字加入到epoll去管理,让每个fd携带超时时间,当epoll超时返回是去判断。这样子去实现端口扫描。当然这个思路仅供参考,由于我也没这个需求所以没有相关代码。
2.2 简单抓包工具
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <net/if_packet.h>
#include <netinet/if_ether.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/ip.h>
#include <netpacket/packet.h>
#include <sys/socket.h>
#define SRCIP "172.16.253.52"
#define SRCIPLEN 13
#define IPLEN 16
struct port_flow
{
uint8_t port_type;
char ip[IPLEN];
uint16_t port;
uint64_t fsum;
};
#define PORT_TCP 1
#define PORT_UDP 2
struct flow_sum
{
time_t stime; //起始统计流量的时间
time_t tl; //统计时长
time_t etime; //结束时间
struct port_flow in_flow[10];
struct port_flow out_flow[10];
};
struct tcp_opt
{
uint8_t kind;
uint8_t length;
};
int main(int argc,char *argv[])
{
char buf[2048] = {0};
//创建数据链路层的套接字 SOCK_DGRAM 表示内核封装mac层接收的数据是从ip层开始的 SOCK_RAW可以自己处理mac的数据
int sfd = socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL));
if (sfd == -1) {
perror("create socket err");
return -1;
}
struct ip *iphead = (struct ip *)buf;
struct tcphdr *tcphead = (struct tcphdr *)(iphead + 1);
struct tcp_opt *t_op = (struct tcp_opt *)(tcphead + 1);
//设置超时时间
struct flow_sum flow = {0};
flow.stime = time(NULL);
flow.tl = 10;
flow.etime = flow.stime + flow.tl;
char ip_src[IPLEN] = {0};
char ip_dst[IPLEN] = {0};
while (1) {
memset(buf,0,sizeof(buf));
int n = recv(sfd,buf,sizeof(buf),0); //抓包
if (n <= 0) {
perror("recvform error");
continue;
}
memcpy(ip_src,inet_ntoa(iphead->ip_src),IPLEN);
memcpy(ip_dst,inet_ntoa(iphead->ip_dst),IPLEN);
uint16_t sport = ntohs(tcphead->source);
uint16_t dport = ntohs(tcphead->dest);
//tcp协议过滤
if (iphead->ip_p == IPPROTO_TCP) {
//端口过滤
if ((dport == 80) || (sport == 80)) {
struct port_flow *pf = &flow.in_flow[0];
memcpy(pf->ip,ip_src,IPLEN);
pf->port = ntohs(tcphead->source);
pf->port_type = PORT_TCP;
pf->fsum += (ntohs(iphead->ip_len) + sizeof(struct ether_header) + 8 + 4);
printf("src ip %s port %d - dst ip %s port:%d\n",ip_src,sport,ip_dst,dport);
printf("ack flag = %d\n",tcphead->ack);
printf("syn flag = %d\n",tcphead->syn);
printf("seq num = %u\n",ntohs(tcphead->th_seq));
printf("ack num = %u\n",ntohs(tcphead->th_ack));
//printf("ip+tcp+data size %d\n %s\n",(ntohs(iphead->ip_len)),(char *)t_op);
}
}
if (time(NULL) > flow.etime) {
break;
}
}
//10s内http的流量
printf("10s http in flow %ld\n",flow.in_flow[0].fsum/10);
return 0;
}
这段代码起源于我的业务,由于我需要去统计各个端口的出入流量而写的demo。
代码比较简单,创建完原始套接字之后直接去recv即可
让我们对比tcpdump的效果
可以看看这四元组是一样的