【翻译】为Wireshark添加一个基础的解析器

近日,为了更好地使用Wireshark对研发中项目进行报文动态分析,尝试编写Wireshark解析器插件,学习了Wireshark开发者向导中的9.2.添加一个基础的解析器一节,顺便翻译了这部分内容。


9.2. 添加一个基础的解析器

下面我们将循序渐进地设计一个基础的解析器。首先我们来虚构一个简单的网络协议foo。它依次包含如下构成要素:

  • 包类型字段(占用8比特位,可能的值为:1,初始;2,终结;3,数据);
  • 标志集字段(占用8比特位:0x01,开始包;0x02,结束包;0x04,优先包);
  • 序列号字段(占用16比特位);
  • IP地址字段(占用32比特位)。

9.2.1. 创建解析器

首先您需要选择解析器的类型:内置型(包含在主程序中)或插件型。
对于初学者来说插件是容易编写的,所以我们还是先做一个插件型解析器吧。温馨提示,解析器由插件型转为内置型是件轻松的事情——所以我们不会因此而失去什么。

例 9.1. 解析器初始设定.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <epan/packet.h>
#include <epan/prefs.h>

/* forward reference */
void proto_register_foo();
void proto_reg_handoff_foo();
static void dissect_foo(tvbuff_t *tvb,packet_info *pinfo,proto_tree *tree);

static int proto_foo=-1;
static int global_foo_port=1234;
static dissector_handle_t foo_handle;

void proto_register_foo(void)
{
if(proto_foo==-1)
{
proto_foo=proto_register_protocol(
"FOO Protocol", /* name */
"FOO", /* short name */
"foo" /* abbrev */
);
}
}

现在来逐一分析这段代码。首先我们有一些常规的包含文件,最好依惯例在文件开始包含进来。随后是一些函数的前置声明,我们稍后定义它们。
接下来我们定义了一个整型变量proto_foo用于记录我们的协议注册信息。它被初始化为-1,当解析器注册到主程序中后,其值便会得到更新。这样做可保证我们方便地判断是否已经做了初始工作。将所有不打算对外输出的全局变量和函数声明为static是一个良好的习惯,因为这可以保证命名空间不被污染。通常这是容易做到的,除非您的解析器非常庞大以致跨越多个文件。
之后的模块变量global_foo_port则包含了协议使用的UDP端口号,我们会对通过该端口的数据流进行解析,当然这只是一个假设。
紧随其后的是解析器句柄foo_handle,我们稍后对它进行初始化。
至此我们已经拥有了和主程序交互的基本元素,接下来最好再把那些预声明的函数定义一下,就从注册函数proto_register_foo开始吧。
首先调用函数proto_register_protocol注册协议。我们能够给协议起3个名字以适用不同的地方。全名和短名用在诸如首选项(Preferences)已激活协议(Enabled protocols)对话框以及记录中已生成的域名列表内。缩略名则用于过滤器。
下面我们需要一个切换函数。

例 9.2. 解析器切换.

1
2
3
4
5
6
7
8
9
10
11
void proto_reg_handoff_foo(void)
{
static gboolean initialized=FALSE;

if(!initialized)
{
foo_handle=create_dissector_handle(dissect_foo,proto_foo);
dissector_add("udp.port",global_foo_port,foo_handle);
initialized=TRUE;
}
}

这段代码做了什么呢?如果解析器尚未初始化,则对它进行初始化。首先创建解析器。这时注册了了函数dissect_foo用于完成实际的解析工作。之后将该解析器与UDP端口号相关联,以使主程序收到该端口的UDP数据流时通知该解析器。
至此我们终于可以写一些解析代码了。不过目前我们仅写点儿基本功能占个位置。

例 9.3. 解析.

1
2
3
4
5
6
7
8
9
10
11
12
static void dissect_foo(tvbuff_t *tvb,packet_info *pinfo,proto_tree *tree)
{
if(check_col(pinfo->cinfo,COL_PROTOCOL))
{
col_set_str(pinfo->cinfo,COL_PROTOCOL,"FOO");
}
/* Clear out stuff in the info column */
if(check_col(pinfo->cinfo,COL_INFO))
{
col_clear(pinfo->cinfo,COL_INFO);
}
}

