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是从父解剖器传入的数据。