原文
这篇文章将向你讲解如何用Lua语言简单地创建协议解剖器。当你使用Wireshark尚未拥有解剖器的自定义协议时,此功能非常有用。例如Wireshark长这样
很难说出数据部分中的各个字节代表什么。
Wireshark是用C语言编写的,Wireshark的解剖器通常也用C语言编写。然而,Wireshark有一个Lua实现,使不熟悉C的人可以轻松编写解剖器。对于那些不熟悉Lua的人来说,它是一种非常轻量级的编程语言,旨在作为应用程序中的脚本语言实现,以扩展其功能。
使用Lua的缺点是这样编写出来的解剖器将比用C编写的解剖器慢。
在我们开始编写解剖器之前,让我们来看看Lua的速成课程。详细了解语言并不必要,但我们必须了解基础知识。
Lua速成班
- Lua是多范式的,在某种程度上支持面向过程风格,函数式编程,并且它还具有一些面向对象的编程特性。它没有开箱即用的类,原型或继承,但它们可以由程序员定制。
- 它是动态输入的
- 范围分为
local
和global
,未声明的情况下默认为全局可见。 - 不需要分号结尾。空格并不像Python中那样重要
- 以
--
开头的行为注释 - 不要使用
++
或+=
。使用i = i +1
替代 - 不等于用
~=
表示而非!=
- 数据类型:string、number、boolean、nil、function、userdata、thread、table。数字型代表所有的数字,包括浮点型和整数。布尔型包含两个值:false和true。字符串由一对双引号或单引号来表示。你可以忘掉thread和userdata。
- nil表示一个无效值,变量在被明确赋值前为nil。
- 在条件表达式中:nil和false为假,其他为真
- Lua有一个名为table的类型,它也是Lua里唯一的数据结构。表实现了关联数组。关联数组可以由数字和其他类型(如字符串)索引。它们没有固定的大小,可以动态添加元素。表通常称为对象。它们是这样创建的:
new_table = {}
赋值:
new_table[20] = 10
new_table["x"] = "test"
a.x = 10 -- same as a["x"] = 10
它们可以具有函数,并且通常与JavaScript中的对象非常相似。
- 条件分支看起来像这样:
if i == 0 then variable = 200
elseif i == 1 then variable = 300
else variable = 400 end
- 循环看起来像这样:
while i < 10 do
i = i + 1
end
for i = 0, 10, 1 do
print(i)
end
for后面跟的内容分别为 i = first, last, delta. break
可以使用,但是continue不被允许
- 函数声明:
function add(arg1, arg2)
return arg1 + arg2
end
函数调用:
local added_number = add(2, 3)
如果你看到这样的函数调用:
a:func1()
a.func2()
然后函数func1
和func2
属于表(对象)a
。使用冒号是语法糖,用于将对象本身作为参数传递给函数。这意味着a:func1()
类似于a.func1(a)
。
那是重要的事情。如果您关心细节,可以阅读 Lua 5.3参考手册。
Setup
Lua脚本放在plugins文件夹的子文件夹中,该文件夹位于Wireshark根文件夹中。子文件夹以Wireshark版本命名。例如。 Windows上的C:\ Program Files \ Wireshark \ plugins \ 2.4.2。启动Wireshark时,该脚本将处于活动状态。在对脚本进行更改后,必须重新启动Wireshark,或者使用Ctrl + Shift + L重新加载所有Lua脚本。
我正在使用当前的最新版本。我在这里做的可能不适用于早期版本。
协议
在这篇文章中探讨的最有趣的协议可能是Wireshark当前不知道的自定义协议,但我使用过的所有自定义协议都与工作有关,我不能在这里发布有关它们的信息。所以我们将看看MongoDB wire protocol。
Wrieshark已经有一个 Mongo dissector ,但是我不会使用那个
根据上面链接的规范,MongoDB有线协议是使用端口号27017的TCP / IP协议。字节排序是小端,意味着最低有效字节首先出现。大多数协议都是大端。唯一的区别是字节的排序。例如,如果我们有一个int32,这些字节为:00 4F 23 11
大端,那么小端版本将是11 23 4F 00
.这是编写解剖器时必须考虑的事项
在这篇文章中,我将只看一下协议的头。看起来像这样
我们可以看到它有四个int32,每个包含4个字节,因为4 * 8 = 32。
设置样板代码
让我们首先设置一些所有解剖器都需要的样板代码:
mongodb_protocol = Proto("MongoDB", "MongoDB Protocol")
mongodb_protocol.fields = {}
function mongodb_protocol.dissector(buffer, pinfo, tree)
length = buffer:len()
if length == 0 then return end
pinfo.cols.protocol = mongodb_protocol.name
local subtree = tree:add(mongodb_protocol, buffer(), "MongoDB Protocol Data")
end
local tcp_port = DissectorTable.get("tcp.port")
tcp_port:add(59274, mongodb_protocol)
我们开始创建一个Proto(protocol)对象并将其命名为mongodb_protocol
。表构造函数有两个参数:name
和description
。该协议需要fields
表和解剖器函数。我们还没有添加任何字段,因此fields
表为空。对于我们指定类型的每个数据包,都会调用一次解剖器函数。
解剖器函数有三个参数:buffer
,pinfo
,tree
。buffer
包含数据包的缓冲区,是一个Tvb对象
。它包含我们想要剖析的数据。pinfo
包含数据包列表的列,是一个Pinfo对象。最后,tree
是树根,是TreeItem对象。
在解剖器函数内部,我们首先检查缓冲区的长度,然后返回它是否为空。
如前所述,
pinfo
对象包含数据包列表中的列。当我们收到MongoDB类型的数据包时,我们可以使用它来设置协议名称。在脚本的第一行,我们将协议的名称设置为“MongoDB”(通过将名称传递给构造函数)。我们在此处设置协议列名称
pinfo.cols.protocol = mongodb_protocol.name
并且协议列名称从TCP更改为MONGODB:
然后,我们必须在“数据包详细信息”窗格中的树结构中创建一个子树。它通过向树对象添加一个额外的树项来完成,该树对象作为参数传递给解剖器函数。
local subtree = tree:add(mongodb_protocol, buffer(), "MongoDB Protocol Data")
字符串是子树的名称。没有添加任何字段,它将如下所示:
最后,我们必须将协议分配给端口。就我而言,我将使用端口59274,因为这是我用来连接Mongo数据库的端口。
local tcp_port = DissectorTable.get("tcp.port")
tcp_port:add(59274, mongodb_protocol)
如果协议使用UDP而不是TCP,也可以使用“udp.port”。
添加字段(fields)
这个阶段该脚本已经可以运行,但它没有做任何有用的事情。为了使脚本能够执行有用的操作,我们必须添加要解析的字段。通过创建ProtoField
对象来创建字段。我们可以通过仅添加第一个字段来开始。 MongoDB有线协议规范中的第一个字段是消息长度,它是一个int32。
mongodb_protocol = Proto("MongoDB", "MongoDB Protocol")
message_length = ProtoField.int32("mongodb.message_length", "messageLength", base.DEC)
mongodb_protocol.fields = { message_length }
function mongodb_protocol.dissector(buffer, pinfo, tree)
length = buffer:len()
if length == 0 then return end
pinfo.cols.protocol = mongodb_protocol.name
local subtree = tree:add(mongodb_protocol, buffer(), "MongoDB Protocol Data")
subtree:add_le(message_length, buffer(0,4))
end
local tcp_port = DissectorTable.get("tcp.port")
tcp_port:add(59274, mongodb_protocol)
我们在上面的解剖器函数中添加以下内容:
message_length = ProtoField.int32("mongodb.message_length", "messageLength", base.DEC)
第一个参数在过滤器设置中用作标签,第二个用作子树中的标签,最后一个用于决定变量值的显示方式。在这种情况下,我想以十进制显示值,但我也可以使用base.HEX
以十六进制格式显示它。但十六进制格式不适用于int32。
ProtoField有几种我们可以使用的函数:uint8(),uint16(),string()等。我们必须使用符合规范的那个。可以在here找到所有功能的列表。
然后,我们将该字段添加到协议的fields
表中:
mongodb_protocol.fields = { message_length }
最后将字段添加到子树:
subtree:add_le(message_length, buffer(0,4))
我使用add_le
而不是add
,因为我们正在使用little-endian
协议。如果协议是大端,我们将不得不使用add
。该函数有两个参数:我们进一步增加的字段和缓冲区范围。我们可以通过使用作为缓冲区对象一部分的range
函数来获取一系列缓冲区。 buffer(offset,length)
是范围函数的缩写形式。 buffer(0,4)
表示我们要从第一个字节开始,然后取4个字节。我们想要从0开始的原因是因为我们正在处理标头中的第一个字段。我们占用4个字节,因为这是int32的大小。
使用Ctrl + Shift + L重新加载Lua脚本后,Wireshark应如下所示:
我们可以看到它正确解析了
messageLength
。我们还可以看到,我们不必解析所有字段以使其工作。我们可以逐步扩展插件。标头中的其他三个字段也是int32s。我们可以像添加消息长度字段一样添加它们。因此,这部分的最终脚本如下所示:
mongodb_protocol = Proto("MongoDB", "MongoDB Protocol")
message_length = ProtoField.int32("mongodb.message_length", "messageLength", base.DEC)
request_id = ProtoField.int32("mongodb.requestid" , "requestID" , base.DEC)
response_to = ProtoField.int32("mongodb.responseto" , "responseTo" , base.DEC)
opcode = ProtoField.int32("mongodb.opcode" , "opCode" , base.DEC)
mongodb_protocol.fields = { message_length, request_id, response_to, opcode }
function mongodb_protocol.dissector(buffer, pinfo, tree)
length = buffer:len()
if length == 0 then return end
pinfo.cols.protocol = mongodb_protocol.name
local subtree = tree:add(mongodb_protocol, buffer(), "MongoDB Protocol Data")
subtree:add_le(message_length, buffer(0,4))
subtree:add_le(request_id, buffer(4,4))
subtree:add_le(response_to, buffer(8,4))
subtree:add_le(opcode, buffer(12,4))
end
local tcp_port = DissectorTable.get("tcp.port")
tcp_port:add(59274, mongodb_protocol)
我们必须在调用范围函数(buffer(offset,length)
)时将偏移量增加4,以便为每个字段读取4个新字节。如果我们处理的不是int32s,我们当然必须增加其他东西。
数据包详细信息窗格最终如下所示:
我们现在很高兴。在下一部分中,我将介绍调试和更高级的解析方法。现在,我们只看到操作码的数值,但操作码名称会更有趣。