近日,为了更好地使用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
/* 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
11void 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
12static 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.common
和Makefile.am
文件中涉及到相关文件和解析器名称的地方一定要修改正确。moduldeinfo.h
和moduleinfo.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
19static 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
13static 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
2proto_register_field_array(proto_foo,hf,array_length(hf));
proto_register_subtree_array(ett,array_length(ett));
变量hf_foo_pdu_type
和ett_foo
依然需要在文件顶部的某处予以声明。
例 9.7. 解析器全局数据结构.1
2static int hf_foo_pdu_type=-1;
static gint ett_foo=-1;
现在我们就可以对协议细节的显示做一番改善了。
例 9.8. 解析器开始数据包解析.1
2
3
4
5
6
7
8
9
10if(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
。我们假设它依照网络字节顺序,所以将最后一个参数设为FALSE
(TRUE
表示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
7static 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//添加到文件头部
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
34static 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)
字段,当然是在它显示的情况下,同样,我们也会将该数据添加到解析树的基部。