FTP深度包检测——ndpi源码分析

一、简介

       在本文中,我们将看到ndpi提供的的分析函数ndpi_detection_process_packet的主要实现。以及ndpi内部是如何对FTP协议实现深度包检测技术。对于前期的初始化和API的使用,请参考附录中的文章。欢迎大家一起讨论学习。然后我们这次会带着下面几个问题阅读源代码:

         1、ndpi的分析函数进行了怎么样的工作?

         2、ndpi内部是如何实现深度包检测技术的?

         3、ndpi里面,对于多种协议的检测是基于怎样的准则进行选择?

注意:因为限于篇幅需要下面进行分析默认针对ipv4,如果有不正确的地方欢迎大家一起来讨论学习

二、分析函数API的实现  

       ndpi内部提供了ndpi_detection_process_packet(在ndpi_main.c中进行了定义)函数作为协议检测的API,函数原型及解释如下:      

       
unsigned int ndpi_detection_process_packet(
    struct ndpi_detection_module_struct *ndpi_struct,
    //ndpi_detection_module_struct在附录2中进行了详细的叙述,在ndpi_init_detection_module函数进行初始化
    struct ndpi_flow_struct *flow,
    //用来维护当前会话的检测协议栈,以及检测的参数等
    const unsigned char *packet,
    //ip层数据报文
    const unsigned short packetlen,
    //报文长度
    const u_int32_t current_tick,
    //包的tick值
    struct ndpi_id_struct *src,struct ndpi_id_struct *dst)
    //ndpi_id_struct里面包含各个协议的源目的端信息

       在ndpi_detection_process_packet函数中,我们根据参数中的ndpi_struct、packet、current_tick、src、dst对flow完成一系列的初始化工作。在其内部维护一个针对会话的协议栈,然后根据多次的检测选择最优的匹配的应用层协议。具体动作细节如下:
       1、检测包长度
        这里它通过检测包长度(packetlen),对包的可用性进行了测试。如果包长度没有20字节(ip数据报文至少20字节),则利用ndpi_int_reset_packet_protocol把flow内部的协议栈顶类型置为UNKNOW。并且清0协议栈信息字段,最后返回UNKNOW类型。这里关于flow协议栈的结构和使用我们将在第三章的FTP协议检测中详细进行介绍。
        2、flow->packet的初始化
        这里通过捕获的ip报文(packet参数)和用户设置的心跳值(current_tick参数),对flow->packet.iph和flow->packet.tick_timestamp进行初始化。
        3、传输层检测及flow的初始化
        这里主要通过函数ndpi_init_packet_header(在ndpi_main.c中进行了定义)进行了实现,他完成比较多的工作。
        1)首先一个就是根据ndpi_packet_struct中的协议栈内容和描述信息,对flow的协议栈内容和描述信息进行了初始化。这部分通过内部的ndpi_apply_flow_protocol_to_packet函数进行了实现。
        2)根据ipv4和ipv6对flow中的packet分别进行初始化(flow->packet.iph和flow->packet.iphv6)
        3)通过ndpi_detection_get_l4_internal对报文的ipv4(ipv6)header进行检测,并且获取传输层协议信息。通过l4protocol变量进行传递,记录传输层协议号。
        4)根据l4protocol字段进行传输层的判别,如果是tcp(协议号是6)则对包内部的syn和ack等字段进行初步的检测。如果是udp(协议号是17),则计算出包的长度。初始化flow->packet当中的字段
        4、flow传输层信息的初始化
        这里主要通过报文获取传输层信息,比如在tcp协议中我们捕获到的报文是握手中的什么角色,是ack包还是其他的。这些信息将对检测提供一些数据。
        1)首先通过src和dst参数初始化flow->src和flow->dst字段
        2)通过ndpi_connection_tracking函数进行我们上述的工作。这里它判断的tcp握手的状态,并且通过flow->next_tcp_seq_nr数组对tcp序列进行了描述。
        我们这里拿一段代码来一起进行分析。看看具体是怎样操作的。 
       