该函数用于解析传递给它的数据包。包数据由tvb参数指向的特殊缓冲区保管。现在我们已深入到协议的细节,对它们您肯定是了若指掌。包信息结构参数pinfo包含了协议的基本数据,以供我们更新。参数tree则指明了详细解析发生的地方。
这里我们仅做了保证通过的少量工作。前两行检查UI中协议(Protocol)列是否已显示。如果该列已存在,就在这儿显示我们的协议名称。这样人们就知道它被识别出来了。另外,如果信息(INFO)列已显示,我们就将它的内容清除。
至此我们已经准备好一个可以编译和安装的基本解析器。不过它目前只能识别和标示协议。
为了编译解析器并创建插件,还需要在解析器代码文件packet-foo.c所在目录下创建一些提供支持的文件:

  • Makefile.am - UNIX/Linux的makefile模板
  • Makefile.common – 包含了插件文件的名称
  • Makefile.nmake – 包含了针对Windows平台的Wireshark插件makefile
  • moduleinfo.h – 包含了插件版本信息
  • moduleinfo.nmake – 包含了针对Windows平台的DLL版本信息
  • packet-foo.c – 这是您的解析器原代码文件
  • plugin.rc.in – 包含了针对Windows平台的DLL资源模板

agentx插件的目录下(plugins\agentx目录下),您能够找到关于这些文件的一个好的例子。Makefile.commonMakefile.am文件中涉及到相关文件和解析器名称的地方一定要修改正确。moduldeinfo.hmoduleinfo.nmake文件中的版本信息也需要正确填充。一切准备妥善后就可以将解析器编译为DLL或共享库文件了(使用nmake工具)。将编译的结果拷贝到Wireshark安装目录中的plugin文件夹下,它就可以正常工作了。

9.2.2. 解析协议细节

现在我们已经有了一个可以运用的简单解析器,让我们再为它添点儿什么吧。首先想到的应该就是标示数据包的有效信息了。解析器在这方面给我们提供了支持。
首先要做的事情是创建一个子树以容纳我们的解析结果。这会使协议的细节显示得井井有条。现在解析器在两种情况下被调用:其一,用于获得数据包的概要信息;其二,用于获得数据包的详细信息。这两种情况可以通过树指针参数tree来进行区分。如果树指针为NULL,我们只需要提供概要信息;反之,我们就需要拆解协议完成细节的显示了。基于此,让我们来增强这个解析器吧。

例 9.4. 插入数据包解析.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void dissect_foo(tvbuff_t *tvb,packet_info *pinfo,proto_tree *tree)
{
if(check_col(pinfo->cinfo,COL_PROTOCOL))
{
col_set_str(pinfo->cinfo,COL_PROTOCOL,"FOO");
}
/* Clear out stuff in the info column */
if(check_col(pinfo->cinfo,COL_INFO))
{
col_clear(pinfo->cinfo,COL_INFO);
}

if(tree)
{
/* we are being asked for details */
proto_item *ti=NULL;
ti=proto_tree_add_item(tree,proto_foo,tvb,0,-1,FALSE);
}
}

这里我们为解析添加一个子树。它将用于保管协议的细节,仅在必要时显示这些内容。
我们还要标识被协议占据的数据区域。在我们的这种情况下,协议占据了传入数据的全部,因为我们假设协议没有封装其它内容。因此,我们用proto_tree_add_item函数添加新的树结点,将它添加到传入的协议树tree中,用协议句柄proto_foo标识它,用传入的缓冲区tvb作为数据,并将有效数据范围的起点设为0,长度设为-1(表示缓冲区内的全部数据)。至于最后的参数FALSE,我们暂且忽略。
做了这个更改之后,在包明细面板区中应该会出现一个针对该协议的标签;选择该标签后,在包字节面板区中包的剩余内容就会高亮显示。
现在进入下一步,添加一些协议解析功能。在这一步我们需要构建一组帮助解析的表结构。这需要对proto_register_foo函数做些修改。首先定义一组静态数组。

