wireshark 源码分析之健壮性探究(一)

wireshark 是如何避免段错误的呢?

下列章节选自 README.developer 开发者手册的第 3 点。

前言

翻译完本小结,总结的很好,但是缺失简单的例子,这需要在阅读源码时留意本小结对 wireshark 源码的指导实践,。

3. Robustness.

3. 健壮性

Wireshark is not guaranteed to read only network traces that contain correctly-
formed packets. Wireshark is commonly used to track down networking
problems, and the problems might be due to a buggy protocol implementation
sending out bad packets.

Wireshark不能保证只读取包含正确格式数据包的网络跟踪文件。Wireshark通常用于跟踪网络问题,而这些问题可能是由于有错误的协议实现发送了错误的数据包导致的。

Therefore, code does not only have to be able to handle
correctly-formed packets without, for example, crashing or looping
infinitely, they also have to be able to handle *incorrectly*-formed
packets without crashing or looping infinitely.

因此,代码不仅需要能够处理正确格式的数据包,比如避免崩溃或无限循环,还需要能够处理不正确格式的数据包,同样也要避免崩溃或无限循环。

//即 wireshark 在处理错误格式的数据包时如何避免段错误和死循环呢?

Here are some suggestions for making code more robust in the face
of incorrectly-formed packets:

以下是一些使代码在面对格式不正确的数据包时更加健壮的建议:

Do *NOT* use "ws_assert()" or "ws_assert_not_reached()" with input data in dissectors.
*NO* value in a packet's data should be considered "wrong" in the sense
that it's a problem with the dissector if found; if it cannot do
anything else with a particular value from a packet's data, the
dissector should put into the protocol tree an indication that the
value is invalid, and should return.  The "expert" mechanism should be
used for that purpose.

在解剖器中不要使用“ws_assert()”或“ws_assert_not_reached()”来处理输入数据。 不要认为数据包中的任何值都是“错误”的(译者注:因为所有可能的值都应该被视为有效的输入),如果发现一个值无法使用,解剖器应该在协议树中放置一个指示,表明该值是无效的,并返回。为此,应该使用“expert”机制,而不是使用断言。

//"NO" 表示“没有一个值”,意思是“没有一个数据包的数据值应该被视为“错误”(即有问题的)”,因为所有可能的值都应该被视为有效的输入。

Use assertions to catch logic errors in your program. A failed assertion
indicates a bug in the code. Use ws_assert() instead of g_assert() to
test a logic condition. Note that ws_assert() will be removed with
WS_DISABLE_ASSERT. Therefore assertions should not have any side-effects,
otherwise the program may behave inconsistently.

使用断言来捕获程序中的逻辑错误。断言失败表明代码中存在错误。使用ws_assert()来测试逻辑条件,而不是使用g_assert()。请注意,ws_assert()将被WS_DISABLE_ASSERT移除。因此,断言不应具有任何副作用,否则程序可能表现不一致。 

//在代码中搜索, ws_assert() 断言用在非 packet-*.c 的代码文件中,这里的解剖器指的是packet-*.c 文件

Use ws_assert_not_reached() instead of g_assert_not_reached() for
unreachable error conditions. For example if (and only if) you know
'myvar' can only have the values 1 and 2 do:

对于无法到达的错误情况,使用 ws_assert_not_reached() 而不是 g_assert_not_reached() 。 例如,如果(且仅当)您知道“myvar”只能具有值 1 和 2 时:
    switch(myvar) {
    case 1:
        (...)
        break;
    case 2:
        (...)
        break;
    default:
        ws_assert_not_reached();
        break;
    }

For dissectors use DISSECTOR_ASSERT() and DISSECTOR_ASSERT_NOT_REACHED()
instead, with the same caveats as above.

对于解剖器,请改用 DISSECTOR_ASSERT()DISSECTOR_ASSERT_NOT_REACHED(),注意事项与上述相同。//此处的及解析器指的是什么?

You should continue to use g_assert_true(), g_assert_cmpstr(), etc for
"test code", such as unit testing. These assertions are always active.
See the GLib Testing API documentation for the details on each of those
functions.

您应该继续将 g_assert_true()、g_assert_cmpstr() 等用于“测试代码”,例如单元测试。 这些断言总是处于激活状态。
有关这些函数的详细信息,请参阅 GLib Testing API 文档。

