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,
#define IPPROTO_IP IPPROTO_IP
IPPROTO_ICMP = 1,
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IGMP = 2,
#define IPPROTO_IGMP IPPROTO_IGMP
IPPROTO_IPIP = 4,
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6,
#define IPPROTO_TCP IPPROTO_TCP
IPPROTO_EGP = 8,
#define IPPROTO_EGP IPPROTO_EGP
IPPROTO_PUP = 12,
#define IPPROTO_PUP IPPROTO_PUP
IPPROTO_UDP = 17,
#define IPPROTO_UDP IPPROTO_UDP
IPPROTO_IDP = 22,
#define IPPROTO_IDP IPPROTO_IDP
IPPROTO_TP = 29,
#define IPPROTO_TP IPPROTO_TP
IPPROTO_DCCP = 33,
#define IPPROTO_DCCP IPPROTO_DCCP
IPPROTO_IPV6 = 41,
#define IPPROTO_IPV6 IPPROTO_IPV6
IPPROTO_RSVP = 46,
#define IPPROTO_RSVP IPPROTO_RSVP
IPPROTO_GRE = 47,
#define IPPROTO_GRE IPPROTO_GRE
IPPROTO_ESP = 50,
#define IPPROTO_ESP IPPROTO_ESP
IPPROTO_AH = 51,
#define IPPROTO_AH IPPROTO_AH
IPPROTO_MTP = 92,
#define IPPROTO_MTP IPPROTO_MTP
IPPROTO_BEETPH = 94,
#define IPPROTO_BEETPH IPPROTO_BEETPH
IPPROTO_ENCAP = 98,
#define IPPROTO_ENCAP IPPROTO_ENCAP
IPPROTO_PIM = 103,
#define IPPROTO_PIM IPPROTO_PIM
IPPROTO_COMP = 108,
#define IPPROTO_COMP IPPROTO_COMP
IPPROTO_SCTP = 132,
#define IPPROTO_SCTP IPPROTO_SCTP
IPPROTO_UDPLITE = 136,
#define IPPROTO_UDPLITE IPPROTO_UDPLITE
IPPROTO_MPLS = 137,
#define IPPROTO_MPLS IPPROTO_MPLS
IPPROTO_RAW = 255,
#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协议结构体
struct ip
{
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ip_hl:4;
unsigned int ip_v:4;
#endif
#if __BYTE_ORDER == __BIG_ENDIAN
unsigned int ip_v:4;
unsigned int ip_hl:4;
#endif
uint8_t ip_tos;
unsigned short ip_len;
unsigned short ip_id;
unsigned short ip_off;
#define IP_RF 0x8000
#define IP_DF 0x4000
#define IP_MF 0x2000
#define IP_OFFMASK 0x1fff
uint8_t ip_ttl;
uint8_t ip_p;
unsigned short ip_sum;
struct in_addr ip_src, ip_dst;
};
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;
# endif
# if __BYTE_ORDER == __BIG_ENDIAN
uint8_t th_off:4;
uint8_t th_x2:4;
# endif
uint8_t th_flags;
# 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;
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;
iphead->ip_tos = 0;
iphead->ip_len = htons(len);
iphead->ip_id = htons(54050);
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;
inet_aton(src_ip, &iphead->ip_src);
inet_aton(dst_ip, &iphead->ip_dst);
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;
uint16_t len;
uint8_t data[0];
};
校验函数
#define CTCPHEADLEN 12
int tcp_checksum(struct tcphdr *tcphead,struct in_addr ip_src,struct in_addr ip_dst,int len)
{
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;
tcphead->th_flags = 0;
tcphead->th_win = htons(64240);
tcphead->th_sum = 0;
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
#define ETH_P_PUP 0x0200
#define ETH_P_PUPAT 0x0201
#define ETH_P_TSN 0x22F0
#define ETH_P_ERSPAN2 0x22EB
#define ETH_P_IP 0x0800
#define ETH_P_X25 0x0805
#define ETH_P_ARP 0x0806
...
比较常用的是
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;
unsigned short sll_protocol;
int sll_ifindex;
unsigned short sll_hatype;
unsigned char sll_pkttype;
unsigned char sll_halen;
unsigned char sll_addr[8];
};
和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);
SET_U_1(p,TCPOPT_MAXSEG);
SET_U_1(p,4);
SET_U_2(p,1460);
SET_U_1(p,TCPOPT_NOP);
SET_U_1(p,TCPOPT_WINDOW);
SET_U_1(p,3);
SET_U_1(p,8);
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[])
{
int sfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (sfd == -1) {
perror("create socket err");
return -1;
}
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_packet(iphead,"172.30.201.131",DST_IP,IPPACKSIZE + TCPPACKSIZE + TCPSYNOPTSIZE);
tcp_packet(tcphead,60334,DST_PORT,32);
tcp_syn_opt(tcphead);
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));
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;
}
看效果:
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};
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);
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));
}
}
if (time(NULL) > flow.etime) {
break;
}
}
printf("10s http in flow %ld\n",flow.in_flow[0].fsum/10);
return 0;
}
这段代码起源于我的业务,由于我需要去统计各个端口的出入流量而写的demo。
代码比较简单,创建完原始套接字之后直接去recv即可
让我们对比tcpdump的效果
可以看看这四元组是一样的
|