一 概述
在了解了tcpdump的原理后,你有没有想过自己去实现抓包过滤? 可能你脑子里有个大概的思路,但是知道了理论知识,其实并不能代表你完全的理解。只要运用后,你才知道哪些点需要注意,之前没有考虑到的。
在写代码前,先捋下思路,和相应的理论知识。
libpcap 库中实现抓包关键代码
sock_fd = cooked ?
socket(PF_PACKET, SOCK_DGRAM, protocol) :
socket(PF_PACKET, SOCK_RAW, protocol);
libpcap库中pcap_open_live 函数最终会调用上面这行代码,而创建的这socket就可以接收数据链路层的数据包。而protocol 可以指定数据链路层协议帧类型,例如IPv4帧,可以传入htons(ETH_P_IP),接收到数据链路层所有协议帧,可以传入htons(ETH_P_ALL)
· socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))
当指定SOCK_DGRAM时,获取的数据包是去掉了数据链路层的头(link-layer header)
· socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))
当指定SOCK_RAW时,获取的数据包是一个完整的数据链路层数据包
已经可以抓数据链路层的数据包,但是如何设置过滤规则呢?
在libpcap 设置过滤规则用到了两个接口,pcap_compile()和 pcap_setfilter ()
函数,其中pcap_compile()主要将我们输入的过滤规则表达式编译成BPF代码,然后存入bpf_program的结构中。而pcap_setfilter 就是将过滤的规则注入内核。这里不关注pcap_compile 如何编译成bpf代码,然后存入bpf_program。 bpf深入的话,其实里面的东西还有很多(例如输入的过滤的规则怎么转发对应的bpf代码,应用层是如何注入bpf规则到内核,内核又是如何不必要重新编译代码,就可以怎么根据不同的socket上的bpf规则过滤数据包的,等等)。言归正传,主要看pcap_setfilter 关键代码,如下
struct sock_fprog *fcode
ret = setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,
fcode, sizeof(*fcode));
其实在liunx上,你只需要简单的创建你的filter代码,通过SO_ATTTACH_FILTER选项发送到内核,并且你的filter代码能通过内核的检查,这样你就可以立即过滤socket上面的数据了。
而 struct sock_fprog 结构如下
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
其中code元素是一个16位宽的操作码,具有特定的指令编码。jt和jf是两个8位宽的跳转目标,一个用于条件“跳转如果真”,另一个“跳转如果假”。最后k元素包含一个可以用不同方式解析的杂项参数,依赖于code给定的指令。
那么过滤规则如何转化为sock_filter 结构体对应的规则呢?不是说不需要深入bpf,其实tcpdump 提供了一种方法,可以将过滤规则转化成对应liunx c下sock_filter 规则。
例如你需要过滤经过本机端口22所有的数据。在liunx 终端安装了tcpdump 。只需在终端输入 tcpdump port 22 -nn -dd 就可生产对应规则,如下图
至此你其实已经完全可以根据要过滤的包,自己实现抓包过滤了。程序如下
int create_link_raw_socket(){
struct sock_filter bpf_code[] = {
// tcpdump port 22 -nn -dd
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 8, 0x000086dd },
{ 0x30, 0, 0, 0x00000014 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 17, 0x00000011 },
{ 0x28, 0, 0, 0x00000036 },
{ 0x15, 14, 0, 0x00000016 },
{ 0x28, 0, 0, 0x00000038 },
{ 0x15, 12, 13, 0x00000016 },
{ 0x15, 0, 12, 0x00000800 },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 8, 0x00000011 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00000016 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00000016 },
{ 0x6, 0, 0, 0x0000ffff },
{ 0x6, 0, 0, 0x00000000 }
};
int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
struct sock_fprog bpf;
memset(&bpf,0x00,sizeof(bpf));
bpf.len = sizeof(bpf_code) / sizeof(struct sock_filter);
bpf.filter = bpf_code;
int ret = setsockopt( fd,SOL_SOCKET, SO_ATTACH_FILTER, &bpf,sizeof(bpf));
if (ret < 0)
{
printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
}
return fd;
}
通过上面的代码你根据返回的 fd ,调用recvfrom 接收到的包就是经过过滤的22端口的数据包。但是你要是想抓的包是去除数据链路层的头,用socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL)); 然后根据tcpdump -dd生成的规则,你会发现加入的规则起不到作用。这里推测 tcpdump -dd 生成的规则只针对链路层。
同时在socket除了你可以添加你要过滤的规则,还有几个两个与bpf规则相关的系统调用
· 在套接字socket 附加filter规则 :
setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &val, sizeof(val));
· 把filter从socket上移除 :
setsockopt(sockfd, SOL_SOCKET, SO_DETACH_FILTER, &val, sizeof(val));
· 运行中锁定附加到socket上的filter:
setsockopt(sockfd, SOL_SOCKET, SO_LOCK_FILTER, &val, sizeof(val));
其中SO_DETACH_FILTER选项可以把filter从socket上移除。这可能不会被经常使用,因为当你关闭socket的时候如果有filter会被自动移除。另外一个不太常见的情况是在同一个socket上添加不同的filter,当你还有另一个filter正在运行:如果你的新filter代码能够通过内核检查,内核小心的把旧的filter移除把新的filter换上,如果检查失败旧的filter将继续保留在socket上。
SO_LOCK_FILTER选项运行锁定附加到socket上的filter。一旦设置,filter不能被移除或者改变。这种允许一个进程设置一个socket、附加一个filter、锁定它们并放弃特权,确保这个filter保持到socket的关闭。
通过上面了解,貌似自己实现抓包过滤并不存在啥技术难度。相反是不是感觉很简单。其实并不见得,本章要实现抓包过滤的应用功能,本质上是类似实现nat转换的功能。大概就是经过本机指定的srcip,srcPort过滤数据包,然后修改数据包,给该数据转发到另一台设备上destip,destPort。
1. 数据链路层抓包
notes : 只给出关键代码
int create_link_raw_socket(){
struct sock_filter bpf_code[] = {
// tcpdump src 10.68.22.140 and port 7777 -nn -dd
{ 0x28, 0, 0, 0x0000000c },
{ 0x15, 0, 14, 0x00000800 },
{ 0x20, 0, 0, 0x0000001a },
{ 0x15, 0, 12, 0x0a44168c },
{ 0x30, 0, 0, 0x00000017 },
{ 0x15, 2, 0, 0x00000084 },
{ 0x15, 1, 0, 0x00000006 },
{ 0x15, 0, 8, 0x00000011 },
{ 0x28, 0, 0, 0x00000014 },
{ 0x45, 6, 0, 0x00001fff },
{ 0xb1, 0, 0, 0x0000000e },
{ 0x48, 0, 0, 0x0000000e },
{ 0x15, 2, 0, 0x00001e61 },
{ 0x48, 0, 0, 0x00000010 },
{ 0x15, 0, 1, 0x00001e61 },
{ 0x6, 0, 0, 0x0000ffff },
{ 0x6, 0, 0, 0x00000000 }
};
int fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
struct sock_fprog bpf;
memset(&bpf,0x00,sizeof(bpf));
bpf.len = sizeof(bpf_code) / sizeof(struct sock_filter);
bpf.filter = bpf_code;
int ret = setsockopt( fd,SOL_SOCKET, SO_ATTACH_FILTER, &bpf,sizeof(bpf));
if (ret < 0)
{
printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
}
return fd;
}
首先创建了socket ,从数据链路层开始过滤,只抓src 10.68.22.140 并且 7777 端口的数据包,这部分代码前面做过说明。
while (1) {
memset(buffer, 0, BUFFER_SIZE);
struct sockaddr_in t_addr;
socklen_t addr_len = sizeof(struct sockaddr_in);
recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
if (recv_ret <= 0) {
continue;
}
if (ptm->do_message_parse) {
d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
if (d_len <= 0) {
continue;
}
}
struct sockaddr_ll addr;
memset( &addr, 0, sizeof(addr) );
addr.sll_family = AF_PACKET;
struct ifreq ifstruct;
strcpy(ifstruct.ifr_name, "eth0");
ioctl(ptm->raw_fd, SIOCGIFINDEX, &ifstruct);
addr.sll_ifindex = ifstruct.ifr_ifindex;
addr.sll_protocol = htons(ETH_P_ALL);
send_ret= sendto(ptm->raw_fd, buffer, d_len, 0, &addr, sizeof(struct sockaddr_ll));
if (send_ret < 0)
{
printf("error:%s\n",strerror(errno));
}
if (send_ret <= 0) {
continue;
}
}
while循环里三个功能
· 1 循环抓包 recvfrom
· 2 抓包处理分析 do_message_parse
· 3 转发包 sendto
其中抓包处理是核心,下面会具体说明,这里说下 struct sockaddr_ll 结构体
/*设备无关的物理层地址结构,数据链路层的头信息通常定义在 sockaddr_ll 的结构体中,通过setsockopt可以设置网卡的多播或混杂模式*/
struct sockaddr_ll
{
unsigned short int sll_family; /* 一般为AF_PACKET */
unsigned short int sll_protocol; /* 上层协议 */
int sll_ifindex; /* 接口类型 */
unsigned short int sll_hatype; /* 报头类型 */
unsigned char sll_pkttype; /* 包类型 */
unsigned char sll_halen; /* 地址长度 */
unsigned char sll_addr[8]; /* MAC地址 */
};
/ * ---------------------------------------------------
sll_ifindex: interface索引,0 匹配所有的网络接口卡
sll_pkttype: 包含了packet类型。
PACK_HOST 包地址为本地主机地址。
PACK_BROADCAST 物理层广播包。
PACK_MULTICAST 发送到物理层多播地址的包。
PACK_OTHERHOST 发往其它在混杂模式下被设备捕获的主机的包。
PACK_OUTGOING 本地回环包(从本机发出的,不小心loopback到当前socket了)
当发送数据包时,指定 sll_family, sll_addr, sll_halen, sll_ifindex, sll_protocol 就足够了。其它字段设置为0; sll_hatype和 sll_pkttype是在接收数据包时使用的; 如果要bind, 只需要使用 sll_protocol和 sll_ifindex;
------------------------------------------------------------*/
若是你想iPv4的struct sockaddr_in 套接字地址,发送数据。sendto 会报错,显示无效的地址。正如你抓的包是从链路层开始抓的,想要发送出去,套接字地址也得从链路层开始设置。
最后看关键处理部分 do_message_parse
int do_message_parse( char *packet,int lens,void * param){
raw_chl * ptm = (raw_chl *)param;
if(ptm == NULL){
return lens;
}
// 链路层
struct ethernet *ethernet = (struct ethernet*) (packet);
char tempbuf[6] ={0};
memcpy(tempbuf,ethernet->ether_dhost,6);
memcpy(ethernet->ether_dhost,ethernet->ether_shost,6);
memcpy(ethernet->ether_shost,tempbuf,6);
const struct ndpi_llc_header_snap *llc;
u_short ethernet_type = ntohs(ethernet ->ether_type);
int offset = sizeof(struct ethernet);
int pyld_eth_len = 0;
if(ethernet_type <= 1500){
pyld_eth_len = ethernet_type;
printf("================ethernet_type:%d\n",ethernet_type);
}
if(pyld_eth_len != 0) {
llc = (struct ndpi_llc_header_snap *)(&packet[offset]);
/* check for LLC layer with SNAP extension */
if(llc->dsap == SNAP || llc->ssap == SNAP) {
ethernet_type = llc->snap.proto_ID;
offset += + 8;
}
/* No SNAP extension - Spanning Tree pkt must be discarted */
else if(llc->dsap == BSTP || llc->ssap == BSTP) {
// printf("\n\nWARNING: only IPv4/IPv6 packets are supported in this demo (vg_security supports both IPv4 and IPv6), all other packets will be discarded\n\n");
return lens;
}
}
while (ethernet_type == ETH_P_8021Q) {
ethernet_type = (packet[offset + 2] << 8) + packet[offset + 3];
offset += 4;
}
// 网络层 ip
struct ip * ip_header = (struct ip*) (packet + offset);
u_int ip_len = ntohs(ip_header->ip_len);
u_int ip_size = IP_HL(ip_header) * 4;
u_int msg_size = 0;
printf("msg_c2s_parse befor ===========enter sniffer-flow:src_ip%s\n",(char*)inet_ntoa(ip_header->ip_src));
printf("msg_c2s_parse befor ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
ip_header->ip_src.s_addr = ptm->local_addr.n_ip;
ip_header->ip_dst.s_addr = ptm->server_addr.n_ip;
ip_header->ip_sum = 0;
printf("msg_c2s_parse after ===========enter sniffer-flow:src_ip%s\n",vg_sock_get_aip( &ptm->local_addr));
printf("msg_c2s_parse after ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
ip_header->ip_sum = in_chksum((u_short *)ip_header,ip_size);
tsd_hdr_t psdHeader;
memset(&psdHeader,0,sizeof(tsd_hdr_t));
// 传输层 udp 和 tcp
switch (ethernet_type) {
case ETH_P_IP:
switch (ip_header->ip_p) {
case IPPROTO_TCP: {
offset += ip_size;
struct tcp* tcp = (struct tcp*) (packet + offset);
u_int tcp_size = TH_OFF(tcp) * 4;
msg_size = ip_len - ip_size - tcp_size;
g_n_port_client = tcp->th_sport;
printf("===src_port:%d\n",ntohs(tcp->th_sport));
printf("===dst_port:%d\n",ntohs(tcp->th_dport));
// tcp->th_sport = ptm->out_addr.n_port;
tcp->th_dport = ptm->server_addr.n_port;
tcp->th_sum = 0;
psdHeader.saddr=ip_header->ip_src.s_addr;
psdHeader.daddr=ip_header->ip_dst.s_addr;
psdHeader.mbz=0;
psdHeader.ptcl=ip_header->ip_p;
psdHeader.tcpl=htons(msg_size + tcp_size);
char szSendBuf[65535] = {0};
memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
memcpy(szSendBuf+sizeof(psdHeader), packet+offset,tcp_size);
memcpy(szSendBuf+sizeof(psdHeader)+tcp_size,packet+offset+tcp_size ,msg_size);
tcp->th_sum=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+tcp_size +msg_size);
// memcpy(packet+offset,tcp,sizeof(struct tcp));
}
break;
case IPPROTO_UDP:
offset += ip_size;
struct udphdr * udp = (struct udphdr*) (packet + offset);
g_n_port_client = udp->source;
printf("===src_port:%d\n",ntohs(udp->source));
printf("===dst_port:%d\n",ntohs(udp->dest));
udp->source = ptm->local_addr.n_port;
udp->dest = ptm->server_addr.n_port;
udp->check = 0;
msg_size = ip_len - SIZE_IPNET - SIZE_UDP_HEAD;
psdHeader.saddr=ip_header->ip_src.s_addr;
psdHeader.daddr=ip_header->ip_dst.s_addr;
psdHeader.mbz=0;
psdHeader.ptcl=ip_header->ip_p;
psdHeader.tcpl=htons(8+msg_size);
char szSendBuf[65535] = {0};
memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
memcpy(szSendBuf+sizeof(psdHeader), udp, 8);
memcpy(szSendBuf+sizeof(psdHeader)+8, packet + offset +8, msg_size);
udp->check=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+8+msg_size);
// memcpy(packet+offset,udp,sizeof(struct udphdr));
offset =offset +SIZE_UDP_HEAD;
break;
default:
break;
}
break;
default:
break;
}
return lens;
}
对tcp/ip 协议没有了解过的,估计上面代码会看的有点吃力,建议还是先熟悉下tcp/ip协议,tcp/ip 不是本章的讨论点。而上面代码其实做了三件事
· 获取到数据链路层数据,修改src,dst的MAC地址,(sendto 从数据链路层的数据包时,需要正确的MAC地址)
· 获取IP 层数据,修改src,dst的IP地址,同时重新计算网络层校验和
· 获取 udp/tcp 层数据 ,修改src,dst的port ,同时计算传输层校验和
你发现要是从链路层抓包的话,转发的时候还得需要对方的MAC地址,才能转发,而这在大多数情况是无法获取到的,因此上面这种方法是有局限性的。个人觉得也就适合对方发你,然后你组包回对方。
难道抓的包包含链路层的数据,转发发送数据的时候,就必须填充对方的MAC地址吗?当然不是,你可以通过创建两个fd,一个捕获包,另一个发送包。于是上面代码变成。
//创建了发送数据包的套接字
int create_net_raw_socket(){
int fd = socket(AF_INET,SOCK_RAW,IPPROTO_UDP|IPPROTO_TCP);
int flag = 1;
int ret = setsockopt(fd,IPPROTO_IP, IP_HDRINCL,&flag,sizeof(int));
if (ret < 0)
{
printf("setsockopt :%d=====%d======%s\n",ret,errno,strerror(errno));
}
return fd;
}
当需要编写自己的IP数据包首部时,可以在原始套接字上设置套接字选项IP_HDRINCL。我们需要修改src,dst的ip地址。因此需要设置IP_HDRINCL
如果IP_HDRINCL选项未开启,则由内核自动构造IP首部并把它置于来自进程的数据之前,进程让内核发送的数据起始地址指的是IP首部之后的第一个字节。(就是指进程不需要管IP首部)
如果IP_HDRINCL选项开启,则进程让内核发送数据的起始地址指的是IP首部的第一个字节,进程调用输出函数写出的数据量必须包括IP首部的大小,整个IP首部由进程构造,不过IPv4校验可置为0,表示进程让内核来设置该值,IPv4首部校验和字段总是由内核计算并存储。简而言之你不需要计算IP首部校验和,只需要置为0,内核会自动计算。
while (1) {
memset(buffer, 0, BUFFER_SIZE);
struct sockaddr_in t_addr;
socklen_t addr_len = sizeof(struct sockaddr_in);
recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
if (recv_ret <= 0) {
continue;
}
printf("proxy_c2s recv_lens:%d\n",recv_ret);
if (ptm->do_message_parse) {
d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
if (d_len <= 0) {
continue;
}
}
// struct sockaddr_ll addr;
// memset( &addr, 0, sizeof(addr) );
// addr.sll_family = AF_PACKET;
// struct ifreq ifstruct;
// strcpy(ifstruct.ifr_name, "eth0");
// ioctl(ptm->raw_fd, SIOCGIFINDEX, &ifstruct); //??I/O??
// addr.sll_ifindex = ifstruct.ifr_ifindex;
// addr.sll_protocol = htons(ETH_P_ALL);
struct sockaddr_in in;
memset(&in,0,sizeof(struct sockaddr_in));
in.sin_family = AF_INET;
in.sin_addr.s_addr = ptm->server_addr.n_ip;
in.sin_port = ptm->server_addr.n_port;
memset(in.sin_zero, 0, sizeof(in.sin_zero));
send_ret= sendto(ptm->send_fd, buffer, d_len, 0,(struct sockaddr *) &in, sizeof(struct sockaddr_in));
if (send_ret <= 0) {
continue;
}
}
while 循环 其实跟之前结构一样,只是套接字地址用了IPV4的sockaddr_in,同时发送的套接字是由create_net_raw_socket()创建的。而最后虽然捕获的数据包是包含链路层数据的,但是最后sendto的时候数据是将链路层的数据截除后发送的。看do_message_parse 数据处理函数。
int do_message_parse( char *packet,int lens,void * param){
raw_chl * ptm = (raw_chl *)param;
if(ptm == NULL){
return lens;
}
struct ethernet *ethernet = (struct ethernet*) (packet);
const struct ndpi_llc_header_snap *llc;
u_short ethernet_type = ntohs(ethernet ->ether_type);
int offset = sizeof(struct ethernet);
int pyld_eth_len = 0;
if(ethernet_type <= 1500){
pyld_eth_len = ethernet_type;
printf("================ethernet_type:%d\n",ethernet_type);
}
if(pyld_eth_len != 0) {
llc = (struct ndpi_llc_header_snap *)(&packet[offset]);
/* check for LLC layer with SNAP extension */
if(llc->dsap == SNAP || llc->ssap == SNAP) {
ethernet_type = llc->snap.proto_ID;
offset += + 8;
}
/* No SNAP extension - Spanning Tree pkt must be discarted */
else if(llc->dsap == BSTP || llc->ssap == BSTP) {
// printf("\n\nWARNING: only IPv4/IPv6 packets are supported in this demo (vg_security supports both IPv4 and IPv6), all other packets will be discarded\n\n");
return lens;
}
}
while (ethernet_type == ETH_P_8021Q) {
ethernet_type = (packet[offset + 2] << 8) + packet[offset + 3];
offset += 4;
}
int recvlens = lens -offset;
int offflag = offset;
struct ip * ip_header = (struct ip*) (packet + offset);
u_int ip_len = ntohs(ip_header->ip_len);
u_int ip_size = IP_HL(ip_header) * 4;
u_int msg_size = 0;
printf("msg_s2c_parse befor ===========enter sniffer-flow:src_ip%s\n",(char*)inet_ntoa(ip_header->ip_src));
printf("msg_s2c_parse befor ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
ip_header->ip_src.s_addr = ip_header->ip_dst.s_addr;
ip_header->ip_dst.s_addr = ptm->server_addr.n_ip;
ip_header->ip_sum = 0;
// printf("msg_s2c_parse after ===========enter sniffer-flow:src_ip%s\n",vg_sock_get_aip( &ptm->in_addr));
printf("msg_s2c_parse after ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
// ip_header->ip_sum = in_chksum((u_short *)ip_header,ip_size);
tsd_hdr_t psdHeader;
memset(&psdHeader,0,sizeof(tsd_hdr_t));
switch (ethernet_type) {
case ETH_P_IP:
switch (ip_header->ip_p) {
case IPPROTO_TCP: {
offset += ip_size;
struct tcp* tcp = (struct tcp*) (packet + offset);
u_int tcp_size = TH_OFF(tcp) * 4;
msg_size = ip_len - ip_size - tcp_size;
printf("ip_len:%d ====ip_size:%d==== tcp_size:%d\n",ip_len,ip_size,tcp_size);
// tcp->th_sport = ptm->in_addr.n_port;
tcp->th_dport = ptm->server_addr.n_port;
printf("msg_s2c_parse===src_port:%d\n",ntohs(tcp->th_sport));
printf("msg_s2c_parse===dst_port:%d\n",ntohs(tcp->th_dport));
tcp->th_sum = 0;
psdHeader.saddr=ip_header->ip_src.s_addr;
psdHeader.daddr=ip_header->ip_dst.s_addr;
psdHeader.mbz=0;
psdHeader.ptcl=ip_header->ip_p;
psdHeader.tcpl=htons(msg_size + tcp_size);
char szSendBuf[65535] = {0};
memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
printf("tcp_size:%d\n",tcp_size);
memcpy(szSendBuf+sizeof(psdHeader), packet+offset,tcp_size);
memcpy(szSendBuf+sizeof(psdHeader)+tcp_size,packet+offset+tcp_size ,msg_size);
tcp->th_sum=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+tcp_size +msg_size);
// memcpy(packet+offset,tcp,sizeof(struct tcp));
}
break;
case IPPROTO_UDP:
offset += ip_size;
struct udphdr * udp = (struct udphdr*) (packet + offset);
udp->source = udp->dest;
udp->dest = ptm->server_addr.n_port;
udp->check = 0;
msg_size = ip_len - SIZE_IPNET - SIZE_UDP_HEAD;
psdHeader.saddr=ip_header->ip_src.s_addr;
psdHeader.daddr=ip_header->ip_dst.s_addr;
psdHeader.mbz=0;
psdHeader.ptcl=ip_header->ip_p;
psdHeader.tcpl=htons(8+msg_size);
char szSendBuf[65535] = {0};
memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
memcpy(szSendBuf+sizeof(psdHeader), udp, 8);
memcpy(szSendBuf+sizeof(psdHeader)+8, packet + offset +8, msg_size);
udp->check=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+8+msg_size);
// memcpy(packet+offset,udp,sizeof(struct udphdr));
offset =offset +SIZE_UDP_HEAD;
break;
default:
break;
}
break;
default:
break;
}
// 去除链路层的数据
memcpy(packet,packet+offflag,recvlens);
return recvlens;
}
处理函数相比较之前链路层的处理函数:
· 不需要修改链路层的Mac地址
· ip层首部校验和置0即可,内核内部会计算
· 最后返回的数据不包含链路层的数据。
2. 网络层抓包
既然可以链路层抓包,当然也可以抛开数据链路层,直接网络层抓包。而方法前面已经提到 socket(PF_PACKET, SOCK_DGRAM,htons(ETH_P_ALL)); 若是你想起来,自然也应该记得这有个很致命的问题,在不深入了解bpf规则的前提下,用tcpdump -dd生产的规则只适应链路层抓包。 那这问题如何解决呢?你想通过网络层抓包,但又不知道bpf规则如何设置?这问题困扰好久,最终不得不借助libpcap生产规则的接口。
int create_raw_socket(){
static const char filter[] = "port 7777 and host 10.68.22.140";
int sock = socket(PF_PACKET, SOCK_DGRAM,htons(ETH_P_ALL));
pcap_t *pcap = pcap_open_dead(DLT_RAW, 65535);
struct bpf_program bpf_prog;
pcap_compile(pcap, &bpf_prog, filter, 0, PCAP_NETMASK_UNKNOWN);
printf("==========bpf_prog.bf_len:%d\n",bpf_prog.bf_len);
struct sock_fprog linux_bpf = {
.len = bpf_prog.bf_len,
.filter = (struct sock_filter *) bpf_prog.bf_insns,
};
int ret = setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER, &linux_bpf, sizeof(linux_bpf));
if (ret < 0)
{
printf("setsockopt:SO_ATTACH_FILTER>>>>error:%s\n",strerror(errno));
}
return sock;
}
pcap_open_dead()官方文档提到
It is typically used when just using libpcap for compiling BPF code;
· 1
其实就是创建了一个pcap_t 结构体,用于创建bpf代码。该接口用处不大。
pcap_compile() 接口 创建bpf代码的
while (1) {
memset(buffer, 0, BUFFER_SIZE);
struct sockaddr_in t_addr;
socklen_t addr_len = sizeof(struct sockaddr_in);
recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
if (recv_ret <= 0) {
continue;
}
printf("proxy_c2s recv_lens:%d\n",recv_ret);
if (ptm->do_message_parse) {
d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
if (d_len <= 0) {
continue;
}
}
struct sockaddr_in in;
memset(&in,0,sizeof(struct sockaddr_in));
in.sin_family = AF_INET;
in.sin_addr.s_addr = ptm->server_addr.n_ip;
in.sin_port = ptm->server_addr.n_port;
memset(in.sin_zero, 0, sizeof(in.sin_zero));
send_ret= sendto(ptm->send_fd, buffer, d_len, 0,(struct sockaddr *) &in, sizeof(struct sockaddr_in));
if (send_ret <= 0) {
continue;
}
}
再看while循环体,本质还是接收包,处理包,发送包3个流程。不过因为抓的是网络层数据包,获取到的包无需再处理数据链路。
再看do_message_parse 函数 你会发现对链路层的处理部分全都不需要了。
int do_message_parse( char *packet,int lens,void * param){
raw_chl * ptm = (raw_chl *)param;
if(ptm == NULL){
return lens;
}
int offset = 0;
struct ip * ip_header = (struct ip*) (packet + offset);
u_int ip_len = ntohs(ip_header->ip_len);
u_int ip_size = IP_HL(ip_header) * 4;
u_int msg_size = 0;
printf("msg_c2s_parse befor ===========enter sniffer-flow:src_ip%s\n",(char*)inet_ntoa(ip_header->ip_src));
printf("msg_c2s_parse befor ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
ip_header->ip_src.s_addr = ptm->local_addr.n_ip;
ip_header->ip_dst.s_addr = ptm->server_addr.n_ip;
ip_header->ip_sum = 0;
printf("msg_c2s_parse after ===========enter sniffer-flow:src_ip%s\n",vg_sock_get_aip( &ptm->local_addr));
printf("msg_c2s_parse after ===========enter sniffer-flow:des_ip:%s\n",(char*)inet_ntoa(ip_header->ip_dst));
ip_header->ip_sum = in_chksum((u_short *)ip_header,ip_size);
tsd_hdr_t psdHeader;
memset(&psdHeader,0,sizeof(tsd_hdr_t));
switch (ip_header->ip_p) {
case IPPROTO_TCP: {
offset += ip_size;
struct tcp* tcp = (struct tcp*) (packet + offset);
u_int tcp_size = TH_OFF(tcp) * 4;
msg_size = ip_len - ip_size - tcp_size;
g_n_port_client = tcp->th_sport;
printf("===src_port:%d\n",ntohs(tcp->th_sport));
printf("===dst_port:%d\n",ntohs(tcp->th_dport));
// tcp->th_sport = ptm->out_addr.n_port;
tcp->th_dport = ptm->server_addr.n_port;
tcp->th_sum = 0;
psdHeader.saddr=ip_header->ip_src.s_addr;
psdHeader.daddr=ip_header->ip_dst.s_addr;
psdHeader.mbz=0;
psdHeader.ptcl=ip_header->ip_p;
psdHeader.tcpl=htons(msg_size + tcp_size);
char szSendBuf[65535] = {0};
memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
memcpy(szSendBuf+sizeof(psdHeader), packet+offset,tcp_size);
memcpy(szSendBuf+sizeof(psdHeader)+tcp_size,packet+offset+tcp_size ,msg_size);
tcp->th_sum=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+tcp_size +msg_size);
// memcpy(packet+offset,tcp,sizeof(struct tcp));
}
break;
case IPPROTO_UDP:
offset += ip_size;
struct udphdr * udp = (struct udphdr*) (packet + offset);
g_n_port_client = udp->source;
printf("===src_port:%d\n",ntohs(udp->source));
printf("===dst_port:%d\n",ntohs(udp->dest));
udp->source = ptm->local_addr.n_port;
udp->dest = ptm->server_addr.n_port;
udp->check = 0;
msg_size = ip_len - SIZE_IPNET - SIZE_UDP_HEAD;
psdHeader.saddr=ip_header->ip_src.s_addr;
psdHeader.daddr=ip_header->ip_dst.s_addr;
psdHeader.mbz=0;
psdHeader.ptcl=ip_header->ip_p;
psdHeader.tcpl=htons(8+msg_size);
char szSendBuf[65535] = {0};
memcpy(szSendBuf, &psdHeader, sizeof(psdHeader));
memcpy(szSendBuf+sizeof(psdHeader), udp, 8);
memcpy(szSendBuf+sizeof(psdHeader)+8, packet + offset +8, msg_size);
udp->check=in_chksum((u_short *)szSendBuf,sizeof(psdHeader)+8+msg_size);
offset =offset +SIZE_UDP_HEAD;
break;
default:
break;
}
return lens;
}
3. 运行结果
测试中本机地址22.189 转发地址是22.140
程序运行在22.189 设备上
22.140:7777 端口 发送给 22.189:8888端口,如下图
程序处理后,会将发往 22.189:8888 的数据包转发给 22.140:9000 端口,如下图
为啥要把lo口的抓发包单独提出来呢? 带着这个问题往下看
lo口的有条重要性质:
· 1.ip addr add 127.2.0.1/16 dev lo 添加了127.2.0.1/16的地址 代表127.2.0.1/16网段所有地址都设置好了,即ping 127.2.128.222 也是可以通的 不需要单独再配置 127.2.128.222 地址
这对某些项目部署工程的时候,要是可以用lo口地址配置一个网段地址,所有该网段的地址都起来了。因此项目中可能会用到lo口地址。例如本人之前有个通过lo口代理转发项目。
udp协议lo还发现一个特性,例如我eth0 地址配置了10.68.22.189 ,然后 lo口配置了127.2.0.1/16 网段的地址。当我用10.68.22.189 地址往 127.3.0.11 这个地址发送数据的时候。虽然127.3.0.11 地址是不存在的,但是数据包是能发送出去的。
程序demo执行如下图所示
tcpdump 抓包显示如下
但是要是我以 127.2.0.11 发往 127.3.0.11。 你会发现数据包压根发送不出去。
程序直接报错误,显示无效参数,如下图
后面发现udp要是想lo发送数据包,首先你发送本机设置的127地址必须是存在的,同时发送的服务端地址不管是发往的是127的还是其他地址。必须是本机存在的。lo口地址发送只能发往本机。
这条特性还是要多注意下,之前发现莫名其妙lo口发送的数据,怎么抓包都抓不到,后面才发现这个问题。
lo口相关知识点已经铺垫好了,言归正传,之前不是介绍链路层的两种抓包,还有网络层的一种抓包。后面发现用lo口发送数据,数据发送出来一条,但是抓包确认抓到两条。
test程序 127.2.0.11 往 127.2.0.44 发送了一次数据包,如下图
抓包程序发现接收到了两次相同的包。 而导致lo口发送一次包,转发了两条数据
tcpdump 抓包如下
从tcpdump 你发现他只收到了一次lo口的包,后面两条是我程序转发的两次数据包。那么为啥tcpdump只收到了一次数据包,tcpdump 做了啥处理了吗?
回顾libpcap pcap_read_packet() 接口 里面接收到套接字地址后,会调用
linux_check_direction()函数,里面有一段关键代码如下
struct pcap_linux *handlep = handle->priv;
if (sll->sll_pkttype == PACKET_OUTGOING) {
/*
* Outgoing packet.
* If this is from the loopback device, reject it;
* we'll see the packet as an incoming packet as well,
* and we don't want to see it twice.
*/
if (sll->sll_ifindex == handlep->lo_ifindex)
return 0;
}
看注释的这段话,lo口的设备抓到的包,在incoming ,Outgoing 都会收到一次,我们不需要抓包两次,因此outgoing的包忽略掉
到这里我们已经看到解决方法,下面给出代码关键部分
while (1) {
memset(buffer, 0, BUFFER_SIZE);
struct sockaddr_ll t_addr;
socklen_t addr_len = sizeof(struct sockaddr_ll);
recv_ret = recvfrom(ptm->raw_fd, buffer, BUFFER_SIZE,0, (struct sockaddr *) &t_addr, &addr_len);
if (recv_ret <= 0) {
printf("recvfrom error:%s\n",strerror(errno));
continue;
}
if (t_addr.sll_pkttype == PACKET_OUTGOING) {
continue;
}
if (ptm->do_message_parse) {
d_len = ptm->do_message_parse(buffer, recv_ret,ptm);
if (d_len <= 0) {
continue;
}
}
printf("do_message_parse:d_len:%d\n",d_len);
struct sockaddr_in in;
memset(&in,0,sizeof(struct sockaddr_in));
in.sin_family = AF_INET;
in.sin_addr.s_addr = ptm->server_addr.n_ip;
in.sin_port = ptm->server_addr.n_port;
memset(in.sin_zero, 0, sizeof(in.sin_zero));
send_ret= sendto(ptm->send_fd, buffer, d_len, 0,(struct sockaddr *) &in, sizeof(struct sockaddr_in));
if (send_ret <= 0) {
continue;
}
}
测试结果,tcpdump 抓包如下
上图可以看到127.2.0.11 往 127.2.0.44 发送了一条udp数据
然后后面 10.68.22.189:9999 将这条数据转发给了 10.68.22.140。转发的部分就程序做的,只转发了一条,说明抓的数据已经做了过滤。
注意点:
· 链路层抓包,提供了两种处理方式,一种是创建raw_fd抓包,同时还是用raw_fd发包,另一种是raw_fd抓包,send_fd 发包。
· 网络层抓包,需要注意bpf规则的生成。同时只提供了raw_fd抓包,send_fd发包一种情况,试过用raw_fd 抓包发包,会报无效的参数错误,未找到问题。
· lo口发送udp数据包,要注意发送的lo地址必须存在,同时发往的地址也必须本机真实存在。lo口发送包,正常情况会抓到两次数据包,要做过滤。
问题:
· 本篇示例只处理请求转发,并没有处理响应转发。了解了请求转发过程,响应其实是一个道理
· 提供的示例是走的udp协议,相对简单。tcp协议面向连接,必须要三次握手成功,才能互相发送数据。也就是必须处理转发和响应
tcp 协议篇幅有限,这里没有讨论tcp,若是你实践了tcp转发。这里啰嗦几句, 即使你转发响应都处理了,你会发现当客户端tcp握手一个没有开放监听的端口,syn 包请求包发送后,数据包到了本机应用层,它会发现tcp请求的端口,本机没有监听,开放。会直接返回Rst包重置掉这个请求。导致转发的三次握手一直连接不上。你只需要在iptables filter链 drop 掉 这个syn请求包即可。纳尼? 说的不简洁,是不是云里雾里。总而言之,你得明白你作为转发的本机其实就是捕获数据,并不会去在本机监听开放端口,而当客户端发起tcp连接,连接的端口并不存在,所以本机会发一个rst重置包,关闭这个连接。
断断续续花了两周多的时间整理,把遇到的坑跟大伙分析。希望对大伙有用。至此本篇完。
目前有 1 条留言 访客:0 条, 博主:0 条 ,引用: 1 条
外部的引用: 1 条
- linux下packet_mmap 前篇 (抓包实现) | 求索阁