例 9.5. 定义数据结构.

1
2
3
4
5
6
7
8
9
10
11
12
13
static hf_register_info hf[]=
{
{
&hf_foo_pdu_type,
{"FOO PDU Type","foo.type",FT_UINT8,BASE_DEC,NULL, 0x0,NULL,HFILL}
}
};

/* Setup protocol subtree array */
static gint *ett[]=
{
&ett_foo
};

接下来,在协议注册代码之后,我们对这些数组进行注册。

例 9.6. 注册数据结构.

1
2
proto_register_field_array(proto_foo,hf,array_length(hf));
proto_register_subtree_array(ett,array_length(ett));

变量hf_foo_pdu_typeett_foo依然需要在文件顶部的某处予以声明。

例 9.7. 解析器全局数据结构.

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

现在我们就可以对协议细节的显示做一番改善了。
例 9.8. 解析器开始数据包解析.

1
2
3
4
5
6
7
8
9
10
if(tree)
{
/* we are being asked for details */
proto_item *ti=NULL;
proto_tree *foo_tree=NULL;

ti=proto_tree_add_item(tree,proto_foo,tvb,0,-1,FALSE);
foo_tree=proto_item_add_subtree(ti,ett_foo);
proto_tree_add_item(foo_tree,hf_foo_pdu_type,tvb,0,1,FALSE);
}

协议的解析变得愈发有趣了。我们提取出协议的第一部分。数据包的首字节定义了foo协议的包类型。
函数proto_item_add_subtree的调用在协议树中添加了一个子树,我们就在这里进行细节解析。子树的展开受控于变量ett_foo。当您在协议间切换时,由它记录子树是否展开。正像您从下面的函数调用中看到的那样,随后的所有解析都会添加到该子树中。函数proto_tree_add_item用于为子树foo_tree添加项,这次调用使用变量hf_foo_pdu_type控制项格式。PDU(协议数据单元)类型是一个单字节数据,位于数据包的首字节,我们将有效数据范围的起点设为0,长度设为1。我们假设它依照网络字节顺序,所以将最后一个参数设为FALSETRUE表示little endian,FALSE表示big endian)。尽管对于单字节数据无所谓字节顺序,但我们最好还是保持指定字节顺序的良好习惯。
如果详细查看静态数组中hf_foo_pdu_type的声明,我们能够获悉定义的明细。

  • hf_foo_pdu_type:节点索引。
  • FOO PDU Type:项标示。
  • foo.type:过滤字符串。我们可以在过滤框中输入诸如foo.type=1的结构。
  • FT_UNIT8:指定该项数据是一个8比特位的无符号整型。这和我们之前调用函数时设置的一字节有效数据是相一致的。
  • BASE_DEC:针对整型数据,指定将其作为十进制数显示。当然视具体情况也可以设置为BASE_HEX(十六进制)和BASE_OCT(八进制),以使数据更易辨识。

至于结构中余下的部分我们暂且忽略。
如果您现在安装并试用这个插件,就会发现一些有用的东西了。
接下来让我们完成这个简单协议的解析工作吧。我们需要再添加一些hf数组成员和程序调用。

例 9.9. 完成数据包解析.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//添加到文件开始的某个地方,作为全局变量
static int hf_foo_flags=-1;
static int hf_foo_sequenceno=-1;
static int hf_foo_initialip=-1;

//添加到“proto_register_foo”函数中的“hf”数组中,作为数组的成员
{
&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}
},

//添加到“dissect_foo”函数中,实现数据包的解析
gint offset=0;
ti = proto_tree_add_item(tree,proto_foo,tvb,0,-1,FALSE);
foo_tree=proto_item_add_subtree(ti,ett_foo);
proto_tree_add_item(foo_tree,hf_foo_pdu_type,tvb,offset,1,FALSE);
offset+=1;
proto_tree_add_item(foo_tree,hf_foo_flags,tvb,offset,1,FALSE);
offset+=1;
proto_tree_add_item(foo_tree,hf_foo_sequenceno,tvb,offset,2,FALSE);
offset+=2;
proto_tree_add_item(foo_tree,hf_foo_initialip,tvb,offset,4,FALSE);
offset+=4;

