Beispiel eines Wireshark Dekoders
02.04.2015
Als Software-Entwickler habe ich immer wieder mit der Analyse von Netzwerkprotokollen zu tun. Dabei ist es vorteilhaft, wenn ich einen Dekoder für diese Netzwerkprotokolle zur Verfügung habe. Bei speziellen Protokollen gibt es meistens noch keine öffentlich verfügbaren Dekoder. Dann schreibe ich mir selbst einen Dekoder für Wireshark (auch Dissector genannt). Im Vergleich zur Situation vor einigen Jahren ist das Schreiben eines Dekoders für Wireshark mit der Programmiersprache Lua wesentlich einfacher geworden. Am Beispiel eines fiktiven Protokollstapels mit zwei Protokollschichten zeige ich, wie so ein Dekoder aussehen kann.
Das Ziel
Das Ziel des Protokoll-Dekoders ist es:
- bestimmte TCP-Ports automatisch einem Netzwerkprotokoll zuzuordnen
- den Typ der Nachricht und die Protokoll-Schicht zu dekodieren und im rechten Informationsfeld von Wireshark anzuzeigen
- eine aufklappbare Baum-Ansicht (tree info) im mittleren Fenster mit den einzelnen Datenfeldern des Protokolls anzuzeigen und
- schließlich die Bytes im unteren Teilfenster von Wireshark den Datenfeldern zuzuordnen
Der fertige Dekoder in Wireshark soll dann so aussehen:
Wireshark mit eigenem Dekoder
Der Protokoll-Stack
Der fiktive Protokollstapel besteht aus zwei Protokoll-Schichten "Example Layer 1" und "Example Layer 2". Die Schicht "Example Layer 2" wird in die Nutzdaten von "Example Layer 1" nach dem Matroschka-Prinzip gesteckt und befindet sich somit eine Protokoll-Ebene über "Example Layer 1":
Protokollstack des Beispiel-Protokolls
Beide Protokollschichten bestehen jeweils aus den Datenfeldern:
- Type: der Typ des Netzwerkpakets - kodiert in einem zwei Byte großen Feld
- Length: die Länge der Nutzdaten in Bytes - kodiert in einem zwei Byte großen Feld
- User Data: die Nutzdaten des Pakets - maximal 65535 Byte (2 hoch 16 - 1)
Die Pakettypen (Type) des Beispiels sind relativ einfach:
- 0x01 ist ein Connect Request zum Verbindungsaufbau
- 0x02 ist ein Connect Response zur Bestätigung des Verbindungsaufbaus durch die Gegenseite
- 0x03 ist ein Datenpaket
- 0x04 ist ein Disconnect zum Verbindungsabbau
Die zweite Protokoll-Schicht hat dieselben Datentypen, aber zusätzlich 0x10 addiert - also zum Beispiel 0x11 für einen Connect Request. Die Datentypen sind in den zwei Byte des "Type"-Feldes der Protokolle definiert. Die Länge des Datenpakets ist ebenfalls in zwei Byte kodiert (network byte order) und gibt die Anzahl der Bytes des Nutzdatenfeldes an (user data).
Beispiel eines Datenpakets
Der Byte-Strom eines Nutzdatenpaketes würde zum Beispiel so aussehen:
Layer 2 Header
Layer 1 Header /\
_____/\____ ___/ \__
/ \ / \
00 03 00 06 00 13 00 02 41 41
| | | | user data
| length | length
Type: data Type: data
- 00 03 - Pakettyp: Datenübertragung
- 00 06 - Länge: 6 Byte Nutzdaten in Layer 1
- 00 13 - Pakettyp: Datenübertragung auf Layer 2
- 00 02 - Länge: 2 Byte Nutzdaten in Layer 2
- 41 41 - eigentliche Nutzdaten, ASCII: AA
Falls die Darstellung nicht ganz klar geworden ist, dann einfach weiter unten auf dieser Seite den Dekoder und die Beispiel-Datenübertragung herunterladen.
Simulierte Client-Server Anwendung
Um einen Protokoll-Dekoder zu testen ist natürlich die Aufzeichnung der Paketdaten erforderlich (Englisch: packet capture). Das folgende Sequenz-Diagramm stellt den Ablauf der Kommunikation des Beispiel-Protokolls dar. Dabei wird ein Verbindungsaufbau auf den beiden Protokollschichten "Layer 1" und anschließend "Layer 2" simuliert, Daten übertragen und die Verbindung wieder abgebaut. Der Server lauscht auf TCP-Port 9000.
Sequenz-Diagramm des Protokolls
Obwohl es sonst anders läuft - in diesem Beispiel schickt der Server das Connect-Request-Paket. Zur Simulation des fiktiven Protokolls ist keine aufwendige Client-Server-Anwendung nötig. Stattdessen reicht das Programm netcat (nc) für Sockets aus. Zum Schreiben der Byteströme über die Netcat-Sockets benutze ich die Funktion printf mit hexadezimaler Darstellung der Daten. Um eine Segmentierung eines Datenpakets auf zwei TCP-Pakete zu simulieren, teile ich ein Paket über zwei printf-Befehle auf und benutze dazwischen das Programm sleep. Durch die Segmentierung kann später die korrekte Bearbeitung des Längenfeldes getestet werden.
# start the simulated client
while [ ! -e start.txt ];do sleep 0.1; done; (sleep 0.4; printf "\x00\x02\x00\x00"; sleep 1; printf "\x00\x03\x00\x04\x00\x12\x00\x00"; sleep 1; printf "\x00\x03\x00\x06\x00\x13\x00\x02\x42\x42"; sleep 2; printf "\x00\x04\x00\x00";sleep 1) | nc 127.0.0.1 9000
# start the simulated server in another terminal in the same directory
touch start.txt;(printf "\x00\x01\x00\x00"; sleep 1; printf "\x00\x03\x00\x04\x00\x11\x00\x00"; sleep 1; printf "\x00\x03\x00\x06\x00\x13\x00\x02\x41\x41"; sleep 1; printf "\x00\x03\x00\x06\x00\x13\x00\x02\x43\x43\x00\x03\x00\x06\x00\x13\x00\x02\x44\x44\x00\x03\x00\x06\x00\x13\x00"; sleep 0.3; printf "\x02\x43\x43"; sleep 0.3; printf "\x00\x03\x00\x06\x00\x13\x00\x02\x45\x45\x00\x03\x00\x06\x00\x13\x00\x02\x46\x46"; sleep 0.2; printf "\x00\x04\x00\x00";sleep 1) | nc -l 9000;rm -f start.txt
Funktionierendes Beispiel zum Herunterladen
Die Netzwerkübertragung der Beispiel-Anwendung mit netcat (nc) habe ich in einer Packet-Capture-Datei mit Wireshark beziehungsweise tcpdump aufgezeichnet. Zusammen mit dem Lua-Dekoder kann Wireshark wie folgt gestartet werden:
Die Beispiel-Dateien können über die Links heruntergeladen werden. Über die Option "-Xlua_script" wird der Dekoder an Wireshark übergeben und die Packet-Capture-Datei geöffnet. Zur Darstellung der Nutzdaten empfiehlt sich als Filter in Wireshark "tcp.len>0". Dieses Beispiel dient zum Experimentieren und Lernen. Damit sollte es möglich sein einen eigenen Dekoder für ein neues Protokoll zu schreiben.
Anhang: Listing des Wireshark-Dekoders example.lua
Das Listing sollte selbsterklärend sein. Wichtig ist die Auswertung des Längenfeldes von "Example Layer 1". Da TCP Bytestrom-orientiert ist, kann es passieren, dass mehrere Protokoll-Pakete in einem TCP-Paket stecken oder ein Protokoll-Paket auf mehrere TCP-Pakete verteilt ist. Diese Fälle werden vom Dekoder abgefangen. Ein anderes interessantes Detail ist die Verknüpfung der beiden Protokoll-Ebenen miteinander. Hierbei wird einfach Dissector.get("example_layer_2") mit den Nutzdaten von "Example Layer 1" aufgerufen, um "Example Layer 2" zu dekodieren.
-- Example Protocol Wireshark dissector (a.k.a. decoder)
-- Author: Torsten Traenkner
-- Version History:
-- 0.01 (02.04.2015)
--
-- This dissector decodes an example protocol.
--
-- use with:
-- wireshark -Xlua_script:example.lua example.pcap
--
-- #############################
-- ## DECODER SETTINGS BEGIN: ##
-- #############################
debug_example_protocol = false
-- add your TCP ports here:
example_tcp_ports = {
9000, 9001
}
-- display example layers as tree info in wireshark
example_add_tree_info = true
-- example_add_tree_info = false
-- ##########################
-- ## DECODER SETTINGS END ##
-- ##########################
do
local example_layer_2_message_types = {
[0x11] = "Connect Request",
[0x12] = "Connect Response",
[0x13] = "Data",
[0x14] = "Disconnect"
};
local example_layer_1_message_types = {
[1] = "Connect Request",
[2] = "Connect Response",
[3] = "Data",
[4] = "Disconnect"
};
local example_tree = 0
function append_text_to_example_tree(text, packet_info)
packet_info.cols.info:append(text)
example_tree:append_text(text)
end
-- #####################
-- ## Example Layer 2 ##
-- #####################
example_layer_2 = Proto("example_layer_2", "Example Layer 2")
local example_layer_2_fields = example_layer_2.fields
example_layer_2_fields.message_type = ProtoField.uint16("example_layer_2_fields.message_type", "Message Type", base.HEX)
example_layer_2_fields.length = ProtoField.uint16("example_layer_2_fields.length", "Length", base.DEC)
example_layer_2_fields.userdata = ProtoField.bytes("example_layer_2_fields.userdata", "Userdata", base.NONE)
function example_layer_2.dissector(buffer, packet_info, tree)
if buffer:len() < 4 then return end
-- message type
local example_layer_2_message_type = buffer(0, 2)
local message_type = "unknown"
if example_layer_2_message_types[example_layer_2_message_type:uint()] ~= nil then
message_type = example_layer_2_message_types[example_layer_2_message_type:uint()]
end
local ascii = ""
-- add layer info to protocol tree
if example_add_tree_info then
-- create subtree for example layer 2
example_layer_2_tree = tree:add(example_layer_2, buffer(0))
-- message type
local treeitem = example_layer_2_tree:add(example_layer_2_fields.message_type, example_layer_2_message_type)
treeitem:set_text("Message Type: " .. message_type)
-- length
example_layer_2_tree:add(example_layer_2_fields.length,buffer(2, 2))
-- print("layer 2 length " .. buffer:len())
if buffer:len() > 4 then
-- user data
example_layer_2_tree:add(example_layer_2_fields.userdata,buffer(4, buffer:len() - 4))
-- convert hexadecimal string to ASCII string
start = 4
endPosition = buffer:len() - 1
for index = start, endPosition do
local c = buffer(index,1):uint()
-- append printable characters
if c >= 0x20 and c <= 0x7E then
ascii = ascii .. string.format("%c", c)
else
-- use a dot for the others bytes
ascii = ascii .. "."
end
end
end
end
if message_type ~= "Data" then
append_text_to_example_tree(" Layer 2 [" .. message_type .. "]", packet_info)
else
append_text_to_example_tree(" Layer 2 [" .. message_type .. ": " .. ascii .. "]", packet_info)
end
end
-- #####################
-- ## Example Layer 1 ##
-- #####################
example_layer_1 = Proto("example_layer_1", "Example Layer 1")
local example_layer_1_fields = example_layer_1.fields
example_layer_1_fields.message_type = ProtoField.uint16("example_layer_1_fields.message_type", "Message Type", base.HEX)
example_layer_1_fields.length = ProtoField.uint16("example_layer_1_fields.length", "Length", base.DEC)
function example_layer_1.dissector(buffer, packet_info, tree)
if buffer:len() < 4 then return end
-- message type
local example_layer_1_message_type = buffer(0, 2)
local message_type = "unknown"
if example_layer_1_message_types[example_layer_1_message_type:uint()] ~= nil then
message_type = example_layer_1_message_types[example_layer_1_message_type:uint()]
end
-- add layer info to protocol tree
if example_add_tree_info then
-- create subtree for example layer 1
example_layer_1_tree = tree:add(example_layer_1, buffer(0))
-- message type
local treeitem = example_layer_1_tree:add(example_layer_1_fields.message_type, example_layer_1_message_type)
treeitem:set_text("Message Type: " .. message_type)
-- length
example_layer_1_tree:add(example_layer_1_fields.length,buffer(2, 2))
end
if message_type ~= "Data" then
append_text_to_example_tree(" [" .. message_type .. "]", packet_info)
end
Dissector.get("example_layer_2"):call(buffer(4, buffer:len() - 4):tvb(), packet_info, tree)
end
-- #########################################
-- ## example protocol all layers chained ##
-- #########################################
example_protocol = Proto("example", "Example Protocol")
-- Example Protocol dissector definition
-- buffer contains the packet data
-- packet_info is packet info
-- tree is root of the Wireshark tree view
function example_protocol.dissector(buffer, packet_info, tree)
if debug_example_protocol then print("decoding example protocol " .. buffer:len()) end
local length = 0
local packet_offset = 0
packet_info.cols.info = tostring(packet_info.src_port) .. " > " .. tostring(packet_info.dst_port)
-- decode in a loop since one tcp packet can contain several example protocol packets
-- also reassemble splitted packets
repeat
-- check if there are enough bytes for the length field
if buffer:len() - length >= 4 then
-- the buffer contains the length field
-- read the length field
local packet_length = buffer(length + 2, 2):uint() + 4
length = length + packet_length
-- check for complete packet
if length > buffer:len() then
-- not all data available yet
packet_info.desegment_len = length - buffer:len()
return
end
-- real dissector starts here with complete packets
packet_info.cols.protocol = example_protocol.name
packet_info.cols.info:append(",")
-- create subtree for exaple protocol
example_tree = tree:add(example_protocol, buffer(packet_offset, packet_length))
Dissector.get("example_layer_1"):call(buffer(packet_offset, packet_length):tvb(), packet_info, example_tree)
packet_offset = packet_offset + packet_length
else
-- the buffer does not contain the length field
packet_info.desegment_len = DESEGMENT_ONE_MORE_SEGMENT
return
end
until length >= buffer:len()
end
-- initialization routine
function example_protocol.init()
if debug_example_protocol then print("in initialization of example protocol") end
local tcp_dissector_table = DissectorTable.get("tcp.port")
-- TCP ports for protocol decoding
for i,port in ipairs(example_tcp_ports) do
if debug_example_protocol then print("register example protocol for port: " .. port) end
tcp_dissector_table:add(port, example_protocol)
end
end
end
Viel Spaß beim Experimentieren ! Falls noch etwas unklar sein sollte, dann kannst du die Kommentar-Funktion benutzen.