:::info 编写于2015.09, 其中一些东西可能已经不适合当前版本, 或者某些理解有些问题, 仅供参考 :::

1 引言

Wireshark是一款优秀的开源协议分析软件,多年来,全球无数开发者为Wireshark编写了数千种协议的解析插件(版本1.12.6已有1500余种),再加上强大易用的分析功能,使其几乎成为协议分析相关人员必备的工具之一。
然而,并没有一种工具可以完全满足所有用户的需要,强大如Wireshark者,也是如此。尤其对于专业的协议分析、安全人员来说,在实际的工作中,往往需要分析某些私有协议的报文,或者用到官方wireshark没有提供的某些功能;而某些开发人员,则需要将Wireshark的报文解析功能移植到自己的应用场景中……这一切,都需要我们在理解Wireshark工作原理的基础上,对其进行二次开发。
Wireshark的两大特点使二次开发比较容易:

  • 代码是开源的
  • 提供了插件机制(C/Lua)

    2 wireshark原理剖析

    本节相关内容可参考Wireshark开发指南第6章”How wireshark works”.

    2.1 总体结构

    Wireshark: 二次开发 (旧) - 图1

功能模块的细节(/epan等表示源代码目录):

模块名 功能 源码子目录
GTK/Qt 处理所有的用户输入/输出(所有的窗口,对话框等等) /ui
GTK: /ui/gtk
Qt: /ui/qt
Core 主要的”粘合代码”(glue code),它把其他的块组合到一起 /
Epan
(Ethereal Packet Analyzer)
协议树(Protocol-Tree) - 保存捕获文件的协议信息数据 /epan
解析器(Dissectors) - 多种协议的解析器 /epan/dissectors
插件(Plugins) - 一些用插件实现的协议解析器 /plugins
显示过滤器(Display-Filters) - 显示过滤器引擎 /epan/dfilter
Wiretap wiretap库用于读/写libpcap格式或者其他文件格式的捕获文件 /wiretap
Capture 抓包引擎相关接口 /
Dumpcap 抓包引擎. 这是唯一需要提升权限来执行的部 /
WinPcap/libpcap (不是Wireshark包的一部分) - 依赖于平台的包捕获库,包含捕获过滤器引擎.这就是我们为什么有不同的显示和捕获 两套过滤语法的原因 - 因为用了两种不同的过滤引擎 -

2.2 抓包

捕获从网络适配器提取包,并将其保存到硬盘上.
访问底层网络适配器需要提升的权限,因此和底层网卡抓包的功能被封装在dumpcap中,这是Wireshark中唯一需要特权执行的程序,代码的其他部分(包括解析器,用户界面等等)只需要普通用户权限。
为了隐藏所有底层的机器依赖性,使用了libpcap/WinPcap库.这此库提供了从多种不同的网络接口 类型(Ethernet, Token Ring,…)上捕获包的通用接口.

2.3 抓包文件

Wireshark可以读写libpcap格式的捕获文件,这是它的默认文件格式,被用于其他很多网络捕获工具, 如tcpdump.另外,Wireshark还可以读写其他网络捕获工具使用的多种不同的文件格式.wiretap库, 和Wireshark一起开发,提供了读写所有这些文件格式的通用接口.如果你需要添加其他的捕获文件格式,应从此处着手.
pcap文件的封装格式如下图所示。magic number的值对于以主机字节序写入的文件来说是0x1a2b3c4d。
Wireshark: 二次开发 (旧) - 图2
两个重要struct见 /wiretap/libpcap.h。

  1. /* "libpcap" file header (minus magic number). */
  2. struct pcap_hdr {
  3. unsigned short version_major;
  4. unsigned short version_minor;
  5. int thiszone;
  6. unsigned int sigfigs;
  7. unsigned int snaplen;
  8. unsigned int network;
  9. };
  10. /* "libpcap" record header. */
  11. struct pcaprec_hdr {
  12. unsigned int ts_sec;
  13. unsigned int ts_usec;
  14. unsigned int incl_len;
  15. unsigned int orig_len;
  16. };

2.4 解析报文

当Wireshark从文件中载入包时,会解析每一个包.Wireshark尝试探测包类型并尽可能地取得更多的包信息.然而此时,只需要显示在报文列表窗格(packet list pane)的信息.
当用户在包列表窗格中选择特定的包时,它会被重新解析一次.此时,Wireshark尝试取得每条信息并显示在报文细节窗格(packet detail pane)中.
Wireshark支持多种文件格式,这是由wiretap目录下代码来实现的。简单来说,在fire_access.c里有一个open_info结构体数组open_info_base,它的一部分如下:

  1. static struct open_info open_info_base[] = {
  2. { "Pcap", OPEN_INFO_MAGIC, libpcap_open, "pcap", NULL, NULL },
  3. { "PcapNG", OPEN_INFO_MAGIC, pcapng_open, "pcapng", NULL, NULL },
  4. { "NgSniffer", OPEN_INFO_MAGIC, ngsniffer_open, NULL, NULL, NULL },
  5. { "Snoop", OPEN_INFO_MAGIC, snoop_open, NULL, NULL, NULL },
  6. { "IP Trace", OPEN_INFO_MAGIC, iptrace_open, NULL, NULL, NULL },
  7. { "Netmon", OPEN_INFO_MAGIC, netmon_open, NULL, NULL, NULL },
  8. { "Netxray", OPEN_INFO_MAGIC, netxray_open, NULL, NULL, NULL },
  9. { "Radcom", OPEN_INFO_MAGIC, radcom_open, NULL, NULL, NULL },
  10. { "Nettl", OPEN_INFO_MAGIC, nettl_open, NULL, NULL, NULL },
  11. { "Visual", OPEN_INFO_MAGIC, visual_open, NULL, NULL, NULL },
  12. { "5 Views", OPEN_INFO_MAGIC, _5views_open, NULL, NULL, NULL },
  13. { "Network Instruments", OPEN_INFO_MAGIC, network_instruments_open, NULL, NULL, NULL },
  14. { "Peek Tagged", OPEN_INFO_MAGIC, peektagged_open, NULL, NULL, NULL },
  15. { "DBS Etherwatch", OPEN_INFO_MAGIC, dbs_etherwatch_open, NULL, NULL, NULL },
  16. { "K12", OPEN_INFO_MAGIC, k12_open, NULL, NULL, NULL },
  17. { "Catapult DCT 2000", OPEN_INFO_MAGIC, catapult_dct2000_open, NULL, NULL, NULL },
  18. { "Aethra", OPEN_INFO_MAGIC, aethra_open, NULL, NULL, NULL },
  19. { "BTSNOOP", OPEN_INFO_MAGIC, btsnoop_open, "log", NULL, NULL },
  20. { "EYESDN", OPEN_INFO_MAGIC, eyesdn_open, NULL, NULL, NULL },
  21. { "TNEF", OPEN_INFO_MAGIC, tnef_open, NULL, NULL, NULL },
  22. { "MIME Files with Magic Bytes", OPEN_INFO_MAGIC, mime_file_open, NULL, NULL, NULL },
  23. { "Lanalyzer", OPEN_INFO_HEURISTIC, lanalyzer_open, "tr1", NULL, NULL },
  24. ...
  25. };

在file_access.c中的init_open_routines函数中,它被赋值给全局变量open_routines。
在file_access.c中的wtap_open_offline函数中,会遍历此数组,直到其中的打开函数可以打开给定的文件。

  1. switch ((*open_routines[i].open_routine)(wth, err, err_info)) {
  2. case -1:
  3. /* I/O error - give up */
  4. wtap_close(wth);
  5. return NULL;
  6. case 0:
  7. /* No I/O error, but not that type of file */
  8. break;
  9. case 1:
  10. /* We found the file type */
  11. goto success;
  12. }

2.5 协议解析器

Wireshark启动时,所有解析器进行初始化和注册。要注册的信息包括协议名称、各个字段的信息、过滤用的关键字、要关联的下层协议与端口(handoff)等。在解析过程,每个解析器负责解析自己的协议部分, 然后把上层封装数据传递给后续协议解析器,这样就构成一个完整的协议解析链条。
解析链条的最上端是Frame解析器,它负责解析pcap帧头。后续该调用哪个解析器,是通过上层协议注册handoff信息时写在当前协议的hash表来查找的。
例如,考虑ipv4解析器有一个hash表,里面存储的信息形如下表2。当它解析完ipv4首部后,就可以根据得到的协议号字段,比如6,那么它就能从此hash表中找到后续解析器tcp。
Wireshark中实际的解析表有3种,分别是字符串表,整数表和启发式解析表。如下图3所示:

协议号 解析器指针
6 *tcp
17 *udp
……

Wireshark: 二次开发 (旧) - 图3

下面以ip协议为例,说明一下它的注册过程。
相关的重要数据结构与全局变量如下。
proto.c:

  1. /* Name hashtables for fast detection of duplicate names */
  2. static GHashTable* proto_names = NULL;
  3. static GHashTable* proto_short_names = NULL;
  4. static GHashTable* proto_filter_names = NULL;
  5. /** Register a new protocol.
  6. @param name the full name of the new protocol
  7. @param short_name abbreviated name of the new protocol
  8. @param filter_name protocol name used for a display filter string
  9. @return the new protocol handle */
  10. int
  11. proto_register_protocol(const char *name, const char *short_name, const char *filter_name);

