现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

linux 下 tcpdump详解 后篇(自己实现抓包过滤)

2019-09-23 18:28 工业·编程 ⁄ 共 23021字 ⁄ 字号 评论 1 条

一 概述

在了解了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 就可生产对应规则,如下图

wps6

至此你其实已经完全可以根据要过滤的包,自己实现抓包过滤了。程序如下

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端口,如下图

wps7

程序处理后,会将发往 22.189:8888 的数据包转发给 22.140:9000 端口,如下图

wps8

四. lo口抓包发包

为啥要把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执行如下图所示

wps9

tcpdump 抓包显示如下

wps10

但是要是我以 127.2.0.11 发往 127.3.0.11。 你会发现数据包压根发送不出去。

程序直接报错误,显示无效参数,如下图

wps11

后面发现udp要是想lo发送数据包,首先你发送本机设置的127地址必须是存在的,同时发送的服务端地址不管是发往的是127的还是其他地址。必须是本机存在的。lo口地址发送只能发往本机。

这条特性还是要多注意下,之前发现莫名其妙lo口发送的数据,怎么抓包都抓不到,后面才发现这个问题。

lo口相关知识点已经铺垫好了,言归正传,之前不是介绍链路层的两种抓包,还有网络层的一种抓包。后面发现用lo口发送数据,数据发送出来一条,但是抓包确认抓到两条。

test程序 127.2.0.11 往 127.2.0.44 发送了一次数据包,如下图

wps12

抓包程序发现接收到了两次相同的包。 而导致lo口发送一次包,转发了两条数据

wps13

tcpdump 抓包如下

wps14

从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 抓包如下

wps15

上图可以看到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 前篇 (抓包实现) | 求索阁

    给我留言

    留言无头像?