Skip to content
Wireshark Wiki 中文翻译整理专题首页原始页面

解析器中的字符串处理

(本页的大部分内容直接取自 Guy 发给 wireshark-dev 邮件列表的电子邮件,但并非全部。)

字符字符串可以使用各种编码来表示字符,例如系统代码页、UTF-8 和 UTF-16;有关这些编码的详细信息,请参见字符编码。

许多应用程序中的字符串处理相对直接。使用库以适合 locale 的编码读写文本,并在内部以 Unicode(通常是 UTF-8)处理所有内容。Wireshark 没有这么轻松。

主要问题在于,Wireshark 必须能够优雅地处理和应对各种编码中的无效字符串。如果某个数据包包含某种冷门编码中的畸形字符串,Wireshark 必须能够将其标记为这种情况,然后继续处理该数据包。我们还没有完全做到这一点。

本页一半是提案,一半是关于 Wireshark 的字符串处理引擎如何工作或应该如何工作的文档。许多内容来自 wireshark-dev 邮件列表上的讨论(例如这个或这个)以及 bugzilla 上的 bug,例如这个。

如果你对此主题有问题、建议或想法,请发送电子邮件到 wireshark-dev@wireshark.org 邮件列表。

第一性原理

字符字符串是来自某个字符集的一系列码点。它使用该字符集的某种特定编码表示为一系列八位组,其中每个字符在该序列中表示为一个由 1 个或多个八位组组成的子序列。

在许多这类编码中,并非所有八位组子序列都对应于字符集中的码点。例如:

  • ASCII 的 8 位编码将每个码点编码为一个八位组,而最高位被置位的八位组不对应于 ASCII 码点;

  • “8-bit” 字符集的 8 位编码将每个码点编码为一个八位组,并且在其中一些字符集中,码点少于 256 个,某些八位组值不对应于字符集中的码点;

  • UTF-8 将每个 Unicode 码点编码为 1 个或多个八位组,并且:

  • 以最高位被置位且其下一位为 0 的八位组开头的八位组序列是无效的,并且不对应于 Unicode 中的码点;

  • 以最高两位被置位的八位组开头,并且其下方的 1 位表示该序列长度为 N 字节,但后面跟随的“高两位为 10 的八位组”少于 N-1 个(要么因为被一个高两位不是 10 的八位组终止,要么因为被字符串末尾终止)的八位组序列是无效的,并且不对应于 Unicode 中的码点;

  • 某个八位组序列没有上述两个问题,但生成的值不是有效的 Unicode 码点,则它是无效的,并且(按定义)不对应于 Unicode 中的码点;

  • UCS-2 将 Unicode 基本多文种平面中的每个码点编码为 2 个八位组(大端或小端),并且从 0 到 65535 的并非所有值都对应于 Unicode 码点(见下一项……);

  • UTF-16 将每个 Unicode 码点编码为 2 个或 4 个八位组(大端或小端),基本多文种平面中的码点编码为 2 个八位组,其他码点编码为一个 2 八位组的“leading surrogate”,后跟一个 2 八位组的“trailing surrogate”(这些是介于 0 和 65535 之间但不是 Unicode 码点的值;见上一项),并且:

  • 未跟随 trailing surrogate 的 leading surrogate(要么因为其后跟随的是一个 2 八位组的 Unicode 码点值,要么因为它位于字符串末尾)不是有效的 UTF-16 序列,并且不对应于 Unicode 中的码点;

  • 未由 leading surrogate 前置的 trailing surrogate(要么因为它位于字符串开头,要么因为其前面是一个 2 八位组的 Unicode 码点值)不是有效的 UTF-16 序列,并且不对应于 Unicode 中的码点;

  • leading surrogate 后跟 trailing surrogate 但得到的值不是有效的 Unicode 码点,则它是无效的,并且(按定义)不对应于 Unicode 中的码点;

  • UCS-4 将每个 Unicode 码点直接编码为 4 个八位组(大端或小端),任何对应于 surrogate 的值或大于最大可能 Unicode 码点值的值都是无效的,并且不对应于 Unicode 中的码点;

等等。

Wireshark 字符串使用场景

Wireshark 中的字符串会被:

  • 显示给用户,要么直接作为包含它们的数据包的摘要或详细信息的一部分显示在该数据包中,要么间接显示,例如,被存储为某个文件的路径名或路径名组件,并在通过某个 ID 而不是路径名引用该文件的数据包中显示;
  • 被数据包匹配表达式(显示过滤器、着色过滤器等)匹配;
  • 由解析器和 tap 在内部处理(无论是在 C、Lua 还是其他语言中);
  • 通过例如 "tshark -T fields -e ..." 交给其他程序。

在所有这些情况下,我们都需要对无效八位组序列做一些处理。

显示给用户