三个全局的哈希表分别用于保存协议名称、协议缩略名和用于过滤器的协议名。

packet.c:

  1. struct dissector_table {
  2. GHashTable *hash_table;
  3. GSList *dissector_handles;
  4. const char *ui_name;
  5. ftenum_t type;
  6. int base;
  7. };
  8. static GHashTable *dissector_tables = NULL;
  9. /*
  10. * List of registered dissectors.
  11. */
  12. static GHashTable *registered_dissectors = NULL;
  13. static GHashTable *heur_dissector_lists = NULL;
  14. /* Register a dissector by name. */
  15. dissector_handle_t
  16. register_dissector(const char *name, dissector_t dissector, const int proto);
  17. /** A protocol uses this function to register a heuristic sub-dissector list.
  18. * Call this in the parent dissectors proto_register function.
  19. *
  20. * @param name the name of this protocol
  21. * @param list the list of heuristic sub-dissectors to be registered
  22. */
  23. void register_heur_dissector_list(const char *name,
  24. heur_dissector_list_t *list);
  25. /* a protocol uses the function to register a sub-dissector table */
  26. dissector_table_t register_dissector_table(const char *name, const char *ui_name, const ftenum_t type, const int base);

dissector_tables可以说是“哈希表的哈希表”,它以解析表名为键(如“ip.proto”),以dissector_table结构指针为值。在dissector_table中的哈希表以无符号数的指针为键(如协议号,为指针是glib hash表API的参数要求),以解析器handle为值;heur_dissector_lists是启发式解析相关的东西,这个问题留待以后研究;registered_dissectors是解析器哈希表,它以解析器名为键(如”ip”),以解析器句柄为值。

packet.h:

  1. typedef struct dissector_table *dissector_table_t;

packet-ip.c:

  1. static dissector_table_t ip_dissector_table;

proto_register_ip函数中:

  1. proto_ip = proto_register_protocol("Internet Protocol Version 4", "IPv4", "ip");
  2. ...
  3. /* subdissector code */
  4. ip_dissector_table = register_dissector_table("ip.proto", "IP protocol", FT_UINT8, BASE_DEC);
  5. register_heur_dissector_list("ip", &heur_subdissector_list);
  6. ...
  7. register_dissector("ip", dissect_ip, proto_ip);
  8. register_init_routine(ip_defragment_init);
  9. ip_tap = register_tap("ip");

register_dissector_table这个函数在packet.c中,在此函数内,创建了名为“ip.proto”的哈希表。解析ip协议后,会查询这个表,找出下一个解析器,并将后续数据的解析移交给它。
packet-ip.c,dissect_ip函数内:

  1. dissector_try_uint_new(ip_dissector_table, nxt, next_tvb, pinfo,
  2. parent_tree, TRUE, iph)


packet.c:

  1. /* Look for a given value in a given uint dissector table and, if found, call the dissector with the arguments supplied, and return TRUE, otherwise return FALSE. */
  2. gboolean
  3. dissector_try_uint_new(dissector_table_t sub_dissectors, const guint32 uint_val, tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, const gboolean add_proto_name, void *data)

在dissector_try_uint_new函数中,会找到协议号对应的解析器句柄,并使用它解析其余数据。

2.6 display filter

// TODO

2.7 启发式解析

// TODO

3 开发环境搭建

本节相关内容可参考Wireshark开发指南第2.2节”Win32/64: Step-by-Step Guide”
要对wireshark代码进行修改,除了下文介绍的lua插件的方式以外,都需要对wirehshark源码进行编译(C外置解析插件不需要编译整个wireshark,都需要下载wireshark源码及需要的库),因此有必要学习如何搭建Wireshark开发环境。
在Linux和Apple OS X系统上编译wireshark较为简单,这里从略,只介绍Windows下编译wireshark 64位版本的方法和步骤。
下面开始按顺序分小节介绍编译步骤,内容以Wireshark 1.12.x和1.99.x版本为依据,其他较旧版本大同小异。

3.1 Windows

3.1.1 下载源码

源码压缩包:https://www.wireshark.org/download/src/all-versions/
Git(应该是主线):git clone https://code.wireshark.org/review/wireshark

3.1.2 准备Visual C++

要编译wireshark,开发电脑上应该安装了Visual Studio并包括了Visual C++,请至少安装Visual Studio 2010以减少不必要的麻烦。

3.1.3 安装Qt

http://www.qt.io/download-open-source/#section-2下载与你的Visual Studio版本及处理器结构相对应的Qt版本。
注意,目前Qt官方安装包只对Visual Studio 2013提供了64bit支持,要使用Visual Studio 2010编译Wireshark,需要下载Qt opensource源码并自行编译为64二进制库。

3.1.4 准备PowerShell

在Win7之前的旧系统上编译Wireshark新版本需要安装PowerShell。

3.1.5 安装Cygwin及相关包

http://www.cygwin.com/下载Cygwin的安装程序,执行在线安装,后面将会看到,如果使用旧的Cygwin版本,可能会导致错误。安装时根据提示,选中以下包(*号为可选项):

  • Archive/unzip
  • *Archive/zip (only needed if you intend to build the U3 package)
  • Devel/bison
  • Devel/flex
  • *Devel/subversion (optional - see discussion about using Subversion below)
  • Interpreters/perl
  • Utils/patch
  • Web/wget

假设其安装到C:\Cygwin64。

3.1.6 安装Python

https://www.python.org/ 下载安装Python 2.7版本,假设安装到C:\Python27

3.1.7 准备编译命令行

到wireshark源码主目录建一个批处理文件,如setenv.bat,内容如下

  1. @ECHO off
  2. SET PATH=%PATH%:.
  3. SET CYGWIN_BIN=C:\cygwin64\bin
  4. SET QT5_BASE_DIR=D:\dev\qt-everywhere-opensource-src-5.3.2\qtbase
  5. SET QT5_BIN=D:\dev\qt-everywhere-opensource-src-5.3.2\qtbase\bin
  6. SET PATH=%PATH%;%CYGWIN_BIN%;%QT5_BIN%
  7. SET WIRESHARK_LIB_DIR=D:\dev\Wireshark-win64-libs-1.12
  8. SET VISUALSTUDIOVERSION=10.0
  9. SET PLATFORM=X64
  10. SET WIRESHARK_VERSION_EXTRA=-zzq-x64
  11. ECHO 设置 Visual Studio environment...
  12. CALL "C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\vcvarsall.bat" amd64
  13. title Command Prompt (MSVC++ 2010 64bit)
  14. GOTO :eof

解释一下:

  • CYGWIN_BIN: cygwin可执行文件目录
  • QT_5XXX: Qt相关目录
  • VISUALSTUDIOVERSION: 编译所用的Visual Studio版本号
  • PLATFORM:目标平台,即将wireshark编译为32位还是64位
  • WIRESHARK_VERSION_EXTRA:附加版本信息,这个字符串会出现在编译好的Wireshark的“About Wireshark”对话框内的版本信息中
  • CALL那行:表示设定Visual C++ 2010 64位编译模式环境变量

    3.1.8 修改/config.nmake文件

    打开wireshark源码主目录下的config.nmake文件,进行以下改动

  • 找到WIRESHARK_LIB_DIR=,将其设置为编译wireshark编译所依赖的第三方库文件的目录,见下文解释

  • 找到LOCAL_CFLAGS=,加入想要的编译器Flag

此文件中的设置项很多,可以根据自己的需要自行修改。

3.1.9 检查编译工具链是否就绪

打开wireshark源码目录中的setenv.bat,(打开后就不要关了,以后还要用),运行
**nmake -f Makefile.nmake verify_tools**
如下图所示:
Wireshark: 二次开发 (旧) - 图4
如果没有错误提示,证明编译所需要的所有软件和工具都安装好了。

3.1.10 安装第三方依赖库

编译wireshark需要依赖不少第三方库,如下图所示
Wireshark: 二次开发 (旧) - 图5
这些库可以用wireshark编译脚本自动下载,也可以手动下载。这些库的下载地址是:
http://anonsvn.wireshark.org/wireshark-$WIRESHARK_TARGET_PLATFORM-libs/tags/$DOWNLOAD_TAG/packages/
其中$WIRESHARK_TARGET_PLATFORM要替换成你所要编译的目标平台,如win32或win64,$DOWNLOAD_TAG要替换成一个日期字符串。比如对于我要编译1.12.0版本,这个地址是:
http://anonsvn.wireshark.org/wireshark-win64-libs/tags/2014-06-19/packages/
如果网络质量好,可以直接运行
**nmake -f Makefile.nmake setup**
来自动下载安装依赖库。

3.1.11 开始编译

先运行
**nmake -f Makefile.nmake distclean**
来清理旧文件,然后运行
**namke -f Makefile.nmake all**
开始编译。在intel i5-4590 CPU和8GB内存机器上,编译过程大约7分钟。

