1. 工作机制

每个解剖器(dissector )解码其协议的一部分,然后将解码交给后续解剖器以获得封装协议。
每个解剖都从Frame解剖器开始,它解剖捕获文件本身的数据包细节(例如时间戳)。从那里它将数据传递给最低级别的数据解剖器,例如以太网头部的以太网解剖器。然后将有效载荷传递给下一个解剖器(例如IP),依此类推。在每个阶段,将解码和显示分组的细节。
解剖可以以两种可能的方式实施。一种是将解剖模块编译到主程序中,这意味着它始终可用。另一种方法是创建一个插件(共享库或DLL)来注册自己来处理解剖。
将解剖器作为插件或内置的方法几乎没有什么区别。最重要的是,插件的重建周期比内置插件的重建周期要短得多。因此,从插件开始使初始开发更简单,而完成的代码可能作为内置解剖器更有意义。

2. 添加一个基本的dissector

我们将从制作的“foo”协议开始。它包含以下基本项目。

A packet type - 8 bits, possible values: 1 - initialisation, 2 - terminate, 3 - data.
A set of flags stored in 8 bits, 0x01 - start packet, 0x02 - end packet, 0x04 - priority packet.
A sequence number - 16 bits.
An IPv4 address.

2.1 设置dissector

你需要做出的第一个决定是,这个解剖器是一个内置的解剖器,包含在主程序中,还是一个插件。
插件最初是最容易编写的,所以让我们从头开始。插件可以很容易地作为内置运行,所以我们没有丢失任何东西。

// 1. 初始化
#include“config.h” 

#include <epan / packet.h> 

#define FOO_PORT 1234 

static int proto_foo = -1; 


void 
proto_register_foo(void)
{ 
    proto_foo = proto_register_protocol(
        “FOO Protocol”,/ * name * / 
        “FOO”,/ * short name * / 
        “foo”/ * abbrev * / 
        ); 
}

首先,我们将调用proto_register_protocol()注册协议。我们可以给它三个名字,用于在不同的地方展示。完整和短名称用于例如“首选项”和“启用协议”对话框以及文档中生成的字段名称列表。缩写用作显示过滤器名称。

// 2. 注册handle,与端口关联
void 
proto_reg_handoff_foo(void)
{ 
    static dissector_handle_t foo_handle; 

    foo_handle = create_dissector_handle(dissect_foo,proto_foo); 
    dissector_add_uint(“udp.port”,FOO_PORT,foo_handle); 
}

j接下来,我们创建一个解剖器handle; 它与foo协议相关联,并与调用例程进行实际解剖。然后我们将句柄与UDP端口号相关联,这样主程序就知道在该端口上获得UDP流量时会调用我们。

// 3. dissect_foo回调函数
static int 
dissect_foo(tvbuff_t * tvb,packet_info * pinfo,proto_tree * tree _U_,void * data _U_)
{ 
	 / *将字符串“FOO”设置为我们的协议* / 
    col_set_str(pinfo-> cinfo,COL_PROTOCOL,“FOO”); 
    / *清除信息列中的内容* / 
    col_clear(pinfo-> cinfo,COL_INFO); 

    return tvb_captured_length(tvb); 
}

dissect_foo这个函数被调用来剖析呈现给它的包。
包数据保存在一个特殊的缓冲区中,在这里称为tvb。随着我们对协议细节的深入了解,我们将对这一点相当熟悉。
packet_info 结构包含协议的一般数据,我们可以在这里更新信息。
tree参数是进行详细剖析的地方。

2.2 dissector解码

现在我们已经启动并运行了基本的dissector,让我们用它做点什么。最简单的事情就是标记有效负载。
我们要做的第一件事是构建一个子树来解码我们的结果。这有助于在详细显示中保持良好的外观。

static int 
dissect_foo(tvbuff_t * tvb,packet_info * pinfo,proto_tree * tree,void * data _U_)
{ 

    col_set_str(pinfo-> cinfo,COL_PROTOCOL,“FOO”); 
    / *清除信息列中的内容* / 
    col_clear(pinfo-> cinfo,COL_INFO); 

    proto_item * ti = proto_tree_add_item(tree,proto_foo,tvb,0,-1,ENC_NA); 

    return tvb_captured_length(tvb); 
}