If there is a case where you are checking not for an invalid data item
in the packet, but for a bug in the dissector (for example, an
assumption being made at a particular point in the code about the
internal state of the dissector), use the DISSECTOR_ASSERT macro for
that purpose; this will put into the protocol tree an indication that
the dissector has a bug in it, and will not crash the application.

如果有这样的情况,你不是在检查数据包中的无效数据项,而是在检查解析器中的错误(例如,在代码的特定点对解析器内部状态做出了某种假设),则应使用DISSECTOR_ASSERT宏来进行检查;这将在协议树中插入一个指示解析器存在错误的标记,并且不会导致应用程序崩溃。

If you are allocating a chunk of memory to contain data from a packet,
or to contain information derived from data in a packet, and the size of
the chunk of memory is derived from a size field in the packet, make
sure all the data is present in the packet before allocating the buffer.
Doing so means that:

如果你正在分配一个用于包含数据包中的数据或从数据包中导出的信息的内存块,并且内存块的大小是从数据包中的大小(Size/Length)字段中导出的,请确保在分配缓冲区之前所有数据都存在于数据包中。这样做意味着:

    1) Wireshark won't leak that chunk of memory if an attempt to
       fetch data not present in the packet throws an exception.

1)如果尝试获取数据包中不存在的数据导致异常抛出,Wireshark不会泄漏该内存块。

and

    2) it won't crash trying to allocate an absurdly-large chunk of
       memory if the size field has a bogus large value.

2)如果大小(Size/Length)字段有一个错误的大值,尝试分配一个非常大的内存块时,它不会崩溃。

If you're fetching into such a chunk of memory a string from the buffer,
and the string has a specified size, you can use "tvb_get_*_string()",
which will check whether the entire string is present before allocating
a buffer for the string, and will also put a trailing '\0' at the end of
the buffer.

如果你正在从缓冲区中获取一个指定大小的字符串到这样的内存块中,你可以使用"tvb_get_*_string()"函数。它将检查整个字符串是否存在于缓冲区中,然后再为字符串分配缓冲区,并在缓冲区的末尾放置一个尾随的'\0'。

If you're fetching into such a chunk of memory a 2-byte Unicode string
from the buffer, and the string has a specified size, you can use
"tvb_get_faked_unicode()", which will check whether the entire string
is present before allocating a buffer for the string, and will also
put a trailing '\0' at the end of the buffer.  The resulting string will be
a sequence of single-byte characters; the only Unicode characters that
will be handled correctly are those in the ASCII range.  (Wireshark's
ability to handle non-ASCII strings is limited; it needs to be
improved.)

如果你正在从缓冲区中获取一个指定大小的 2 字节 Unicode 字符串到这样的内存块中,你可以使用 "tvb_get_faked_unicode()" 函数。它将检查整个字符串是否存在于缓冲区中,然后再为字符串分配缓冲区,并在缓冲区的末尾放置一个尾随的 '\0'。生成的字符串将是一系列单字节字符;Wireshark 能够正确处理的 Unicode 字符仅限于 ASCII 范围内的字符。(Wireshark 处理非 ASCII字符串的能力有限,需要进一步改进。)

If you're fetching into such a chunk of memory a sequence of bytes from
the buffer, and the sequence has a specified size, you can use
"tvb_memdup()", which will check whether the entire sequence is present
before allocating a buffer for it.

如果你正在从缓冲区中获取一个指定大小的字节序列到这样的内存块中,你可以使用"tvb_memdup()"函数。它将检查整个序列是否存在于缓冲区中,然后再为序列分配缓冲区。

Otherwise, you can check whether the data is present by using
"tvb_ensure_bytes_exist()" although this frequently is not needed: the
TVB-accessor routines can handle requests to read data beyond the end of
the TVB (by throwing an exception which will either mark the frame as
truncated--not all the data was captured--or as malformed).

否则,你可以使用"tvb_ensure_bytes_exist()"函数来检查数据是否存在,尽管通常不需要这样做:TVB访问器例程可以处理读取超出TVB结尾的数据请求(通过抛出异常,该异常将标记帧为被截断——未捕获所有数据——或不正常)。