3.2 Mac OS X

以1.99.8为例。

  1. 下载源码并解压
  2. 进入主目录,运行./macosx-setup.sh,根据提示,安装所有所需依赖项
    如果不需要Qt支持,请注释掉macosx-setup.sh中的QT_VERSION=x.y.z这一行,且在第4步不要加入Qt相关的东西
  3. 设定3个环境变量
    export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/Users/zzq/Qt5.5.0/5.5/clang_64/lib/pkgconfig:/usr/X11/lib/pkgconfig<br />export CMAKE_PREFIX_PATH=:/Users/zzq/Qt5.5.0/5.5/clang_64/lib/cmake<br />export PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/Users/zzq/Qt5.5.0/5.5/clang_64/bin
  4. mkdir build; cd build
  5. cmake ..
  6. make –j 6 app_bundle 如果wireshark 1.12.6,为 make –j 6 all
  7. 编好的文件在build/run下面,也可以make install安装

使用cmake编译时,默认的编译选项由主目录下的CMakeList.txt中的

  1. if( NOT CMAKE_BUILD_TYPE )
  2. set( CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING
  3. "Choose the type of build, options are: None Debug Release RelWithDebInfo MinSizeRel."
  4. FORCE)
  5. endif()

设定为RelWithDebInfo,如果想要开发调试,最好设成Debug。可以在这里设置,也可以在cmake命令行中设置:cmake –DCMAKE_BUILD_TYPE=Debug.
另外,为了使Mac OSX下编译的GTK+版本的wireshark能运行,需要先启动X11(由Quartz安装,默认在Launchpad中的”其他“里),然后在shell里输入export DISPLAY=:0.0,再运行./wireshark就可以了。

4 wireshark插件

4.1 概述

wireshark提供了灵活的插件机制,使用户可以方便地扩展wireshark的功能。插件的功能主要包括,但不限于协议解析器。
可以使用Lua或C语言来编写Wireshark插件,下表对比了这两种方式,绿色背景代表占优的一方。

对比项目 C插件 Lua插件
开发难度 容易,只需要了解Lua语言
开发语言 难以掌握的C 轻巧方便的Lua
开发环境 C编译器、第三方库、复杂的环境搭建等等 文本编辑器即可
代码量
可测试性 方便
插件执行速度 非常快 较慢
可调用的已有功能代码 所有 仅一部分
部署方式 与主程序一起编译,或插件目录 插件目录

4.2 示例

假定我们要分析一个私有协议Foo,这个协议使用UDP端口9877传输数据,其报文格式如下表所示,括号内为字节数:

type (1)
1 - 初始化
2 - 终止
3 - 数据
flag (1)
…….1 正常
……1. 拥塞
…..1.. 优先
seqNo (2)
包序号
ipAddr (4)
IP地址

产生此协议报文的C++程序如下(for VC++ on Windows)

  1. #include <WinSock2.h>
  2. #include <stdio.h>
  3. #include <time.h>
  4. #pragma comment(lib, "ws2_32.lib")
  5. #define UDP_PORT_FOO 9877
  6. struct proto_foo
  7. {
  8. UINT8 type;
  9. UINT8 flags;
  10. UINT16 seqno;
  11. UINT32 ipaddr;
  12. };
  13. int main(int argc, char** argv)
  14. {
  15. int ret;
  16. SOCKET sockfd;
  17. SOCKADDR_IN addr;
  18. proto_foo data;
  19. INT16 seq = 1;
  20. WORD dwVersion = MAKEWORD(2, 2);
  21. WSAData wsaData;
  22. WSAStartup(dwVersion, &wsaData);
  23. sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  24. addr.sin_family = AF_INET;
  25. addr.sin_port = htons(UDP_PORT_FOO);
  26. if(argc < 2)
  27. {
  28. printf("will send to 220.181.57.217\n");
  29. addr.sin_addr.s_addr = inet_addr("220.181.57.217");
  30. }
  31. else
  32. addr.sin_addr.s_addr = inet_addr(argv[1]);
  33. data.ipaddr = 0x04030201;
  34. for(;;)
  35. {
  36. srand((unsigned int)time(NULL));
  37. data.type = rand() % 3 + 1;
  38. data.flags = rand() % 4 + 1;
  39. if(data.flags == 3)
  40. data.flags = 4;
  41. data.seqno = htons(seq++);
  42. ret = sendto(sockfd, (const char*)&data, sizeof(proto_foo), 0,
  43. (SOCKADDR*)&addr, sizeof(addr));
  44. if(SOCKET_ERROR == ret)
  45. {
  46. printf("sendto error\n");
  47. break;
  48. }
  49. Sleep(1000);
  50. }
  51. closesocket(sockfd);
  52. WSACleanup();
  53. return 0;
  54. }

编译并运行以上程序,并用wireshark抓包保存,之后要使用这个pcap文件来测试编写的foo解析插件。
下面将要两种方式实现foo协议的解析插件,结合这个实例介绍这两种插件的开发方法。

5 Lua插件

5.1 Wireshark对Lua的支持

本节相关内容可参考Wireshark开发指南第10章”Lua Support in Wireshark”。

Wireshark集成了Lua解释器,以支持Lua脚本(自己编译的话根据编译配置决定是否支持Lua)。
启动wireshark,依次点击“Help”,”About Wireshark“菜单,在打开的对话框中的”Wireshark”标签页上观察版本信息,如果如下图一样显示With Lua,说明此版本支持Lua插件。
Wireshark: 二次开发 (旧) - 图6
然后打开wireshark主目录下的init.lua文件,确保disable_lua的值为false,即开启了lua:
Wireshark: 二次开发 (旧) - 图7
在Wireshark中,可以使用Lua编写以下几种插件:

  • Dissectors

协议解析器,用于解析报文

  • Post-dissectors

后置解析器,在其他解析器之后被调用

  • Listeners

监听器,用来收集解析后的信息

还需要注意:

  1. wireshark启动时,会调用下图目录中的init.lua,顺序是先调用global目录的,再调用personal目录的;

Wireshark: 二次开发 (旧) - 图8

  1. 通过命令行参数:-X lua_script:my.lua 传入的my.lua将会在Init.lua之后被调用
  2. 所有的Lua脚本会在解析器注册过程的最后被调用,而这一过程是在wireshark启动时就发生的,早于报文被读取的时刻。

    5.2 Lua插件API简介

    本节相关内容可参考Wireshark开发指南第11节”Wireshark’s Lua API Reference”。

    Wireshark提供了丰富的Lua API供开发者使用,这里只介绍下文需要用到的一些。

    5.2.1 Proto

    表示一个新的Protocol,在Wireshark中Protocol对象有很多用处,解析器是其中主要的一个。主要接口有:
接口 说明
proto:__call(name,desc) 创建Proto对象。name和desc分别是对象的名称和描述,前者可用于过滤器等
proto.name get名称
proto.fields get/set字段
proto.prefs get配置项
proto.init 初始化,无参数
proto.dissector 解析函数,3个参数tvb,pinfo,tree,分别是报文内容,报文信息和解析树结构
proto:register_heuristic(listname, func) 为Proto注册一个启发式解析器,被调用时,参数func将被传入与dissector方法相同的3个参数

5.2.2 ProtoField

表示协议字段,一般用于解析字段后往解析树上添加节点。根据字段类型不同,其接口可以分为两大类。
整型:
ProtoField.{type} (abbr, [name], [desc],[base], [valuestring], [mask])
type包括:uint8, uint16, uint24, uint32, uint64, framenum
其他类型
ProtoField.{type} (abbr, [name], [desc])
type包括:float, double, string, stringz, bytes, bool, ipv4, ipv6, ether,oid, guid
这些接口都会返回一个新的字段对象。方括号内是可选字段,花括号内是可替换的类型字段。

5.2.3 Tvb

Tvb(Testy Virtual Buffer)表示报文缓存,也就是实际的报文数据,可以通过下面介绍的TvbRange从报文数据中解出信息。主要接口有:

接口 说明
tvb:__tostring() 将报文数据转化为字符串,可用于调试
tvb:reported_len() get tvb的(not captured)长度
tvb:len() get tvb的(captured)长度
tvb:reported_length_remaining() 获取当前tvb的剩余长度,如果偏移值大于报文长度,则返回-1
tvb:offset() 返回原始偏移

5.2.4 TvbRange

表示Tvb的可用范围,常用来从Tvb中解出信息。主要接口有

接口 说明
tvb:range([offset], [length]) 从tvb创建TvbRange,可选参数分别是偏移和长度,默认值分别是0和总长度
tvbrange:{type}() 将tvbrange所表示范围内的数据转换成type类型的值,type包括但不限于:uint,uint64,int,int64,float,ipv4,ether,nstime,string,ustring,bytes,bitfield等,其中某些类型的方法可以带一些参数

5.2.5 Pinfo

报文信息(packet information)。主要接口有:

接口 说明
pinfo.len pinfo.caplen get报文长度
pinfo.abs_ts get报文捕获时间
pinfo.number get报文编号
pinfo.src
pinfo.dst
get/set报文的源地址、目的地址
pinfo.columns
pinfo.cols
get报文列表列(界面)