这段代码解析了这个简单的虚构协议的全部内容。我们引入了一个新的变量offset以记录数据包解析的位置。将这些额外的代码块放入合适的位置,整个协议就可以得到全面的解析。

9.2.3. 改善解析信息

我们可以添加一些数据来改善协议的显示。第一步是添加一些文本标签。我们先来标示数据包类型。对于这类情况,添加一些额外的数据是有好处的。首先我们添加一个简单的类型名表。

例 9.10. 命名数据包类型.

1
2
3
4
5
6
7
static const value_string packettypenames[]=
{
{1,"Initialise"},
{2,"Terminate"},
{3,"Data"},
{0,NULL}
};

该数据结构可以方便地查询到值所对应的名称。有对其直接操作的函数,不过我们不需调用它们,因为这已经由相关的支持代码实现了。我们仅需使用VALS宏在数据的适当部分指定细节即可。

例 9.11. 为协议添加名称.

1
2
3
4
{
&hf_foo_pdu_type,
{"FOO PDU Type","foo.type",FT_UINT8,BASE_DEC,VALS(packettypenames),0x0,NULL,HFILL}
}

这有助于数据包的辨认,我们可以对标志结构做类似的处理。不过实现这个还需向表中添加更多的数据。

例 9.12. 为协议添加标志.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//添加到文件头部
#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;

//添加到“proto_register_foo”函数中的“hf”数组中,作为数组的成员
{
&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",FT_BOOLEAN,8,NULL,FOO_PRIORITY_FLAG,NULL,HFILL
}
},

//添加到“dissect_foo”函数中的合适位置
proto_tree_add_item(foo_tree,hf_foo_flags,tvb,offset,1,FALSE);
proto_tree_add_item(foo_tree,hf_foo_startflag,tvb,offset,1,FALSE);
proto_tree_add_item(foo_tree,hf_foo_endflag,tvb,offset,1,FALSE);
proto_tree_add_item(foo_tree,hf_foo_priorityflag,tvb,offset,1,FALSE);
offset+=1;

这里需要解释一下。对于标志字段,由于每位表示一种标志,我们就采用FT_BOOLEAN类型,以表示标志是否设置。其次,我们在数据的第七域中包含了标志掩码,以使系统获得相关的比特位。我们也将第五域改为8,指示当获取标志字段时读取8位数据。最后我们将这些新建的结构添加到解析程序中。注意,我们一定要对每个标志处理保持相同的偏移。
现在,该解析器的功能已显得相当完整了,但我们仍可实施一套方案使其趋于完美。目前我们的解析仅用Foo Protocol来标示数据包,这虽然正确但实用价值不大。我们能够添加一些细节来对其增强。首先我们需要获得协议类型的实际值,这可以通过函数tvb_get_guint8方便地做到。获得该值后我们就可以开展工作了。首先我们可以在包列表面板区的信息(INFO)列显示PDU的类别信息——当查看协议踪迹时这是非常有用的。其次,我们也可以在包明细面板区中显示这些信息。

例 9.13. 增强显示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void dissect_foo(tvbuff_t *tvb,packet_info *pinfo,proto_tree *tree)
{
guint8 packet_type=tvb_get_guint8(tvb,0);

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

if(tree)
{
/* we are being asked for details */
proto_item *ti=NULL;
proto_tree *foo_tree=NULL;
gint offset=0;

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

这里,在获得前8比特位的值后,我们使用内置的应用函数val_to_str获得该值对应的数据。如果值不存在,我们会直接以16进制形式显示该值。我们使用该数据两次,一次用于列表的信息(INFO)字段,当然是在它显示的情况下,同样,我们也会将该数据添加到解析树的基部。

君子爱财,取之有道;弱水三千,只取一元
0%