if (tcph->syn != 0 && tcph->ack == 0 && flow->l4.tcp.seen_syn == 0 && flow->l4.tcp.seen_syn_ack == 0
	&& flow->l4.tcp.seen_ack == 0) {
      flow->l4.tcp.seen_syn = 1;}
if (tcph->syn != 0 && tcph->ack != 0 && flow->l4.tcp.seen_syn == 1 && flow->l4.tcp.seen_syn_ack == 0
	&& flow->l4.tcp.seen_ack == 0) {
      flow->l4.tcp.seen_syn_ack = 1;}
if (tcph->syn == 0 && tcph->ack == 1 && flow->l4.tcp.seen_syn == 1 && flow->l4.tcp.seen_syn_ack == 1
	&& flow->l4.tcp.seen_ack == 0) {
      flow->l4.tcp.seen_ack = 1;}
           上面这段代码中,我们不难看出ndpi中通过flow->l4.tcp中的seen_syn、seen_syn_ack和seen_ack记录tcp的握手状态。然后根据分析报文中的syn和ack字段进行归类,为后期的检测提供数据基础。
         5、建立传输层的描述
         这一部分通过ndpi_selection_packet进行记录,主要是提供给下面第6部分一个传输层上分类的检测。这里的ndpi_selection_packet将记录传输层的信息。包括是tcp还是udp,里面还检测不出来时的灰色选项(tcp or udp)。数据结构这里通过一个整形进行记录,大家还有不明白可以参考附录中的ndpi协议的注册及维护的文章。里面第三章的第一点详细讲述了该方法。
         6、调用对应的检测函数
         这里ndpi根据第5点提供的参考,对捕获的数据包进行了传输层的分类。分成tcp、udp和none tcp&&udp(既不是tcp也不是udp)。然后对于每个类别,调用其对用的注册函数。
         注:关于检测协议函数的注册,我们在协议的注册与维护一文中进行了详细的介绍。具体请参考附录2
         我们这里列一段代码来一起看看他的具体操作是怎么样的:
         
if (flow != NULL && flow->packet.tcp != NULL) {
    //....tcp类别
} else if (flow != NULL && flow->packet.udp != NULL) {
    //....udp类别      
    for (a = 0; a < ndpi_struct->callback_buffer_size_udp; a++) {
    //对每个udp协议,ndpi内部根据先前的检测结果,判断是否应该继续进行检测。ndpi内部很多运用移位和与或运算,有兴趣的朋友可以先去看看这些
    if ((ndpi_struct->callback_buffer_udp[a].ndpi_selection_bitmask & ndpi_selection_packet) ==
	  ndpi_struct->callback_buffer_udp[a].ndpi_selection_bitmask
	  && NDPI_BITMASK_COMPARE(flow->excluded_protocol_bitmask,
				  ndpi_struct->callback_buffer_udp[a].excluded_protocol_bitmask) == 0
	  && NDPI_BITMASK_COMPARE(ndpi_struct->callback_buffer_udp[a].detection_bitmask,
				  detection_bitmask) != 0) {
	//这里调用在初始化阶段注册的检测函数,func字段是一个函数指针。我们接下来的FTP协议也是通过这种方式进行调用。不过FTP是tcp类别的
	ndpi_struct->callback_buffer_udp[a].func(ndpi_struct, flow);

	if(flow->detected_protocol_stack[0] != NDPI_PROTOCOL_UNKNOWN)
	  break; /* Stop after detecting the first protocol */
      }
    }
} else {
   //....非tcp&&udp类别
}