在显示场景中,无效八位组序列应显示为一系列 \xNN 转义序列,一次显示一个八位组。不可打印字符是一个正交问题;它们可以用我们的 Unicode 的 UTF-8 编码来表示,但不应在 UI 中按其自身显示。显示时也应将它们替换为转义序列:

  • 对于码点 >= 0x80,将它们显示为 \uXXXXXX 转义序列(是否裁剪前导零,以及裁剪多少个,留给读者练习;可能最多裁剪两个前导零,但如果只有一个前导零,我不确定该怎么做)——请注意,这不会显示组成该码点的八位组的值,它会显示 Unicode 码点;

  • 对于 0x7F 和大多数码点 < 0x20,将它们显示为 \uXX、\xXX 或 \ooo(是否坚持使用 Traditional Octal(TM)、十六进制或 Unicode,留给读者练习);

  • 对于拥有自己的 C 字符串转义序列的字符(例如 tab、CR、LF),我会将它们显示为该转义序列

(将来,我们可能希望在协议树中让字符串的“value”成为编码和原始八位组的组合;如果某些代码想要用于处理目的的值,它会调用一个例程,将该值转换为 UTF-8,并用 REPLACEMENT CHARACTER 替换无效序列;如果它想要用于显示目的的值,它会调用一个例程,将其转换为 UTF-8,并用转义序列替换无效序列,同时将不可打印字符显示为适当的转义序列。

这就提出了一个问题:在构建协议树时,如果 item 是用 proto_tree_create_item() 创建的,我们是否根本需要把 value 放入协议树项,还是只推迟到实际需要它时再提取 value。懒处理 FTW……)

数据包匹配表达式

在数据包匹配表达式中使用字符串字段时:

  • 所有将来自数据包的字符串值与常量文本字符串进行比较的比较操作,如果该字符串有无效八位组序列,则应失败(因此,0x20 0xC0 0xC0 0xC0 作为一个声称是 UTF-8 的字符串,既不等于也不不等于 " " 或 "hello" 或……);

  • 将来自数据包的字符串值与八位组字符串进行比较的比较操作(与诸如 20:c0:c0:c0 之类的内容进行比较)应对字符串的原始八位组逐个八位组进行比较(因此,无论编码是什么,0x20 0xC0 0xC0 0xC0 都与 20:c0:c0:c0 比较相等);

  • 来自数据包的两个字符串值之间的相等比较操作在以下任一情况下成功:

  • 这两个字符串值在其编码中有效,并转换为相同的 UTF-8 字符串,或

  • 这两个字符串值具有相同的编码并具有相同的八位组。

此外,应有一个单目函数 "valid",它接受一个字符串字段并返回一个 boolean,表示该字符串是否包含任何无效八位组序列。

同样,不可打印字符是一个正交问题。用户应能够在字符串比较常量中同时指定 C 风格转义("\n" 等)和 unicode 转义(\uXXXX)。这意味着,如果你想匹配一个字面量 "",并且你正在 shell 中输入,则需要键入 "\\" 才能让所有转义正确处理。糟糕。

内部处理

在“内部处理”的场景中,如果被查看的字符串部分包含无效八位组序列,则处理应失败;否则处理仍应工作。例如,以 0x47 0x45 0x54 0x20 0xC0 开头的 HTTP request 应被视为一个 operand 无效的 GET request,但以 0x47 0x45 0x54 0xC0 开头的 HTTP request 应被视为无效 request。

显示过滤器引擎应使用一种内部字符串表示,允许处理嵌入的 null bytes(C 风格字符串出局)。需要检查外部工具和依赖项是否能处理这种情况(PCRE2 可以)。

导出到其他程序

看起来有两个可能的使用场景:

  • 程序关注的是原始字节,在这种情况下,我们就应该发送原始字节;如果字符串无效,读取程序会处理它
  • 程序关注的是字符串,在这种情况下,我们应以适合 locale 的编码(最常见的是 UTF-8)发送它,并将无效序列映射为该编码适当的 replacement character

这两个场景应该能够覆盖我能想到的 99% 的情况,并且对我们而言工作量相对最小。第二个应为默认值,因为“其他程序”最常见的情况可能是“shell 的 stdout”或“文本文件”。

API 设计

无效序列

从数据包中获取字符串的函数不应将无效八位组序列映射为一系列 \xNN 转义序列,因为这会干扰在进行数据包匹配和内部处理时对字符串的正确处理。对于这些情况,也许组合使用以下方式:

  • 将无效序列替换为 REPLACEMENT CHARACTER,并且
  • 提供一个单独的指示,说明已经这样做

会是正确的做法。然而,这会丢弃信息,因此你无法将该字符串显示为带有以 \xNN 序列显示的无效序列。

目前,我倾向于继续使用“在 tvb_get_string* 例程中将无效序列替换为 REPLACEMENT CHARACTER”的策略,但不把它视为最终解决方案。

缓冲区长度

函数要么按长度获取字符串(tvb_get_string),要么在第一个 null-terminator 处停止(tvb_get_stringz)。按长度获取时,函数会按原样传递嵌入的 null。不过这会带来一个小问题,因为没有其他方法可以可靠地确定返回缓冲区的大小(如果输入是可能包含超出基本集合的码点的非 UTF8 编码,则不可能预测该字符串的 UTF-8 编码所占用的字节数)。

因此,tvb_get_string 函数最终应转换为返回一个带计数字符串(wmem_strbuf_t)。

Imported from https://wiki.wireshark.org/Development/StringHandling on 2020-08-11 23:13:08 UTC

相关 Wireshark Wiki 页面

网络分析技术档案