取得报文列表列后,就可以设置该列的文本,比如
pinfo.cols.info = “hello world”
将Info列的文本设为hello world。

5.2.6 TreeItem

表示报文解析树中的一个树节点。主要接口有:

接口 说明
treeitem:add([protofield], [tvbrange], [value], [label]) 向当前树节点添加一个子节点
treeitem:set_text(text) 设置当前树节点的文本
treeitem:prepend_text(text) 在当前树节点文本的前面加上text
treeitem:append_text(text) 在当前树节点文本的后面加上text

5.2.7 DissectorTable

表示一个具体协议的解析表,比如,协议TCP的解析表”tcp.port”包括http,smtp,ftp等。可以依次点击wireshark菜单“Internals”、“Dissector tables”,来查看当前的所有解析表。tcp.port解析表在“Integer tables”选项卡中,顾名思义,它是通过类型为整型的tcp端口号来识别下游协议的:
Wireshark: 二次开发 (旧) - 图9
DissectorTable的主要接口有:

接口 说明
DissectorTable.get(name) get名为name的解析表的引用
dissectortable:add(pattern, dissector) 将Proto或Dissector对象添加到解析表,即注册。pattern可以是整型值,整型值范围或字符串,这取决于当前解析表的类型
dissectortable:remove(pattern, dissector) 将满足pattern的一个或一组Proto、Dissector对象从解析表中删除

5.3 Dissector

首先新建一个文件,命名为foo.lua,注意此文件的编码方式不能是带BOM的UTF8,否则wireshark加载它时会出错(不识别BOM):

  1. -- @brief Foo Protocol dissector plugin
  2. -- @author zzqcn
  3. -- @date 2015.08.12
  4. -- create a new dissector
  5. local NAME = "foo"
  6. local PORT = 9877
  7. local foo = Proto(NAME, "Foo Protocol")
  8. -- dissect packet
  9. function foo.dissector (tvb, pinfo, tree)
  10. end
  11. -- register this dissector
  12. DissectorTable.get("udp.port"):add(PORT, foo)

这是一个lua解析器的骨架:创建解析器对象、解析器函数、将解析器注册到wireshark解析表。
写完之后,将foo.lua拷贝到plugins/<版本号>目录即可,然后用文件打开之前抓的foo协议的Pcap文件foo.pcap。
Wireshark: 二次开发 (旧) - 图10
是的,wireshark没有提示错误,然而什么变化也没有:当然,因为我们没有编写实际的解析代码。
依次打开”Internals”、”Dissector tables“菜单,选中”Interger tables”标签页,下拉滚动条,找到“UDP port“树节点,展开,再往下来,会发现FOO协议赫然在列,证明foo插件确实被正确加载了:
Wireshark: 二次开发 (旧) - 图11
下面需要写一些具体的代码,首先是定义foo协议的各个字段:

  1. -- create fields of foo
  2. local fields = foo.fields
  3. fields.type = ProtoField.uint8 (NAME .. ".type", "Type")
  4. fields.flags = ProtoField.uint8 (NAME .. ".flags", "Flags")
  5. fields.seqno = ProtoField.uint16(NAME .. ".seqno", "Seq No.")
  6. fields.ipaddr = ProtoField.ipv4(NAME .. ".ipaddr", "IPv4 Address")

根据foo协议字段类型的不同,分别调用ProtoField的不同方法创建它们,其中第一个参数是字段的缩写,第2个参数是字段的全名,另外还有一些可选参数表示进制,掩码之类,这里略去。
然后编写具体的解析函数:

  1. -- dissect packet
  2. function foo.dissector (tvb, pinfo, tree)
  3. local subtree = tree:add(foo, tvb())
  4. local offset = 0
  5. -- show protocol name in protocol column
  6. pinfo.cols.protocol = foo.name
  7. -- dissect field one by one, and add to protocol tree
  8. local type = tvb(offset, 1)
  9. subtree:add(fields.type, type)
  10. subtree:append_text(", type: " .. type:uint())
  11. offset = offset + 1
  12. subtree:add(fields.flags, tvb(offset, 1))
  13. offset = offset + 1
  14. subtree:add(fields.seqno, tvb(offset, 2))
  15. offset = offset + 2
  16. subtree:add(fields.ipaddr, tvb(offset, 4))
  17. end

wireshark约定解析器函数接口有3个参数,第一个是报文数据buffer tvb,第2个是报文信息结构pinfo,第3个是协议解析树tree。
subtree = tree:add(foo, tvb())为foo协议往协议解析树上添加了一个新节点subtree;
pinfo.cols.protocol = foo.name把wireshark报文列表上的”Protocol“列的文本置为foo协议名称”Foo”;
接下来,根据Foo协议的规范依次解析各字段,并把它们的信息加入到协议解析树。
编写完成后把新的foo.lua拷贝到插件目录,重启wireshark打开foo.pcap,显示效果如下:
Wireshark: 二次开发 (旧) - 图12
也可以对foo协议应用显示过滤器:
Wireshark: 二次开发 (旧) - 图13

5.4 Post-dissector

post-dissector和dissector不同,它会在所有dissectors都执行过后再被执行,这也就post前缀的由来。post-dissector的构建方式和dissector差不多,主要一个区别是注册的方式,post-dissector调用的是register_postdissetor接口。下面给出两个示例。

5.4.1 最简单的post-dissector

这个示例主要是演示post-dissector脚本的骨架,它的功能是在packet list的所有info列加上了xxx公司的网址。

  1. -- @brief A simple post-dissector, just append string to info column
  2. -- @author zzqcn
  3. -- @date 2015.08.13
  4. local myproto = Proto("xxx","Dummy proto to edit info column")
  5. -- the dissector function callback
  6. function myproto.dissector(tvb,pinfo,tree)
  7. pinfo.cols.info:append(" xxx.com.cn")
  8. end
  9. -- register our new dummy protocol for post-dissection
  10. register_postdissector(myproto)


此插件运行效果如下图:
image014.jpg

5.4.2 识别滑动特征

这个示例简单地演示了如何使用post-dissector来识别滑动特征。例子中,通过识别tcp载荷中是否含有字符串”weibo“来判断报文是否为weibo业务,如果是,则在packet list的protocol列标出,并在proto tree添加树节点,给出滑动特征在TCP载荷中的位置。

  1. -- @brief A post-dissector, to indentify pattern in payload
  2. -- @author zzqcn
  3. -- @date 2015.08.26
  4. local weibo = Proto("weibo", "Weibo Service")
  5. local function get_payload_offset(data, proto_type)
  6. local mac_len = 14;
  7. local total_len;
  8. local ip_len = (data(14, 1):uint() - 64) * 4;
  9. if (proto_type == 0x06) then
  10. local tcp_len = (data(46, 1):uint()/16) * 4;
  11. total_len = mac_len + ip_len + tcp_len;
  12. elseif (proto_type == 0x11) then
  13. local udp_len = 8;
  14. total_len = mac_len + ip_len + udp_len;
  15. end
  16. return total_len
  17. end
  18. -- the dissector function callback
  19. function weibo.dissector(tvb, pinfo, tree)
  20. local proto_type = tvb(23, 1):uint();
  21. if(proto_type ~= 0x06) then
  22. return
  23. end
  24. local offset = get_payload_offset(tvb, proto_type);
  25. local data = tvb(offset):string();
  26. local i, j = string.find(data, "weibo") <----------------------
  27. if(i) then
  28. pinfo.cols.protocol = weibo.name
  29. local subtree = tree:add(weibo, tvb(offset+i-1))
  30. debug(i)
  31. subtree:append_text(", ptn_pos: " .. i .. "-" .. j)
  32. end
  33. end
  34. -- register our plugin for post-dissection
  35. register_postdissector(weibo)

运行效果如下图。
Wireshark: 二次开发 (旧) - 图15

5.5 Listener

Listner用来设置一个监听条件,当这个条件发生时,执行事先定义的动作。
实现一个Listner插件至少要实现以下接口:

  • 创建Listener
    listener = Listener.new([tap], [filter]),其中tap, filter分别是tap和过滤条件
  • listener.packet在条件命中时调用
  • listener.draw在每次需要重绘GUI时调用
  • listener.reset清理时调用

以上实现代码一般都包在一个封装函数中,最后把这个函数注册到GUI菜单:
register_menu(name, action, [group])

下面的示例代码对pcap文件中的http报文进行了简单的计数统计:

  1. -- @brief a simple Listener plugin
  2. -- @author zzqcn
  3. -- @date 2015.08.13
  4. local function zzq_listener()
  5. local pkts = 0
  6. local win = TextWindow.new("zzq Listener")
  7. local tap = Listener.new(nil, "http")
  8. win:set_atclose(function() tap:remove() end)
  9. function tap.packet (pinfo, tvb, tapinfo)
  10. pkts = pkts + 1
  11. end
  12. function tap.draw()
  13. win:set("http pkts: " .. pkts)
  14. end
  15. function tap.reset()
  16. pkts = 0
  17. end
  18. -- Rescan all packets and just run taps - dont reconstruct the display.
  19. retap_packets()
  20. end
  21. register_menu("xxx/zzq Listener", zzq_listener, MENU_STAT_GENERIC)