三、FTP深度包检测的实现

          这里我们将介绍一下,ndpi里面FTP的深度深度包检测是怎么实现。通过上述的第二章第6点的func调用,我们在ndpi_detection_process_packet函数内部的tcp传输层类别调用FTP的检测函数ndpi_search_ftp_tcp。具体定义在<ndpi home>/src/lib/protocol/中,里面囊括了ndpi支持的所有协议的源代码。我们马上一起来看看ndpi是怎么样完成FTP的深度包检测的。这一部分我们分为两part进行介绍,具体如下:
       1、源和目的ip及端口检测
        这里首先是两个分别针对源端口和目的端口的if语句检查,这里应该是考虑到FTP请求发起方的问题。检查内容包括:
        1)src非空
        2)ip报文的源ip地址和src中的FTP地址是否匹配
        3)协议栈顶不等于UNKNOW类型
        4)比较src中的检测协议映射是否包含FTP协议
        5)src中ftp_timer的设置非0
        然后完成上面的工作之后,如果会话的目的端口号大于1024,并且源端口号大于1024或等于20。则判定这个会话为FTP会话,通过ndpi_int_ftp_add_connection(这个函数在ftp.c中定义和实现,具体实现的函数在ndpi_main.c中抽象出来和其他检测协议共用)修改协议栈。这里因为调用关系比较多,我们不列代码了。我们将在下面第2部分,针对栈的实现和操作进行介绍。
        2、检测协议栈的操作
        ndpi内部的ndpi_flow_struct中通过数组和自定义的栈描述描述信息,实现了深度检测的栈结构。其实属性Linux内核的人会发现,这个结果和进程内核栈的实现是非常相似的。只是比较简单,没有了想thread_info那样的索引。栈本身是通过detected_protocol_stack数组进行实现,然后在protocol_stack_info字段中定义了两个信息来描述这个栈。一个是entry_is_real_protocol,它描述的是栈中是否检测出了正确的协议。第二个是current_stack_size_minus_one,它描述的是栈的大小。detected_protocol_stack栈中的是从高地址开始的,也就是detected_protocol_stack[0]代表的是栈顶。current_stack_size_minus_one表示的栈底部。具体定义如下:
       
typedef struct ndpi_flow_struct {
  u_int16_t detected_protocol_stack[NDPI_PROTOCOL_HISTORY_SIZE]; //define in ndpi_struct.h中
#if NDPI_PROTOCOL_HISTORY_SIZE > 1
#  if NDPI_PROTOCOL_HISTORY_SIZE > 5
#    error protocol stack size not supported
#  endif
  struct {
    u_int8_t entry_is_real_protocol:5; 
    u_int8_t current_stack_size_minus_one:3;
  }//栈描述结构
#if !defined(WIN32)
    __attribute__ ((__packed__))
#endif
    protocol_stack_info;
#endif  
  .......
} ndpi_flow_struct_t;
       对栈的操作,并没有进行封装。直接在各自的功能函数中进行实现,以上面的添加协议为例,将最终在ndpi_int_change_flow_protocol(ndpi_main.c中实现)进行操作。这里的函数是在FTP的检测函数ndpi_search_ftp_tcp内部通过ndpi_int_ftp_add_connection协议操作函数,最终在ndpi_main.c中得到调用和实现。具体的调用过程这里省略,有兴趣的朋友可以联系我。我们这里列一段ndpi_int_change_flow_protocol的栈操作代码,其实并没有我们相信中那么难。
       
/* now shift and insert */
for (a = stack_size - 1; a > 0; a--) {
    flow->detected_protocol_stack[a] = flow->detected_protocol_stack[a - 1];
}
flow->protocol_stack_info.entry_is_real_protocol <<= 1;
/* now set the new protocol */
flow->detected_protocol_stack[0] = detected_protocol;
       上面这几行代码,实现了栈的滑动和插入过程。没有想象中那么难吧。
        
      3、FTP主动模式和被动模式的检测
      这里我们需要补充一点背景知识:
      FTP主动模式:由服务器端发FTP请求
      FTP被动模式:由客户端发送FTP请求
      在ndpi_search_ftp_tcp中,ndpi通过search_passive_ftp_mode和search_active_ftp_mode调用并进行实现。这里因这两个函数原理差不多,我们针对search_passive_ftp_mode进行介绍。这里主要采用的方法是对捕获的FTP数据报文进行搜索,包搜索的实现是通过memcmp函数进行实现,这个是公共库中的函数。在#include <string.h>或#include<memory.h>中,我们可以获得这个函数。函数原型如下:
      int memcmp(const void *buf1, const void *buf2, unsigned int count);