Note also that you should only fetch string data into a fixed-length
buffer if the code ensures that no more bytes than will fit into the
buffer are fetched ("the protocol ensures" isn't good enough, as
protocol specifications can't ensure only packets that conform to the
specification will be transmitted or that only packets for the protocol
in question will be interpreted as packets for that protocol by
Wireshark).  If there's no maximum length of string data to be fetched,
routines such as "tvb_get_*_string()" are safer, as they allocate a buffer
large enough to hold the string.  (Note that some variants of this call
require you to free the string once you're finished with it.)

还要注意的是,只有在代码确保获取的字节数不超过缓冲区可容纳的字节数时,才应将字符串数据获取到固定长度的缓冲区中。仅仅依靠协议规范是不够的,因为协议规范不能保证只有符合规范的数据包会被传输,或者只有针对该协议的数据包会被Wireshark解释为该协议的数据包。如果没有字符串数据的最大长度限制,则使用"tvb_get_*_string()"等例程更安全,因为它们会分配一个足够大的缓冲区来容纳字符串。(请注意,这些调用的某些变体要求你在使用完字符串后释放它。)

If you have gotten a pointer using "tvb_get_ptr()" (which you should not
have: you should seriously consider a better alternative to this function),
you must make sure that you do not refer to any data past the length passed
as the last argument to "tvb_get_ptr()"; while the various "tvb_get"
routines perform bounds checking and throw an exception if you refer to data
not available in the tvbuff, direct references through a pointer gotten from
"tvb_get_ptr()" do not do any bounds checking.

如果您使用"tvb_get_ptr()"获取了一个指针(您不应该这样做,应该认真考虑使用更好的替代函数),那么您必须确保不要引用超出传递给"tvb_get_ptr()"作为最后一个参数的长度的任何数据;虽然各种"tvb_get"例程执行边界检查,并在引用不可用于tvbuff的数据时抛出异常,但通过从"tvb_get_ptr()"获取的指针进行的直接引用不进行任何边界检查。

//这些函数是如何做边界检查的呢?

If you have a loop that dissects a sequence of items, each of which has
a length field, with the offset in the tvbuff advanced by the length of
the item, then, if the length field is the total length of the item, and
thus can be zero, you *MUST* check for a zero-length item and abort the
loop if you see one.  Otherwise, a zero-length item could cause the
dissector to loop infinitely.  You should also check that the offset,
after having the length added to it, is greater than the offset before
the length was added to it, if the length field is greater than 24 bits
long, so that, if the length value is *very* large and adding it to the
offset causes an overflow, that overflow is detected.

如果您有一个循环来分析一系列具有长度字段的项目,且在 tvbuff 中的偏移量按项目的长度前进,那么,如果长度字段是项目的总长度,因此可以为零,您必须检查是否有零长度的项目,并在看到一个时中止循环。否则,零长度的项目可能会导致解析器无限循环。如果长度字段大于24位,则在将长度添加到偏移量之前,还应检查添加后的偏移量是否大于添加长度之前的偏移量,以便在长度值非常大且将其添加到偏移量会导致溢出时检测到该溢出。

If you have a

    for (i = {start}; i < {end}; i++)

loop, make sure that the type of the loop index variable is large enough
to hold the maximum {end} value plus 1; otherwise, the loop index
variable can overflow before it ever reaches its maximum value.  In
particular, be very careful when using gint8, guint8, gint16, or guint16
variables as loop indices; you almost always want to use an "int"/"gint"
or "unsigned int"/"guint" as the loop index rather than a shorter type.

如果你使用如下形式的for循环:

        for (i = {start}; i < {end}; i++)

请确保循环索引变量的类型足够大,能够存储最大的{end}值加1;否则,在达到最大值之前,循环索引变量可能会溢出。特别是,在使用gint8、guint8、gint16或guint16变量作为循环索引时要非常小心;通常情况下,你应该使用“int” / “gint”或“unsigned int” / “guint”作为循环索引,而不是使用较短的类型。

If you are fetching a length field from the buffer, corresponding to the
length of a portion of the packet, and subtracting from that length a
value corresponding to the length of, for example, a header in the
packet portion in question, *ALWAYS* check that the value of the length
field is greater than or equal to the length you're subtracting from it,
and report an error in the packet and stop dissecting the packet if it's
less than the length you're subtracting from it.  Otherwise, the
resulting length value will be negative, which will either cause errors
in the dissector or routines called by the dissector, or, if the value
is interpreted as an unsigned integer, will cause the value to be
interpreted as a very large positive value.

如果您正在从缓冲区中提取长度字段,该字段对应于数据包的某个部分的长度,并从该长度中减去某个值,该值对应于数据包部分中的头的长度,那么请始终检查该长度字段的值是否大于或等于您要从中减去的长度,并在其小于您要从中减去的长度时报告数据包错误并停止解析数据包。否则,将导致所得到的长度值为负数,这将导致解析器或解析器调用的例程中出现错误,或者如果该值被解释为无符号整数,则会导致该值被解释为非常大的正值。

Any tvbuff offset that is added to as processing is done on a packet
should be stored in a 32-bit variable, such as an "int"; if you store it
in an 8-bit or 16-bit variable, you run the risk of the variable
overflowing.

任何在分析数据包时被增加的 tvbuff 偏移量,都应该存储在一个 32 位变量中,例如 "int" 类型。如果将其存储在 8 位或 16 位变量中,可能会发生变量溢出的风险。

sprintf() -> snprintf()
Prevent yourself from using the sprintf() function, as it does not test the
length of the given output buffer and might be writing into unintended memory
areas. This function is one of the main causes of security problems like buffer
exploits and many other bugs that are very hard to find. It's much better to
use the snprintf() function declared by <stdio.h> instead.

sprintf() -> snprintf() 不要使用sprintf()函数,因为它不会检测给定输出缓冲区的长度,可能会写入到意想不到的内存区域。这个函数是安全问题(如缓冲区溢出)和许多其他难以找到的错误的主要原因之一。最好使用< stdio.h>声明的snprintf()函数。

You should test your dissector against incorrectly-formed packets.  This
can be done using the randpkt and editcap utilities that come with the
Wireshark distribution.  Testing using randpkt can be done by generating
output at the same layer as your protocol, and forcing Wireshark/TShark
to decode it as your protocol, e.g. if your protocol sits on top of UDP:

您应该针对格式不正确的数据包测试您的解析器。这可以使用随附在 Wireshark 分发中的 randpkt和 editcap 实用程序来完成。使用 randpkt 进行测试可以通过在与您的协议相同的层级生成输出,并强制 Wireshark / TShark 将其解码为您的协议来完成,例如,如果您的协议位于UDP之上:

    randpkt -c 50000 -t dns randpkt.pcap
    tshark -nVr randpkt.pcap -d udp.port==53,<myproto>

Testing using editcap can be done using preexisting capture files and the
"-E" flag, which introduces errors in a capture file.  E.g.:

使用editcap进行测试可以使用现有的抓包文件和“-E”标志,该标志会在捕获文件中引入错误。例如:

//牛哇

    editcap -E 0.03 infile.pcap outfile.pcap
    tshark -nVr outfile.pcap

//解释一下这个命令;这将使用“badchecksum”错误修改“infile.cap”文件,并将结果写入“outfile.cap”。此错误会更改捕获文件中的一个或多个校验和,导致Wireshark / TShark不会将它们视为正确的协议数据。您可以使用其他错误标志来模拟其他类型的错误,例如“trunc”,该错误会将捕获文件截断到指定的大小。

The script fuzz-test.sh is available to help automate these tests.

可使用fuzz-test.sh脚本来自动化执行这些测试。

//解释一下:脚本 fuzz-test.sh 是一种工具,用于针对大量随机生成和格式错误的数据包捕获自动测试 Wireshark/TShark 解析器。 它使用 randpkt 和 editcap 实用程序生成测试用例,并使用 diff 工具将解析器的输出与预期输出进行比较。 通过运行此脚本,您可以快速识别解析器的错误和问题,并在发布代码之前修复它们。

完~

wireshark源码分析问题这几天在看wireshark(ethereal)源代码。看源代码的主要兴趣点是它的分析模块(dissect)。分析之后他的数据存在哪儿,怎么打印的(-V参数)。我想把分析后的数据,提取出来,存在自己定义的数据结构里面,或者按我自己的格式写入文本中。 看了几天,对一些数据结构,似懂非懂,一些流程也是似懂非懂。可能由于经验不足的原因,搞来搞去就在几个函数,结构体里面打转。好几次以为找到切入点,发现又回来原来的起点。 这两天看晕了。有点打击,水平太差劲了。。呵呵。先这边问问,看看有没有熟悉的朋友。指点一下。先谢谢了。 这样问问题可能太细了。感觉也不大合适。 1. 我应该如何来看代码?如何找到突破点? 2. 有wireshark有了解的朋友,说说你们关于源码剖析的体会。 3. 说什么都可以,朋友觉得对我有用,有启发就好。千万别 “我顶,UP啊”。呵呵:emn23:我觉得重要的是看 pcap库 本帖最后由 peidright 于 2010-04-02 16:36 编辑 楼上说得对!。 看源代码之前,问下你自己,看代码的目的是什么? 对于 wireshark 来说,你是想学他写界面? 还是抓包? 还是业务逻辑? 界面的话,wireshark 还行 抓包的话,应该看pcap库 业务逻辑的话。不应该看wireshark,看tcpdump.看下啊,:em03:看看这个也许对你有帮助 添加一个基础的RDP解析器 下面我们将循序渐进地设计一个基础的RDP解析器。它依次包含如下构成要素: 包类型字段(占用8比特位,可能的值为:1,初始;2,终结;3,数据); 标志集字段(占用8比特位:0x01,开始包;0x02,结束包;0x04先包); 序列号字段(占用16比特位); 1. 创建解析器 首先您需要选择解析器的类型:内置型(包含在主程序中)或插件型。 插件是容易编写的,先做一个插件型解析器吧。 例1. 解析器初始设定. #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include void proto_register_rdp(); void proto_reg_handoff_rdp(); static void dissect_rdp(tvbuff_t *tvb,packet_info *pinfo,proto_tree *tree); static int proto_rdp=-1; static dissector_handle_t rdp_handle; static gint ett_rdp = -1; define TCP_PORT_RDP 3389 void proto_register_rdp(void) { proto_rdp=proto_register_protocol( "RDP Protocol", "RDP", "rdp"); } 现在来逐一分析这段代码。首先我们有一些常规的包含文件,最好依惯例在文件开始包含进来。随后是一些函数的前置声明,我们稍后定义它们。 接下来我们定义了一个整型变量"proto_rdp"用于记录我们的协议注册信息。它被初始化为"-1",当解析器注册到主程序中后,其值便会得到更新。这样做可保证我们方便地判断是否已经做了初始工作。将所有不打算对外输出的全局变量和函数声明为"static"是一个良好的习惯,因为这可以保证命名空间不被污染。通常这是容易做到的,除非您的解析器非常庞大以致跨越多个文件。 之后的模块变量"TCP_PORT_RDP"则包含了协议使用的TCP端口号,我们会对通过该端口的数据流进行解析。 solaris10下proc编译问题 >紧随其后的是解析器句柄"rdp_handle",我们稍后对它进行初始化。 至此我们已经拥有了和主程序交互的基本元素,接下来最好再把那些预声明的函数定义一下,就从注册函数"proto_register_rdp"开始吧。 首先调用函数"proto_register_protocol"注册协议。我们能够给协议起3个名字以适用不同的地方。全名和短名用在诸如"首选项(Preferences)"和"已激活协议(Enabled protocols)"对话框以及记录中已生成的域名列表内。缩略名则用于过滤器。 下面我们需要一个切换函数。 例2. 解析器切换. void proto_reg_handoff_rdp(void) { static gboolean initialized=FALSE; if(!initialized) { rdp_handle = create_dissector_handle(dissect_rdp, proto_rdp); dissector_add("tcp.port", TCP_PORT_RDP, rdp_handle); initialized=TRUE; } } 这段代码做了什么呢?如果解析器尚未初始化,则对它进行初始化。首先创建解析器。这时注册了了函数"dissect_rdp"用于完成实际的解析工作。之后将该解析器与TCP端口号相关联,以使主程序收到该端口的UDP数据流时通知该解析器。 至此我们终于可以写一些解析代码了。不过目前我们仅写点儿基本功能占个位置。 例3.解析 static void dissect_rdp(tvbuff_t *tvb,packet_info *pinfo,proto_tree *tree) { if(check_col(pinfo->cinfo, COL_PROTOCOL)) { col_set_str(pinfo->cinfo, COL_PROTOCOL, "RDP"); } if(check_col(pinfo->cinfo,COL_INFO)) { col_clear(pinfo->cinfo,COL_INFO); } } 该函数用于解析传递给它的数据包。包数据由"tvb"参数指向的特殊缓冲区保管。现在我们已深入到协议的细节,对它们您肯定是了若指掌。包信息结构参数"pinfo"包含了协议的基本数据,以供我们更新。参数"tree"则指明了详细解析发生的地方。 这里我们仅做了保证通过的少量工作。前两行检查UI中"协议(Protocol)"列是否已显示。如果该列已存在,就在这儿显示我们的协议名称。这样人们就知道它被识别出来了。另外,如果"信息(INFO)"列已显示,我们就将它的内容清除。 至此我们已经准备好一个可以编译和安装的基本解析器。不过它目前只能识别和标示协议。 为了编译解析器并创建插件,还需要在解析器代码文件"packet-rdp.c"所在目录下创建一些提供支持的文件: - Makefile.am - UNIX/Linux的makefile模板 - Makefile.common - 包含了插件文件的名称 - Makefile.nmake - 包含了针对Windows平台的Wireshark插件makefile - moduleinfo.h - 包含了插件版本信息 - moduleinfo.nmake - 包含了针对Windows平台的DLL版本信息 - packet-rdp.c - 这是您的解析器原代码文件 - plugin.rc.in - 包含了针对Windows平台的DLL资源模板 "Makefile.common"和"Makefile.am"文件中涉及到相关文件和解析器名称的地方一定要修改正确。"moduldeinfo.h"和"moduleinfo.nmake"文件中的版本信息也需要正确填充。一切准备妥善后就可以将解析器编译为DLL或共享库文件了(使用nmake工具)。在wireshark文件夹下的"plugins"文件夹中,建立"rdp"文件夹。将修改过的Makefile.common,Makefile.am,moduleinfo.nmake,moduldeinfo.h,Makefile.nmake及packet-rdp.c文件考到"rdp"文件夹下,然后进行编译,rdp插件自动生成完整,就可以正常工作了。 1. 解析协议细节 现在我们已经有了一个可以运用的简单解析器,让我们再为它添点儿什么吧。首先想到的应该就是标示数据包的有效信息了。解析器在这方面给我们提供了支持。 首先要做的事情是创建一个子树以容纳我们的解析结果。这会使协议的细节显示得井井有条。现在解析器在两种情况下被调用http://www.boomss.com:其一,用于获得数据包的概要信息;其二,用于获得数据包的详细信息。这两种情况可以通过树指针参数"tree"来进行区分。如果树指针为NULL,我们只需要提供概要信息;反之,我们就需要拆解协议完成细节的显示了。基于此,让我们来增强这个解析器吧。 例4 static void dissect_rdp(tvbuff_t *tvb,packet_info *pinfo,proto_tree *tree) { proto_item *ti=NULLV; if(check_col(pinfo->cinfo,COL_PROTOCOL)) { col_set_str(pinfo->cinfo,COL_PROTOCOL,"RDP"); } if(check_col(pinfo->cinfo,COL_INFO)) { col_clear(pinfo->cinfo,COL_INFO); } if(tree) { ti = proto_tree_add_item(tree, proto_rdp, tvb, offset, -1, FALSE);} } 这里我们为解析添加一个子树。它将用于保管协议的细节,仅在必要时显示这些内容。 我们还要标识被协议占据的数据区域。在我们的这种情况下,协议占据了传入数据的全部,因为我们假设协议没有封装其它内容。因此,我们用"proto_tree_add_item"函数添加新的树结点,将它添加到传入的协议树"tree"中,用协议句柄"proto_rdp"标识它,用传入的缓冲区"tvb"作为数据,并将有效数据范围的起点设为"0",长度设为"-1"(表示缓冲区内的全部数据)。至于最后的参数"FALSE",我们暂且忽略。 做了这个更改之后,在包明细面板区中应该会出现一个针对该协议的标签;选择该标签后,在包字节面板区中包的剩余内容就会高亮显示。 现在进入下一步,添加一些协议解析功能。在这一步我们需要构建一组帮助解析的表结构。这需要对"proto_register_rdp"函数做些修改。首先定义一组静态数组。 例5 定义数据结构 static hf_register_info hf[]= { { &hf;_rdp_version, { "TPKT Header:Version", "rdp.version",
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值