Du bist hier:Start»Linux»Entwicklung»Wireshark

Beispiel eines Wireshark Dekoders

02.04.2015

Wireshark Logo

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

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

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:

# example of a data packet

              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

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 wireshark and capture the interface localhost (lo)

# 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.

Kommentar schreiben

Ihre Daten werden verschlüsselt übertragen. Der Kommentar wird gelesen und eventuell veröffentlicht.
Wenn der Inhalt des Kommentars oder Teile des Kommentars nicht veröffentlicht werden, dann werden die gespeicherten Daten nach maximal 4 Wochen gelöscht. Um zukünftigen Missbrauch der Kommentarfunktion zu verhindern, werden die zum Kommentar gehörenden IP Adressen maximal 4 Wochen gespeichert.