要查看运行结果,要选择”Statistics“菜单中的xxx/zzq Listerner子菜单来触发。此插件的运行效果如下图:
Wireshark: 二次开发 (旧) - 图16

6 C插件

6.1 Wireshark对C插件的支持

每个解析器解码自己的协议部分, 然后把封装协议的解码传递给后续协议。
因此它可能总是从一个Frame解析器开始, Frame解析器解析捕获文件自己的数据包细节(如:时间戳), 将数据交给一个解码Ethernet头部的Ethernet frame解析器, 然后将载荷交给下一个解析器(如:IP), 如此等等. 在每一步, 数据包的细节会被解码并显示.
可以用两种可能的方式实现协议解析. 一是写一个解析器模块, 编译到主程序中, 这意味着它将永远是可用的. 另一种方式是实现一个插件(共享库/DLL), 它注册自身用于处理解析。
插件形式和内置形式的解析器之间的差别很小. 在Windows平台, 通过列于libwireshark.def中的函数, 我们可以访问有限的函数, 但它们几乎已经够用了.
比较大的好处是插件解析器的构建周期要远小于内置. 因此以插件开始会使最初的开发工作变得简单, 而最终代码的布署会和内置解析器一样。
另见 README.developer 文件doc/README.developer包含更多有关实现解析器(而且在某些情况下, 比本文档要新一些)的信息.

6.2 构建解析器

首先需要决定解析器是要以built-in方式,还是以plugin方式实现。plugin方式实现比较容易上手。
解析器初始化:

  1. #include "config.h"
  2. #include <epan/packet.h>
  3. #define FOO_PORT 9877
  4. static int proto_foo = -1;
  5. void
  6. proto_register_foo(void)
  7. {
  8. proto_foo = proto_register_protocol (
  9. "FOO Protocol", /* name */
  10. "FOO", /* short name */
  11. "foo" /* abbrev */
  12. );
  13. }

首先include一些必需的头文件。proto_foo用来记录我们的协议,当将此解析器注册到主程序时,它的值将会更新。把所有非外部使用的变量和函数声明为static是一个好的编程实践,可以避免名字空间污染。一般情况下这不是问题,除非我们的解析器非常大,分成了多个文件。
我们#define了协议的UDP端口FOO_PORT。
现在我们已经有了与主程序交互所需的基本东西了。接下来实现2个解析器构建函数(dissector setup functions)。
首先调用proto_register_protocol()函数来注册协议。可以给它3个名字用来将来在不同的地方显示。比如full和short name用于“Preferences”和“Enabled protocols”对话框。abbrev name用于显示过滤器。
接下来我们需要handoff例程。

  1. void
  2. proto_reg_handoff_foo(void)
  3. {
  4. static dissector_handle_t foo_handle;
  5. foo_handle = create_dissector_handle(dissect_foo, proto_foo);
  6. dissector_add_uint("udp.port", FOO_PORT, foo_handle);
  7. }

首先创建一个dissector handle,它和foo协议及执行实际解析工作的函数关联。接下来将此handle与UDP端口号关联,以便主程序在看到此端口上的UDP数据时调用我们的解析器。
标准wireshark解析器习惯是把proto_register_foo()和proto_reg_handoff_foo()做为解析器代码的最后2个函数。
最后我们来编写一些解析器代码。目前将它做为基本的占位符。

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

此函数用于解析交给它的packets。packet数据放在名为tvb的特殊缓存中。对此随着我们对协议细节了解的深入将会变得非常熟悉。packet_info结构包含有关协议的一般数据,我们应该在此更新信息。tree参数是细节解析发生的地方。
现在我们进行最小化的实现。第1行我们设置我们协议的文本,以示用户可以看到协议被识别了。另外唯一做的事情是清除INFO列中的所有数据,如果它正在被显示的话。
此时,我们已经准备好基本的解析器,可以进行编译和安装了。它什么也不做,除了识别协议并标识它。
为了编译此解析器并创建插件,除了packet-foo.c中的源代码,还有一堆必需的支持文件,它们是:

  • Makefile.am - This is the UNIX/Linux makefile template
  • CMakeLists.txt - 使用cmake编译时所需的脚本
  • Makefile.common - This contains the file names of this plugin
  • Makefile.nmake - This contains the Wireshark plugin makefile for Windows
  • moduleinfo.h - This contains plugin version info
  • moduleinfo.nmake - This contains DLL version info for Windows
  • packet-foo.h, packet-foo.c - This is your dissector source
  • plugin.rc.in - This contains the DLL resource template for Windows

这些文件的例子可以在plugins/gryphon中找到,把所有与gryphon相关的东西都改成foo即可。plugin.rc.in不需要改动,windows编译不需要的文件也不需要改动。
把以上文件准备好、修改好之后,cmd进入plugins/foo目录,运行
**nmake -f Makefile.nmake xxx**
来进行编译,就像编译wireshark源码一样。编译好之后生成foo.dll,将它拷贝到编译好的wireshark的plugins目录(可能会有中间目录,视情况)。
还可以修改plugins目录下面的Makefile.nmake文件,在PLUGIN_LIST中加入新插件的目录名,这样下次编译wireshark时会一起编译你的插件。