当buf1<buf2时,返回值<0
当buf1=buf2时,返回值=0
当buf1>buf2时,返回值>0
然后工具有了,接下来就是利用这个函数对捕获的包进行比较。看看有没有被动模式包含的字段,这里我们再列一段代码一起看看:
if (packet->payload_packet_len > 34 && ndpi_mem_cmp(packet->payload, "229 Entering Extended Passive Mode", 34) == 0) {
    if (dst != NULL) {
      ndpi_packet_src_ip_get(packet, &dst->ftp_ip);
      dst->ftp_timer = packet->tick_timestamp;
      dst->ftp_timer_set = 1;
      NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG, "saved ftp_ip, ftp_timer, ftp_timer_set to dst");
      NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG,
	       "FTP Extended PASSIVE MODE FOUND: use Server %s\n", ndpi_get_ip_string(ndpi_struct, &dst->ftp_ip));
    }
    if (src != NULL) {
      ndpi_packet_dst_ip_get(packet, &src->ftp_ip);
      src->ftp_timer = packet->tick_timestamp;
      src->ftp_timer_set = 1;
      NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG, "saved ftp_ip, ftp_timer, ftp_timer_set to src");
      NDPI_LOG(NDPI_PROTOCOL_FTP, ndpi_struct, NDPI_LOG_DEBUG,
	       "FTP Extended PASSIVE MODE FOUND: use Server %s\n", ndpi_get_ip_string(ndpi_struct, &src->ftp_ip));
    }
    return;
}
上述代码中,通过if中的判断如果包长度大于34,并且能匹配到229 Entering Extended Passive Mode。则进入判断分别对src和dst进行非空的测试,最后通过NDPI_LOG记录进去日志中。注:这里的src和dst并不是指源端口,而是上文中描述的ndpi_id_struct结构。

四、附录

     附录一:ndpi网站(源代码用svn详细见网站介绍):
                           http://www.ntop.org/products/ndpi/

     附录二:协议的注册与维护——ndpi源码分析
                          http://blog.csdn.net/grublinux/article/details/37670619

     附录三:pcapreader——源码分析
                          http://blog.csdn.net/grublinux/article/details/31603915

        最后谢谢在这个过程中,williamy给予我的帮助。




根据你提供的代码片段,可以看出你正在使用 `nDPI` 库对 `DPDK` 抓取的数据进行协议识别,并将识别结果写入到新的 pcap 文件中。 在代码中,`$READER` 变量指向了一个文件路径,这个路径代表了 `nDPI` 库提供的用于读取和解析数据的程序。 首先,脚本中的 `build_results` 函数遍历了指定目录中所有以 `.pcap` 扩展名结尾的文件(通过 `PCAPS` 变量保存文件名列表),并使用 `$READER` 执行命令对每个 pcap 文件进行处理。 命令 `$READER -q -i pcap/$f -w result/$f.out` 的含义如下: - `-q` 参数表示以静默模式运行,即不输出额外的信息。 - `-i pcap/$f` 参数指定要读取的 pcap 文件路径。 - `-w result/$f.out` 参数指定要写入的结果文件路径。 这个命令的作用是将输入的 pcap 文件通过 nDPI 库进行解析,并将识别结果写入到 `result/$f.out` 文件中。 接下来,`check_results` 函数对已经生成的结果文件进行检查。它使用相似的逻辑遍历 `PCAPS` 列表中的每个 pcap 文件: - 首先,检查对应的结果文件是否存在。 - 如果结果文件存在,首先执行命令 `$READER -q -i pcap/$f -w /tmp/reader.out`,将输入文件通过 nDPI 库处理,并将结果写入到临时文件 `/tmp/reader.out` 中。 - 接着,使用 `diff` 命令比较结果文件和临时文件的差异,使用 `wc -l` 命令统计差异行数,并将结果存储在 `NUM_DIFF` 变量中。 - 如果差异行数为 0,则打印 `"$f OK"` 表示结果一致。 - 否则,打印 `"$f ERROR"` 表示结果不一致,并打印执行的命令和差异的内容。 - 最后,删除临时文件 `/tmp/reader.out`。 整个脚本的目的是使用 `nDPI` 库对抓取的数据进行协议识别,生成对应的结果文件,并在检查阶段验证识别结果是否正确。如果识别结果与期望结果不一致,则输出错误信息,并将退出状态码设为 1。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值