proto_tree_add_item为dissector添加一个子树。该子树将保存此协议的所有细节。

现在让我们进入下一步并添加一些协议解析。对于这一步,我们需要构建一些有助于解剖的表格。这需要对proto_register_foo()进行一些补充。

static int hf_foo_pdu_type = -1; 
static gint ett_foo = -1;

void 
proto_register_foo(void)
{ 
    static hf_register_info hf [] = { 
        {&hf_foo_pdu_type,/*此节点的索引*/
            {“FOO PDU Type”,/*此节点名字*/
            “foo.type”, /* 这是过滤字符串。它使我们能够foo.type=1在过滤器框中输入构造。*/
            FT_UINT8,/* 指定此项为8位无符号整数。*/
            BASE_DEC,/*告诉它打印为十进制数。/
            NULL,0x0,
            NULL,HFILL} 
        } 
    }; 

    / *设置协议子树数组* / 
    static gint * ett [] = { 
        &ett_foo 
    }; 

    proto_foo = proto_register_protocol(
        “FOO Protocol”,/ * name * / 
        “FOO”,/ * short name * / 
        “foo”/ * abbrev * / 
        ); 

    proto_register_field_array(proto_foo,hf,array_length(hf));
    proto_register_subtree_array(ett,array_length(ett)); 
}

接下来,数据包开头的一个字节数据,用于定义foo协议的数据包类型。

proto_item * ti = proto_tree_add_item(tree,proto_foo,tvb,0,-1,ENC_NA); 
proto_tree * foo_tree = proto_item_add_subtree(ti,ett_foo); 
proto_tree_add_item(foo_tree,hf_foo_pdu_type,tvb,0,1,ENC_BIG_ENDIAN);

proto_item_add_subtree()调用将一个子节点添加到协议树中,这是我们进行详细剖析的地方。该节点的扩展由ett_foo 变量控制。这会记住在数据包之间移动时是否应该扩展节点。所有后续解剖都将添加到此树中,您可以从下一个调用中看到。
proto_tree_add_item()在foo_tree中调用,这次使用hf_foo_pdu_type控制项的格式。pdu类型是一个字节的数据,从0开始。我们假设它是网络顺序(也称为大端)ENC_BIG_ENDIAN。

接下来解析所有的字节。

...
static int hf_foo_flags = -1;
static int hf_foo_sequenceno = -1;
static int hf_foo_initialip = -1;
...

static int
dissect_foo(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
{
    gint offset = 0; /*引入了一个新的变量offset,以帮助跟踪我们在数据包解剖中的位置。*/

    ...
    proto_item *ti = proto_tree_add_item(tree, proto_foo, tvb, 0, -1, ENC_NA);
    proto_tree *foo_tree = proto_item_add_subtree(ti, ett_foo);
    proto_tree_add_item(foo_tree, hf_foo_pdu_type, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset += 1;
    proto_tree_add_item(foo_tree, hf_foo_flags, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset += 1;
    proto_tree_add_item(foo_tree, hf_foo_sequenceno, tvb, offset, 2, ENC_BIG_ENDIAN);
    offset += 2;
    proto_tree_add_item(foo_tree, hf_foo_initialip, tvb, offset, 4, ENC_BIG_ENDIAN);
    offset += 4;
    ...

    return tvb_captured_length(tvb);
}

void
proto_register_foo(void) {
    ...
        ...
        { &hf_foo_flags,
            { "FOO PDU Flags", "foo.flags",
            FT_UINT8, BASE_HEX,
            NULL, 0x0,
            NULL, HFILL }
        },
        { &hf_foo_sequenceno,
            { "FOO PDU Sequence Number", "foo.seqn",
            FT_UINT16, BASE_DEC,
            NULL, 0x0,
            NULL, HFILL }
        },
        { &hf_foo_initialip,
            { "FOO PDU Initial IP", "foo.initialip",
            FT_IPv4, BASE_NONE,
            NULL, 0x0,
            NULL, HFILL }
        },
        ...
    ...
}

2.3 改善dissector

我们当然可以通过一些额外的数据来改进协议的显示。第一步是添加一些文本标签。让我们从标记数据包类型开始。

/* 将名称添加到协议*/
static const value_string packettypenames [] = { 
    {1,“Initialise”},
    {2,“Terminate”},
    {3,“Data”},
    {0,NULL} 
};

{&hf_foo_pdu_type,
        {“FOO PDU Type”,“foo.type”,
        FT_UINT8,BASE_DEC,
        VALS(packettypenames),0x0,
        NULL,HFILL} 
    }


/*将标志添加到协议*/
#define FOO_START_FLAG 0x01 
#define FOO_END_FLAG 0x02 
#define FOO_PRIORITY_FLAG 0x04 

static int hf_foo_startflag = -1; 
static int hf_foo_endflag = -1; 
static int hf_foo_priorityflag = -1; 

static int 
dissect_foo(tvbuff_t * tvb,packet_info * pinfo,proto_tree * tree,void * data _U_)
{ 
    ... 
        ... 
        static const int * bits [] = { 
            &hf_foo_startflag,
            &hf_foo_endflag,
            &hf_foo_priorityflag 
        }; 

        proto_tree_add_bitmask(foo_tree,tvb,offset,hf_foo_flags,ett_foo,bits,ENC_BIG_ENDIAN); 
        offset + = 1; 
        ... 
    ...
    return tvb_captured_length(tvb); 
} 

void 
proto_register_foo(void){ 
    ... 
        ... 
        {&hf_foo_startflag,
            {“FOO PDU Start Flags”,“foo.flags.start”,
            FT_BOOLEAN,8,
            NULL,FOO_START_FLAG,
            NULL,HFILL} 
        },
        {&hf_foo_endflag,
            { “FOO PDU End Flags”,“foo.flags.end”,
            FT_BOOLEAN,8,
            NULL,FOO_END_FLAG,
            NULL,HFILL} 
        },
        {&hf_foo_priorityflag,
            {“FOO PDU Priority Flags”,“foo.flags.priority”,
            NULL,FOO_PRIORITY_FLAG,
            NULL,HFILL} 
        } 
        ... 
    ... 
} 
...

现在看起来已经开始看起来相当全面,但我们还可以采取其他一些措施让事情看起来更漂亮。
首先,我们可以设置非详细视图的INFO列,以显示它是什么类型的PDU - 这在查看协议跟踪时非常有用。其次,我们还可以在解剖窗口中显示此信息。

static int
dissect_foo(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data _U_)
{
    gint offset = 0;
    guint8 packet_type = tvb_get_guint8(tvb, 0);

    col_set_str(pinfo->cinfo, COL_PROTOCOL, "FOO");
    /* Clear out stuff in the info column */
    col_clear(pinfo->cinfo,COL_INFO);
    col_add_fstr(pinfo->cinfo, COL_INFO, "Type %s",
             val_to_str(packet_type, packettypenames, "Unknown (0x%02x)"));

    proto_item *ti = proto_tree_add_item(tree, proto_foo, tvb, 0, -1, ENC_NA);
    proto_item_append_text(ti, ", Type %s",
        val_to_str(packet_type, packettypenames, "Unknown (0x%02x)"));
    proto_tree *foo_tree = proto_item_add_subtree(ti, ett_foo);
    proto_tree_add_item(foo_tree, hf_foo_pdu_type, tvb, offset, 1, ENC_BIG_ENDIAN);
    offset += 1;

    return tvb_captured_length(tvb);
}

3. 数据包重组

有些协议一个大包有时需要分割传送。在这种情况下,解剖不能正确进行,直到你有了所有的数据。第一个包没有足够的数据,随后的包没有预期的格式。要解剖这些包,您需要等到所有包都到达后再开始解剖。

3.1 重组udp包

作为一个例子,让我们检查一个协议,它是在UDP之上分层的,它分裂了自己的数据流。如果一个数据包大于某个给定大小,它将被分割成块,并在协议中以某种方式发出信号。

为了处理这些流,我们需要一些东西来触发。我们需要知道这个包是多包序列的一部分。我们需要知道序列中有多少包。我们还需要知道什么时候有所有的包。

对于本例,我们假设有一个简单的协议内信令机制来提供详细信息。一种标志字节,表示多包序列和最后一个包的存在,后面跟着该序列的ID和包序列号。

msg_pkt ::= SEQUENCE {
    .....
    flags ::= SEQUENCE {
        fragment    BOOLEAN,
        last_fragment   BOOLEAN,
    .....
    }
    msg_id  INTEGER(0..65535),
    frag_id INTEGER(0..65535),
    .....
}


#include <epan/reassemble.h>
   ...
save_fragmented = pinfo->fragmented;
flags = tvb_get_guint8(tvb, offset); offset++;
if (flags & FL_FRAGMENT) { /* fragmented */
    tvbuff_t* new_tvb = NULL;
    fragment_data *frag_msg = NULL;
    guint16 msg_seqid = tvb_get_ntohs(tvb, offset); offset += 2;
    guint16 msg_num = tvb_get_ntohs(tvb, offset); offset += 2;

    pinfo->fragmented = TRUE;
    frag_msg = fragment_add_seq_check(
    	msg_reassembly_table,/*用于记账*/
        tvb, /*正在解析的tvb缓冲区*/
        offset, /*数据包开始的偏移量*/
        pinfo, /*The provided packet info*/
        msg_seqid, /*The sequence number of the fragment stream*/
        NULL, /* ID for fragments belonging together */
        msg_num, /*the packet number within the sequence */
        tvb_captured_length_remaining(tvb, offset), /* fragment length - to the end */
        flags & FL_FRAG_LAST); /* More fragments? */

我们首先保存这个数据包的碎片状态,以便稍后恢复它。接下来是一些特定于协议的内容,从流中挖掘片段数据(如果有的话)。在确定了它的存在之后,我们让函数fragment_add_seq_check()来完成它的工作。

new_tvb = process_reassembled_data(tvb, offset, pinfo,
        "Reassembled Message", frag_msg, &msg_frag_items,
        NULL, msg_tree);

    if (frag_msg) { /* Reassembled */
        col_append_str(pinfo->cinfo, COL_INFO,
                " (Message Reassembled)");
    } else { /* Not last packet of reassembled Short Message */
        col_append_fstr(pinfo->cinfo, COL_INFO,
                " (Message fragment %u)", msg_num);
    }

    if (new_tvb) { /* take it all */
        next_tvb = new_tvb;
    } else { /* make a new subset */
        next_tvb = tvb_new_subset_remaining(tvb, offset);
    }
}
else { /* Not fragmented */
    next_tvb = tvb_new_subset_remaining(tvb, offset);
}

.....
pinfo->fragmented = save_fragmented;

将片段数据传递给重组处理程序后,我们现在可以检查是否有整个消息。如果有足够的信息,该例程将返回新重组的数据缓冲区。

之后,我们向显示器添加一些信息性消息,以显示这是序列的一部分。然后可以继续对缓冲区和解剖进行一些操作。通常你可能不会进一步解剖,除非碎片已经重新组装,因为没有太多东西可以找到。有时,如果您愿意,可以对序列中的第一个数据包进行部分解码。

/*初始化*/
static reassembly_table reassembly_table; 

static void 
proto_register_msg(void)
{ 
    reassembly_table_register(&msg_reassemble_table,
        &addresses_ports_reassembly_table_functions); 
}

首先,reassembly_table,在协议初始化例程中声明并初始化结构。第二个参数指定应该用于标识片段的函数。我们将使用 addresses_ports_reassembly_table_functions以便通过给定的序列号(msg_seqid),来自数据包的源和目标地址以及端口来识别片段。

接下来,fragment_items分配一个结构并用一系列ett项,hf数据项和字符串标记填充。ett和hf值应该包含在相关表中,就像协议可能使用的所有其他变量一样。hf变量需要放在结构中,如下所示。当然,名称可能需要调整。

/*数据结构*/
    static int hf_msg_fragments = -1;
    static int hf_msg_fragment = -1;
    static int hf_msg_fragment_overlap = -1;
    static int hf_msg_fragment_overlap_conflicts = -1;
    static int hf_msg_fragment_multiple_tails = -1;
    static int hf_msg_fragment_too_long_fragment = -1;
    static int hf_msg_fragment_error = -1;
    static int hf_msg_fragment_count = -1;
    static int hf_msg_reassembled_in = -1;
    static int hf_msg_reassembled_length = -1;
    ...
    static gint ett_msg_fragment = -1;
    static gint ett_msg_fragments = -1;
    ...
    static const fragment_items msg_frag_items = {
        /* Fragment subtrees */
        &ett_msg_fragment,
        &ett_msg_fragments,
        /* Fragment fields */
        &hf_msg_fragments,
        &hf_msg_fragment,
        &hf_msg_fragment_overlap,
        &hf_msg_fragment_overlap_conflicts,
        &hf_msg_fragment_multiple_tails,
        &hf_msg_fragment_too_long_fragment,
        &hf_msg_fragment_error,
        &hf_msg_fragment_count,
        /* Reassembled in field */
        &hf_msg_reassembled_in,
        /* Reassembled length field */
        &hf_msg_reassembled_length,
        /* Tag */
        "Message fragments"
    };
    ...
    static hf_register_info hf[] =
    {
    ...
    {&hf_msg_fragments,
        {"Message fragments", "msg.fragments",
        FT_NONE, BASE_NONE, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_fragment,
        {"Message fragment", "msg.fragment",
        FT_FRAMENUM, BASE_NONE, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_fragment_overlap,
        {"Message fragment overlap", "msg.fragment.overlap",
        FT_BOOLEAN, 0, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_fragment_overlap_conflicts,
        {"Message fragment overlapping with conflicting data",
        "msg.fragment.overlap.conflicts",
        FT_BOOLEAN, 0, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_fragment_multiple_tails,
        {"Message has multiple tail fragments",
        "msg.fragment.multiple_tails",
        FT_BOOLEAN, 0, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_fragment_too_long_fragment,
        {"Message fragment too long", "msg.fragment.too_long_fragment",
        FT_BOOLEAN, 0, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_fragment_error,
        {"Message defragmentation error", "msg.fragment.error",
        FT_FRAMENUM, BASE_NONE, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_fragment_count,
        {"Message fragment count", "msg.fragment.count",
        FT_UINT32, BASE_DEC, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_reassembled_in,
        {"Reassembled in", "msg.reassembled.in",
        FT_FRAMENUM, BASE_NONE, NULL, 0x00, NULL, HFILL } },
    {&hf_msg_reassembled_length,
        {"Reassembled length", "msg.reassembled.length",
        FT_UINT32, BASE_DEC, NULL, 0x00, NULL, HFILL } },
    ...
    static gint *ett[] =
    {
    ...
    &ett_msg_fragment,
    &ett_msg_fragments
    ...

这些hf变量在重组例程内部使用,以创建有用的链接,并将数据添加到解剖。它产生从一个数据包到另一个数据包的链接,例如具有到完全重组数据包的链接的部分数据包。同样地,存在来自重组的各个分组的后向指针。其他变量用于标记错误。

3.2 重组TCP分段

#include "config.h"

#include <epan/packet.h>
#include <epan/prefs.h>
#include "packet-tcp.h"

...

#define FRAME_HEADER_LEN 8

/* This method dissects fully reassembled messages */
static int
dissect_foo_message(tvbuff_t *tvb, packet_info *pinfo _U_, proto_tree *tree _U_, void *data _U_)
{
    /* TODO: implement your dissecting code */
    return tvb_captured_length(tvb);
}

/* determine PDU length of protocol foo */
static guint
get_foo_message_len(packet_info *pinfo _U_, tvbuff_t *tvb, int offset, void *data _U_)
{
    /* TODO: change this to your needs */
    return (guint)tvb_get_ntohl(tvb, offset+4); /* e.g. length is at offset 4 */
}

/* The main dissecting routine */
static int
dissect_foo(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void *data)
{
    tcp_dissect_pdus(tvb, pinfo, tree, TRUE, FRAME_HEADER_LEN,
                     get_foo_message_len, dissect_foo_message, data);
    return tvb_captured_length(tvb);
}

...

只需调用tcp_dissect_pdus(),将消息解析代码移动到另一个函数中。每当重新组装消息时,都会调用此函数。
参数tvb,pinfo,tree和data只是移交给 tcp_dissect_pdus()。
第4个参数是一个标志,用于指示是否应重新组装数据。这也可以根据解剖器偏好来设置。
参数5表示至少有多少数据可用于确定foo消息的长度。
参数6是指向返回此长度的方法的函数指针。当至少前一个参数中给出的字节数可用时,它会被调用。
参数7是指向真实消息解析器的函数指针。
参数8是从父解剖器传入的数据。