Wireshark: 二次开发 (旧) - 图17
如果是在Mac OSX系统中编译插件(CMake方式),需要修改主目录下的CMakeLists.txt,搜索plugin字符串,找到set(PLUGIN_SRC_DIRS下面的行,在路径中加入plugins/foo(plugins目录的Makefile.am文件可能不需要修改,其中SUBDIRS项中列出了各插件的源码目录) ;然后如同3.2节所述,进入build目录,执行**make –j 6 plugins**,即可编译插件们。

然后启动wireshark,打开Dissector Tables窗口,可以查到以下信息,说明wireshark已经正确加载我们的插件。
Wireshark: 二次开发 (旧) - 图18
打开foo.pcap,效果如下图所示,此时没有协议解析树,只在报文列表中添加了协议名:
Wireshark: 二次开发 (旧) - 图19

6.3 完善解析器

接下来可以做一些复杂一点的解析工作。最简单的事情是对载荷进行标记。
首先创建一个subtree用来放解析结果。这有助于在detailed display中更佳显示。对解析器的调用有2种情况。一种情况用于获取packet的摘要,另一种情况用于解析packet的细节。这两种情况由tree指针的不同来区别。如果tree指针为NULL,用于获取简略信息。如果是非NULL,则需要解析协议的各个细部。记住这些后,让我们来增强我们的解析器。

  1. static void
  2. dissect_foo(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree)
  3. {
  4. col_set_str(pinfo->cinfo, COL_PROTOCOL, "FOO");
  5. /* Clear out stuff in the info column */
  6. col_clear(pinfo->cinfo,COL_INFO);
  7. if (tree) { /* we are being asked for details */
  8. proto_item *ti = NULL;
  9. ti = proto_tree_add_item(tree, proto_foo, tvb, 0, -1, ENC_NA);
  10. }
  11. }

这里所做的是把一个subtree加入到解析中。此subtree会保存此协议的所有细节,且不会在不需要时弄乱显示。
我们还可以标记被此协议所消费的数据区域。在目前的情况下,这统治是传递过来的所有数据,因为我们假定此协议不再封装其他协议。因此,我们用proto_tree_add_item()往tree里添加新的节点,标识它的协议名,用tvb缓冲区做为数据,并消费此数据的0到最后1个字节(-1表示结束)。ENC_NA(not applicable)是编码参数。
在这些改变之后,在detailed display中就会有此协议的标识,且选中它将会高亮此packet的剩余内容。如下图所示:
Wireshark: 二次开发 (旧) - 图20
现在,让我们进行下一步,添加一些协议解析。这一步我们需要创建2个表来帮助解析。这需要在proto_register_foo()函数中添加一些代码。
在proto_register_foo()的前面添加了2个static数组。这些数组在proto_register_protocol()调用之后被注册。

  1. void
  2. proto_register_foo(void)
  3. {
  4. static hf_register_info hf[] = {
  5. { &hf_foo_pdu_type,
  6. { "FOO PDU Type", "foo.type",
  7. FT_UINT8, BASE_DEC,
  8. NULL, 0x0,
  9. NULL, HFILL }
  10. }
  11. };
  12. /* Setup protocol subtree array */
  13. static gint *ett[] = {
  14. &ett_foo
  15. };
  16. proto_foo = proto_register_protocol (
  17. "FOO Protocol", /* name */
  18. "FOO", /* short name */
  19. "foo" /* abbrev */
  20. );
  21. proto_register_field_array(proto_foo, hf, array_length(hf));
  22. proto_register_subtree_array(ett, array_length(ett));
  23. }

变量hf_foo_pdu_type和ett_foo也需要在此文件的前面声明。

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

现在我们可以用一些细节来增加协议的显示。

  1. if (tree) { /* we are being asked for details */
  2. proto_item *ti = NULL;
  3. proto_tree *foo_tree = NULL;
  4. ti = proto_tree_add_item(tree, proto_foo, tvb, 0, -1, ENC_NA);
  5. foo_tree = proto_item_add_subtree(ti, ett_foo);
  6. proto_tree_add_item(foo_tree, hf_foo_pdu_type, tvb, 0, 1, ENC_BIG_ENDIAN);
  7. }

现在解析开始看起来更加有趣了。我们开始破解此协议的第1个比特。packet起始处的一个字节数据定义了foo协议的packet type。
proto_item_add_subtree()调用往协议树中增加了一个子节点。此节点的展开是由ett_foo变量控制的。它会记住节点是否应该展开,在你在packet中移动的时候。所有后续的解析会添加到此树中,就像在接下来的调用中看到的那样。proto_tree_add_item向foo_tree添加了新项,并用hf_foo_pdu_type来控制此项的格式。pdu type是1个字节的数据,从0开始。我们假定它是网络字节序(也叫big endian),因此用ENC_BIG_ENDIAN。对于1个字节的数来说,没用字节序之说,但这是好的编程实践。
我们来看static数组中的定义细节:

  • hf_foo_pdu_type - 此节点的索引
  • FOO PDU Type - 此项的标识
  • foo.type - 过滤用的字符串。它使我们可以在过滤器框中输入foo.type=1的语句
  • FT_UINT8 - 指出此项是一个8bit的无符号整数。
  • BASE_DEC - 对于整型来说,它令其打印为一个10进制数。还可以是16进制(BASE_HEX)或8进制(BASE_OCT)。

我们目前忽略结构中的其余成员。
如果此时编译并安装此插件,我们会看到它开始显示一些看起来有用的东西。
现在我们来完成这个简单协议的解析。我们需要添加更多的变量在hf数组中,以及更多的函数调用。

  1. ...
  2. static int hf_foo_flags = -1;
  3. static int hf_foo_sequenceno = -1;
  4. static int hf_foo_initialip = -1;
  5. ...
  6. static void
  7. dissect_foo(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree)
  8. {
  9. gint offset = 0;
  10. ...
  11. if (tree) { /* we are being asked for details */
  12. proto_item *ti = NULL;
  13. proto_tree *foo_tree = NULL;
  14. ti = proto_tree_add_item(tree, proto_foo, tvb, 0, -1, ENC_NA);
  15. foo_tree = proto_item_add_subtree(ti, ett_foo);
  16. proto_tree_add_item(foo_tree, hf_foo_pdu_type, tvb, offset, 1, ENC_BIG_ENDIAN);
  17. offset += 1;
  18. proto_tree_add_item(foo_tree, hf_foo_flags, tvb, offset, 1, ENC_BIG_ENDIAN);
  19. offset += 1;
  20. proto_tree_add_item(foo_tree, hf_foo_sequenceno, tvb, offset, 2, ENC_BIG_ENDIAN);
  21. offset += 2;
  22. proto_tree_add_item(foo_tree, hf_foo_initialip, tvb, offset, 4, ENC_BIG_ENDIAN);
  23. offset += 4;
  24. }
  25. ...
  26. }
  27. void
  28. proto_register_foo(void) {
  29. ...
  30. ...
  31. { &hf_foo_flags,
  32. { "FOO PDU Flags", "foo.flags",
  33. FT_UINT8, BASE_HEX,
  34. NULL, 0x0,
  35. NULL, HFILL }
  36. },
  37. { &hf_foo_sequenceno,
  38. { "FOO PDU Sequence Number", "foo.seqn",
  39. FT_UINT16, BASE_DEC,
  40. NULL, 0x0,
  41. NULL, HFILL }
  42. },
  43. { &hf_foo_initialip,
  44. { "FOO PDU Initial IP", "foo.initialip",
  45. FT_IPv4, BASE_NONE,
  46. NULL, 0x0,
  47. NULL, HFILL }
  48. },
  49. ...
  50. ...
  51. }
  52. ...

此时的解析效果如下图所示:
Wireshark: 二次开发 (旧) - 图21
再修改一些细节,比如flag的位显示方式、foo协议树子节点字符串,packet列表中Info列的显示等等,最后效果如下:
Wireshark: 二次开发 (旧) - 图22
最终代码:

  1. /* packet-foo.c
  2. * Routines for Foo protocol packet disassembly
  3. * By zzqcn
  4. */
  5. #include "config.h"
  6. #include <epan/packet.h>
  7. #include <epan/prefs.h>
  8. //#include <epan/dissectors/packet-tcp.h>
  9. #include "packet-foo.h"
  10. #define FOO_PORT 9877
  11. #define FOO_NAME "Foo Protocol"
  12. #define FOO_SHORT_NAME "Foo"
  13. #define FOO_ABBREV "foo"
  14. static int proto_foo = -1;
  15. static int hf_foo_pdu_type = -1;
  16. static int hf_foo_flags = -1;
  17. static int hf_foo_seqno = -1;
  18. static int hf_foo_ip = -1;
  19. static gint ett_foo = -1;
  20. static const value_string pkt_type_names[] =
  21. {
  22. {1, "Initilize"},
  23. {2, "Terminate"},
  24. {3, "Data"},
  25. {0, NULL}
  26. };
  27. #define FOO_START_FLAG 0x01
  28. #define FOO_END_FLAG 0x02
  29. #define FOO_PRIOR_FLAG 0x04
  30. static int hf_foo_start_flag = -1;
  31. static int hf_foo_end_flag = -1;
  32. static int hf_foo_prior_flag = -1;
  33. void proto_register_foo(void);
  34. void proto_reg_handoff_foo(void);
  35. static int dissect_foo(tvbuff_t*, packet_info*, proto_tree*, void*);
  36. void
  37. proto_register_foo(void)
  38. {
  39. static hf_register_info hf[] =
  40. {
  41. {
  42. &hf_foo_pdu_type,
  43. {
  44. "Type", "foo.type",
  45. FT_UINT8, BASE_DEC,
  46. VALS(pkt_type_names), 0x0,
  47. NULL, HFILL
  48. }
  49. },
  50. {
  51. &hf_foo_flags,
  52. {
  53. "Flags", "foo.flags",
  54. FT_UINT8, BASE_HEX, NULL, 0x0, NULL, HFILL
  55. }
  56. },
  57. {
  58. &hf_foo_start_flag,
  59. {
  60. "Start Flag", "foo.flags.start",
  61. FT_BOOLEAN, 8,
  62. NULL, FOO_START_FLAG, NULL, HFILL
  63. }
  64. },
  65. {
  66. &hf_foo_end_flag,
  67. {
  68. "End Flag", "foo.flags.end",
  69. FT_BOOLEAN, 8,
  70. NULL, FOO_END_FLAG, NULL, HFILL
  71. }
  72. },
  73. {
  74. &hf_foo_prior_flag,
  75. {
  76. "Priority Flag", "foo.flags.prior",
  77. FT_BOOLEAN, 8,
  78. NULL, FOO_PRIOR_FLAG, NULL, HFILL
  79. }
  80. },
  81. {
  82. &hf_foo_seqno,
  83. {
  84. "Sequence Number", "foo.seq",
  85. FT_UINT16, BASE_DEC,
  86. NULL, 0x0, NULL, HFILL
  87. }
  88. },
  89. {
  90. &hf_foo_ip,
  91. {
  92. "IP Address", "foo.ip",
  93. FT_IPv4, BASE_NONE,
  94. NULL, 0x0, NULL, HFILL
  95. }
  96. }
  97. };
  98. static gint *ett[] = { &ett_foo };
  99. proto_foo = proto_register_protocol (
  100. FOO_NAME,
  101. FOO_SHORT_NAME,
  102. FOO_ABBREV);
  103. proto_register_field_array(proto_foo, hf, array_length(hf));
  104. proto_register_subtree_array(ett, array_length(ett));
  105. }
  106. static int
  107. dissect_foo(tvbuff_t *tvb, packet_info *pinfo, proto_tree *tree, void* data)
  108. {
  109. guint8 packet_type = tvb_get_guint8(tvb, 0);
  110. col_set_str(pinfo->cinfo, COL_PROTOCOL, "FOO");
  111. /* Clear out stuff in the info column */
  112. col_clear(pinfo->cinfo,COL_INFO);
  113. col_add_fstr(pinfo->cinfo, COL_INFO, "Type %s",
  114. val_to_str(packet_type, pkt_type_names, "Unknown (0x%02x)"));
  115. /* proto details display */
  116. if(tree)
  117. {
  118. proto_item* ti = NULL;
  119. proto_tree* foo_tree = NULL;
  120. gint offset = 0;
  121. ti = proto_tree_add_item(tree, proto_foo, tvb, 0, -1, ENC_NA);
  122. proto_item_append_text(ti, ", Type %s",
  123. val_to_str(packet_type, pkt_type_names, "Unknown (0x%02x)"));
  124. foo_tree = proto_item_add_subtree(ti, ett_foo);
  125. proto_tree_add_item(foo_tree, hf_foo_pdu_type, tvb, offset, 1, ENC_BIG_ENDIAN);
  126. offset += 1;
  127. proto_tree_add_item(foo_tree, hf_foo_flags, tvb, offset, 1, ENC_BIG_ENDIAN);
  128. proto_tree_add_item(foo_tree, hf_foo_start_flag, tvb, offset, 1, ENC_BIG_ENDIAN);
  129. proto_tree_add_item(foo_tree, hf_foo_end_flag, tvb, offset, 1, ENC_BIG_ENDIAN);
  130. proto_tree_add_item(foo_tree, hf_foo_prior_flag, tvb, offset, 1, ENC_BIG_ENDIAN);
  131. offset += 1;
  132. proto_tree_add_item(foo_tree, hf_foo_seqno, tvb, offset, 2, ENC_BIG_ENDIAN);
  133. offset += 2;
  134. proto_tree_add_item(foo_tree, hf_foo_ip, tvb, offset, 4, ENC_BIG_ENDIAN);
  135. offset += 4;
  136. }
  137. return tvb_reported_length(tvb);
  138. }
  139. void
  140. proto_reg_handoff_foo(void)
  141. {
  142. static dissector_handle_t foo_handle;
  143. foo_handle = new_create_dissector_handle(dissect_foo, proto_foo);
  144. dissector_add_uint("udp.port", FOO_PORT, foo_handle);
  145. }

7 wireshark协议解析库的应用

wireshark会将epan目录下的代码编译成一个动态库libwireshark.dll/so/dylib,其中封装了所有内置解析器的解析接口、插件式解析器的hook,以及完整的报文解析api。第三方程序可以调用此动态库,在不运行wireshark进程的情况下使用其报文解析功能。
然而,这种第三方调用并不被wireshark所明确支持,甚至可以说是不推荐的,可以看有人在stackoverflow上对于想通过编程调用libwireshark的提问,wireshark原作者参与了问题回答:http://stackoverflow.com/questions/10308127/using-libwireshark-to-get-wireshark-functionality-programatically,以下是一部分内容:
libwireshark is not intended to be used outside of Wireshark itself, and trying to do so will leave you on your own for trying to figure out what is going wrong. libwireshark actually part of the packet analyzing portion of Wireshark (called epan for Ethereal packet analyzer), which you can see in the Developer’s Guide is not all of Wireshark. What libwireshark actually provides is the main interface for all of the built-in protocol dissectors, hooks for the plugin dissectors, and the complete packet dissection API. It relies on the machinery set up by the rest of Wireshark for things that are not directly packet dissection tools, but enable the dissectors to do their work (e.g. allocate a deallocate memory chunks, handle compressed or encrypted data, etc).
Write a dissector in stead.
我觉得需要注意几点:

  • 必须通过查看wireshark源码或者进行调试,来了解需要调用libwireshark动态库提供的哪些api,以及调用的顺序和参数等;
  • 不同的wireshark版本,libwireshark动态库提供的api有可能不同,有可能以前有的api新版本没有了,也有可能参数变了;
  • 调用libwireshark中的api时,不要认为只是简直地调用了一个功能函数,其实内部有可能做了很多事情,比如创建或写入了一个很大的哈希表之类;
  • 不适合在需要长时间运行的程序中使用libwireshark,因为其内部复杂的数据结构会不断消耗内存,直到内存耗尽,程序崩溃。(参考wireshark wiki-KnownBugs/OutOfMemory)

7.1 调用协议解析库

7.1.1 涉及到的数据结构与函数

首先我们得知道需要调用哪些函数。通过调试自己编译的wireshark,我发现如果要实现简单的协议解析,主要只需要以下几个函数(源码位置均相对于wireshark源码主目录而言):

函 数 功 能       源码位置
epan_register_plugin_types 初始化libwireshark的所有插件类型,必须在epan_init之前被调用 Epan/epan.h
epan_init 初始化协议解析库 同上
epan_cleanup 清理协议解析库 同上
epan_new 创建新的epan句柄,每次打开新文件执行,关闭文件时销毁 同上
epan_free 销毁epan句柄 同上
epan_dissect_new
epan_dissect_init
创建或初始化协议解析数据结构edt。new是从堆中创建,init是初始化栈上变量 同上
epan_dissect_run 执行协议解析 同上
epan_dissect_free
epan_dissect_cleanup
销毁或清理协议解析数据结构edt 同上
prefs_reset 重置偏好设置 epan/prefs.h
init_dissection 初始化数据包级协议解析 epan/packet.h
cleanup_dissection 清理数据包级协议解析 同上

除此之外,还需要导出一些辅助的函数,如proto_item_fill_label等等。
不仅如此,还需要熟悉协议解析过程所涉及到的一些数据结构,主要有:

数据结构 功 能       源码位置
epan_dissect_t   协议解析信息,保存协议数据及协议解析树 epan/epan.h
epan/epan_dissect.h
field_info 协议字段信息 epan/proto.h
header_field_info 协议首部字段信息 同上
proto_tree/proto_node 协议树 同上
frame_data 单帧(数据包)信息 epan/frame_data.h
wtap_pkthdr wtap首部 wiretap/wtap.h
tvbuff_t 数据缓存 -

以上就是一些主要的函数及数据结构。实际的协议解析过程中,可能会涉及到更多的函数及数据结构,这里就不多说了,具体可以查看wireshark源码。如果对于某些函数,或者解析过程有不了解的,也可以自己编译wireshark,然后调试它。

7.1.2 代码实现

首先是包含必需的头文件

  1. #if WIN32
  2. #define WS_MSVC_NORETURN __declspec(noreturn)
  3. #else
  4. #define WS_MSVC_NORETURN
  5. #endif
  6. #include <cstdint>
  7. #include "glib.h"
  8. #include "wsutil/report_err.h"
  9. #include "wsutil/privileges.h"
  10. #include "epan/prefs.h"
  11. #include "epan/epan.h"
  12. #include "epan/proto.h"
  13. #include "epan/epan_dissect.h"
  14. #include "epan/frame_data.h"
  15. #include "epan/packet.h"
  16. #include "epan/dfilter/dfilter.h"
  17. #include "wiretap/wtap.h"
  18. #include "register.h"

其中WS_MSVC_NORETURN宏在windows编译时需要定义一下,它和wireshark在windows平台的符号导出相关。如果是使用VC++开发,需要在项目的“附加包含目录”里添加以上头文件的路径:
Wireshark: 二次开发 (旧) - 图23

接下来声明几个函数,分别是libwireshark初始化、反初始化和解析报文。

  1. typedef void(*wsTreeFunc) (proto_tree*);
  2. int wsInit();
  3. int wsFini();
  4. int wsDissect(const uint8_t* pkt, uint32_t len, wsTreeFunc func);
  5. void wsPrintTree(proto_tree* tree);

wsDissect函数的第3个参数是一个回调函数指针,用于在解析出proto_tree用该函数进行访问,它的原型是wsTreeFunc。wsPrintTree是一个默认的wsTreeFunc,它简单地把proto_tree的内容格式化打印到标准输出上。
在wireshark高版本上,解析时需要一个epan_t的结构,它的生命期就是一个pcap文件的生命期,因此需要在实现文件中定义一个全局变量:
epan_t* g_epan = 0;
wsInit的实现如下

  1. int wsInit()
  2. {
  3. init_process_policies();
  4. try
  5. {
  6. init_report_err(kg_report_failure,
  7. kg_report_open_failure,
  8. kg_report_read_failure,
  9. kg_report_write_failure);
  10. epan_register_plugin_types();
  11. epan_init(register_all_protocols,
  12. register_all_protocol_handoffs,
  13. kg_register_callback, NULL);
  14. prefs_reset();
  15. g_epan = epan_new();
  16. }
  17. catch(...)
  18. {
  19. fprintf(stderr, "init wireshark failed\n");
  20. return -1;
  21. }
  22. return 0;
  23. }

其中斜体部分一般不需要调用。epaninit第3个参数也可以为NULL。以kg开头的一堆函数都是我定义的回调函数,wireshark在特定情况下会调用这些回调函数,我只是简单地定义了空函数:

  1. void kg_report_failure(const char* fmt, va_list)
  2. {
  3. }
  4. void kg_report_open_failure(const char* filename, int err, gboolean for_writing)
  5. {
  6. }
  7. void kg_report_read_failure(const char* filename, int err)
  8. {
  9. }
  10. void kg_report_write_failure(const char* filename, int err)
  11. {
  12. }
  13. void kg_register_callback(register_action_e action, const char* msg, gpointer data)
  14. {
  15. }

比较值得注意的是粗体的部分:prefs_reset重置了当前偏好设置,如果不执行这一函数,在启动时某些偏好设置没有初始化,会导致程序崩溃(如geoip设置);g_epan = epan_new()创建了一个epan_t句柄,这个句柄在整个pcap文件生命期内都应该存在,并应在打开下一个文件前销毁并创建新的实例。
wsFini用来清理epan资源,其实现如下:

  1. int wsFini()
  2. {
  3. if(g_epan)
  4. {
  5. epan_free(g_epan);
  6. g_epan = 0;
  7. }
  8. epan_cleanup();
  9. return 0;
  10. }

最关键的解析函数wsDissect的实现如下:

  1. int wsDissect(const uint8_t* pkt, uint32_t len, wsTreeFunc func)
  2. {
  3. epan_dissect_t edt;
  4. tvbuff_t* tvb;
  5. frame_data fdata;
  6. struct wtap_pkthdr phdr;
  7. if(!g_epan)
  8. return -1;
  9. if(NULL == pkt || len < 1)
  10. return -1;
  11. memset(&phdr, 0, sizeof(struct wtap_pkthdr));
  12. phdr.rec_type = REC_TYPE_PACKET;
  13. phdr.caplen = len;
  14. phdr.len = len;
  15. // fcs_len应设为0,否则ether协议解析器将认为frame最后2字节为校验值,影响应用层协议解析
  16. phdr.pseudo_header.eth.fcs_len = 0;
  17. tvb = tvb_new_real_data(pkt, len, len);
  18. memset(&fdata, 0, sizeof(frame_data));
  19. fdata.num = 1; //pkt->uid;
  20. fdata.pkt_len = len;
  21. fdata.cap_len = len;
  22. fdata.cum_bytes = len;
  23. fdata.lnk_t = WTAP_ENCAP_ETHERNET;
  24. fdata.flags.encoding = PACKET_CHAR_ENC_CHAR_ASCII;
  25. fdata.flags.has_ts = 0; // 1
  26. fdata.abs_ts.secs = 0; //pkt->ts_sec;
  27. fdata.abs_ts.nsecs = 0; //pkt->ts_usec;
  28. edt.tree = NULL;
  29. epan_dissect_init(&edt, g_epan, TRUE, TRUE);
  30. epan_dissect_run(&edt,
  31. WTAP_FILE_TYPE_SUBTYPE_PCAP,
  32. &phdr,
  33. tvb,
  34. &fdata,
  35. NULL);
  36. func(edt.tree->first_child);
  37. epan_dissect_cleanup(&edt);
  38. return 0;
  39. }

其中最关键的是其中的粗体字部分。epan_dissect_run需要多个参数,其上面的一大段只不过是为了构造这些参数而已,比如phdr, tvb, fdata。解析成功后,调用回调函数func来访问proto_tree,注意应从root节点的第一个chid节点开始遍历。
默认回调函数wsPrintTree的实现如下:

  1. static void wsPrintTreeDo(proto_tree* tree, int level);
  2. void wsPrintTree(proto_tree* tree)
  3. {
  4. wsPrintTreeDo(tree, 0);
  5. }
  6. void wsPrintTreeDo(proto_tree* tree, int level)
  7. {
  8. gchar label[ITEM_LABEL_LENGTH];
  9. gchar* label_ptr;
  10. if(!tree)
  11. return;
  12. if(!PROTO_ITEM_IS_HIDDEN(tree))
  13. {
  14. for(int i=0; i<level; ++i)
  15. printf(" ");
  16. if(tree->finfo->rep)
  17. label_ptr = tree->finfo->rep->representation;
  18. else
  19. {
  20. label_ptr = label;
  21. proto_item_fill_label(tree->finfo, label);
  22. }
  23. printf("%s\n", label_ptr);
  24. }
  25. wsPrintTreeDo(tree->first_child, level+1);
  26. wsPrintTreeDo(tree->next, level);
  27. }

它在内部调用了一个递归函数wsPrintTreeDo函数,用来递归遍历proto_tree的各级节点并输出。wsPrintTreeDo的第2个参数是树节点的深度级别,用来在打印时进行缩进,以显示树的层次关系。解析树节点中的文本信息在finfo成员中。
主程序caller使用这些函数,并构造了一个简单的报文来进行解析:

  1. #include <cstdio>
  2. #include <cstdlib>
  3. #include "ws.h"
  4. #define DATA_LEN 73
  5. // 帧数据, 不包括PCAP文件头和帧头
  6. // 数据为ethernet - ipv4 - udp - DNS, 上网时随便捕获的.
  7. const uint8_t data[DATA_LEN] =
  8. {
  9. 0x7E, 0x6D, 0x20, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x45, 0x00,
  10. 0x00, 0x3B, 0x5F, 0x15, 0x00, 0x00, 0x40, 0x11, 0xF1, 0x51, 0x73, 0xAB, 0x4F, 0x08, 0xDB, 0x8D,
  11. 0x8C, 0x0A, 0x9B, 0x90, 0x00, 0x35, 0x00, 0x27, 0xEF, 0x4D, 0x43, 0x07, 0x01, 0x00, 0x00, 0x01,
  12. 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x74, 0x04, 0x73, 0x69, 0x6E, 0x61, 0x03, 0x63, 0x6F,
  13. 0x6D, 0x02, 0x63, 0x6E, 0x00, 0x00, 0x01, 0x00, 0x01
  14. };
  15. int main(int argc, char** argv)
  16. {
  17. int ret = 0;
  18. ret = wsInit();
  19. if(ret != 0)
  20. goto END;
  21. wsDissect(data, DATA_LEN, wsPrintTree);
  22. wsFini();
  23. END:
  24. return 0;
  25. }


运行结果:
Wireshark: 二次开发 (旧) - 图24

7.2 调用display filter

display filter(以下简称df)是wireshark一个强大的功能,可以使我们过滤自己感兴趣的报文。libwireshark也暴露了df相关接口,基数据结构和函数包括:

  • dfilter_t display filter句柄
  • dfilter_compile 编译df句柄
  • dfilter_free 销毁df句柄
  • epan_dissect_prime_dfilter 在解析之前,为epan_dissect_t准备df
  • dfilter_apply_edt 对解析结果应用df,返回布尔值,表示是否命中df

相关声明都在/epan/dfilter/dfilter.h。

我们修改7.1节中的代码,加入display filter支持。首先是头文件:

  1. #include "epan/dfilter/dfilter.h"
  2. typedef void(*wsTreeFunc) (proto_tree*, bool);
  3. bool wsSetFilter(const char* str);
  4. int wsClearFilter();
  5. void wsPrintTree(proto_tree* tree, bool dfHit);

其中:

  • 加入必要的头文件dfilter.h
  • 修改wsTreeFunc的原型,加入第2个bool型的参数,表示是否命中df,当然wsPrintTree也相应地要做修改
  • 添加了2个函数wsSetFilter和wsClearFilter,分别用于设置和清除df。wsSetFilter接受一个字符串参数,表示过滤语句文本,如“http”之类

接下来是实现文件,需要定义一个全局的df变量。
dfilter_t* g_dfilter = 0;
wsSetFilter和wsClearFilter实现:

  1. bool wsSetFilter(const char* str)
  2. {
  3. bool ret = false;
  4. wsClearFilter();
  5. ret = dfilter_compile(str, &g_dfilter);
  6. return ret;
  7. }
  8. int wsClearFilter()
  9. {
  10. if(g_dfilter)
  11. {
  12. dfilter_free(g_dfilter);
  13. g_dfilter = 0;
  14. }
  15. return 0;
  16. }

wsDissect需要修改:

  1. int wsDissect(const uint8_t* pkt, uint32_t len, wsTreeFunc func)
  2. {
  3. ...
  4. bool hit = false;
  5. ...
  6. edt.tree = NULL;
  7. epan_dissect_init(&edt, g_epan, TRUE, TRUE);
  8. if(g_dfilter)
  9. epan_dissect_prime_dfilter(&edt, g_dfilter); // <-----
  10. epan_dissect_run(&edt,
  11. WTAP_FILE_TYPE_SUBTYPE_PCAP,
  12. &phdr,
  13. tvb,
  14. &fdata,
  15. NULL);
  16. if(g_dfilter)
  17. hit = dfilter_apply_edt(g_dfilter, &edt); // <-----
  18. func(edt.tree->first_child, hit);
  19. epan_dissect_cleanup(&edt);
  20. return 0;
  21. }

注意黑体的部分。
修改后的wsPrintTree:

  1. void wsPrintTree(proto_tree* tree, bool dfHit)
  2. {
  3. if(dfHit)
  4. wsPrintTreeDo(tree, 0);
  5. else
  6. printf("NOT hit dfilter, don't care.\n");
  7. }

当命中df时照旧输出,否则打印一条提示。
最后修改主程序。

  1. int main(int argc, char** argv)
  2. {
  3. ...
  4. wsDissect(data, DATA_LEN, wsPrintTree);
  5. if(!wsSetFilter("udp"))
  6. {
  7. fprintf(stderr, "set dfilter failed\n");
  8. wsFini();
  9. goto END;
  10. }
  11. wsDissect(data, DATA_LEN, wsPrintTree);
  12. wsClearFilter();
  13. ...
  14. }

一共调用wsDissect解析两次,第一次调用时没有设置任何displayer filter,由于wsDissect函数中命中标记hit默认设为false,那么此报文不会命中任何df,wsPrintTree会打印出一条提示;第二次调用前设置了df条件“udp”,此报文正好是udp报文,那么会命中df,输出解析结果:
Wireshark: 二次开发 (旧) - 图25

参考资料与网址