Wireshark - własny protokół
July 8, 2024

Protokół oznacza “oficjalny standard postępowania w danych sytuacjach”. Jako programiści embedded słysząc słowo “protokół” myślimy najczęściej o protokole komunikacyjnym, czyli zestawie reguł, na podstawie których urządzenia komunikują się ze sobą. Protokoły komunikacyjne są wszechobecne, od tych niskopoziomowych w obrębie PCB (I2C, SPI, UART), przez protokoły sieciowe (TCP, UDP), po wysokopoziomowe (JSON-RPC). W tym artykule pokażemy jak dodać do Wireshark obsługę naszego własnego protokołu - napiszemy plugin w Lua, który posłuży do dekodowania ramek wiadomości. Do dzieła!

W pracy programisty embedded nie raz tworzymy własne protokoły, następnie implementujemy je, żeby potem spędzić wiele godzin szukając dlaczego komunikacja nie działa. Debugowanie komunikacji między urządzeniami jest stałą częścią naszej pracy, dlatego warto korzystać z narzędzi które tą pracę ułatwią.

O ile na najniższym poziomie często wystarczy analizator stanów logicznych, o tyle w wyższych wartstwach i przy bardziej skomplikowanej komunikacji warto sięgnąć po inne narzędzia. Jednym z najlepszych narzędzi do analizy protokołów sieciowych jest Wireshark. Zwykle kojarzy się on nam z analizą ruchu sieciowego TCP/IP, jednak w praktyce możemy wykorzystać jego zaawansowany interfejs również do analizy innych protokołów, w tym naszych własnych protokołów.

W tym artykule pokażemy jak dodać do Wireshark obsługę naszego własnego protokołu - napiszemy plugin w Lua, który posłuży do dekodowania ramek wiadomości. Do dzieła!

Przykładowy protokół

Na potrzeby tego artykułu zdefiniujmy prosty protokół excom (EXample COMmunication Protocol), który mógłby posłużyć do sterowania urządzeniem wyposażonym w wyświetlacz LCD oraz diody LED. Protokół ten będzie działał na zasadzie zapyatnie-odpowiedź, co reprezentują odpowiednio typy request_t i response_t. Dla uproszczenia zakładamy że dane przesyłane będą w little-endian. Oto plik excom_protocol.h:
// excom proto - EXample COMmunication Protocol
#pragma once

#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>

#define __packed __attribute__((__packed__))

enum RequestType {
    REQ_DISPLAY = 1,
    REQ_LED = 2,
};

typedef struct  {
    uint32_t text_length;
    char text[];
} __packed display_request_t;

typedef struct {
    uint16_t id;
    bool state;
} __packed led_request_t;

typedef union {
    display_request_t display;
    led_request_t led;
} __packed request_data_t;

typedef struct {
    uint32_t id;
    uint8_t type;
    request_data_t data;
} __packed request_t;

typedef struct {
    uint32_t id;
    bool status;
} __packed response_t;

#define REQUEST_BASE_SIZE (sizeof(request_t) - sizeof(request_data_t))

Przyjmijmy że komunikacja przebiega pomiędzy dwoma urządzeniami:

  • klient - wysyła request_t i oczekuje response_t;
  • serwer - oczekuje request_t, przetwarza zapytanie i odsyła odpowiedź response_t;
W naszym modelu każde zapytanie będzie posiadało identyfikator request_t.id, dla którego serwer będzie odsyłał odpowiedź z tym samym indentyfikatorem. Klient ma do dyspozycji dwa rodzaje zapytań, rozróżniane na podstawie pola request_t.type przyjmującego jedną z wartości RequestType. Dostępne zapytania to:
  • display_request_t - wyświetlenie tekstu na wyświetlaczu;
  • led_request_t - zmiana stanu diody;

Aplikacja testowa

O ile moglibyśmy zaimplementować nasz protokół na rzeczywistych urządzeniach, to na potrzeby tego artykułu zasymulujemy to na PC. W tym artykule wykorzystamy protokół TCP jako warstwę transportową (model OSI) dla naszego protokołu excom (warstwa aplikacji). Uprości to na początku znacząco testowanie naszego protokołu w Wireshark. W kolejnym artykule pokażemy jak łatwo zmodyfikować nasze rozwiązanie tak, aby pominąć TCP lub wykorzystać inny protokół niższej warstwy.

Do naszej symulacji zdefiniujemy dwie aplikacje: excom_client.c oraz excom_server.c. Każda czyta z STDIN i wysyła na STDOUT - w ten sposób programy są maksymalnie uproszczone i zawierają jedynie logikę przesyłania danych z naszego protokołu:
  • excom_client.c
    • wysyła kolejne display_request_t z tekstem snprintf(..., "Hello world %zu!", i) do momentu otrzymania response_t ze statusem true
    • potem wysyła serię led_request_t dla id w zakresie [0, 42)
  • excom_server.c
    • w pętli odczytuje request_t i wysyła response_t, response_t.status ustawia na true jedynie w co 42 pakiecie

Link do pełnego kodu dostępny w źródłach [1].

Tak przygotowane aplikacje możemy łatwo “owinąć” w TCP korzystając z narzędzia socat:
# Terminal 1
socat -x TCP-LISTEN:9000,reuseaddr,fork EXEC:./excom_server.elf
# Terminal 2
socat -x TCP:localhost:9000 EXEC:./excom_client.elf
W jednym terminalu startujemy nasz serwer - socat będzie nasłuchiwał na porcie 9000 i dla każdego klienta wystartuje naszą aplikację server.elf przesyłając STDIN/STDOUT po TCP. Analogicznie, w drugim terminalu socat połączy się po TCP do portu 9000 i wystartuje client.elf przesyłając STDIN/STDOUT.
Przepływ danch podczas nawiązywania połączenia
Flaga -x spowoduje dodatkowo wypisanie całej komunikacji jako hexdump. Pomoże to zweryfikować że wszystko działa jak należy. Po wykonaniu drugiej komendy, w obydwu terminalach powinniśmy zobaczyć przesyłane dane:
$ socat -x TCP-LISTEN:9000,reuseaddr,fork EXEC:./excom_server.elf
> 2024/04/22 11:09:37.000217666  length=23 from=0 to=22
 01 00 00 00 01 0e 00 00 00 48 65 6c 6c 6f 20 77 6f 72 6c 64 20 30 21
< 2024/04/22 11:09:37.000217760  length=5 from=0 to=4
 01 00 00 00 00
> 2024/04/22 11:09:37.000217877  length=23 from=23 to=45
 02 00 00 00 01 0e 00 00 00 48 65 6c 6c 6f 20 77 6f 72 6c 64 20 31 21
< 2024/04/22 11:09:37.000217934  length=5 from=5 to=9
 02 00 00 00 00

Pierwsze kroki z Wireshark

Mając przygotowane środowisko, możemy wystartować Wireshark żeby podsłuchać komunikację między excom_server.c a excom_client.c.

Jeśli jeszcze tego nie zrobiliśmy, instalujemy Wireshark pobierając instalator ze strony (Windows/Mac) lub korzystając z managera pakietów (Linux). Artykuł przeprowadzający przez proces instalacji znajdziecie w źródłach [2]. Na Linuxie warto dodać swojego użytkownika do grupy wireshark, aby nie musieć startować Wiresharka z sudo (jako root).
Aby rozpocząć zbieranie pakietów w ruchu lokalnym na naszym PC, wybieramy interfejs Loopback: lo. Jako że lokalnie może odbywać się też komunikacja na innych portach, ograniczymy nasłuchiwanie do portu 9000 korzystając z tzw. “Capture filter” wpisując port 9000 w odpowiednim polu jak pokazano poniżej:
Następnie startujemy programy klienta i serwera korzystając z socat, tak jak zostało to pokazane w ostatnium listingu w poprzedniej sekcji. Powinniśmy zobaczyć przesłane pakiety w Wireshark:
Całą procedurę startu Wireshark można uprościć korzystając z komendy wireshark -k -i lo -f 'port 9000', która wykonuje to wszystko w jednym kroku! Jak widać, obecnie dane przesyłane w ramkach TCP są wyświelane po prostu jako “Data”. Czas to zmienić!

Dissector protokołów

Wireshark korzysta z tzw. dissectorów do analizowania otrzymanych pakietów. Dissector parsuje dane zgodnie z danym protokołem, po czym może przekazać część danych do dissectorów innych protokołów. O ile Wireshark posiada wbudowane wsparcie dla większości popularnych protokołów, o tyle o naszym na pewno jeszcze nie wie. Na szczęście istnieje możliwość dodawania własnych dissectorów jako “pluginów” do Wiresharka. Plugin taki możemy zdefiniować w języku skryptowym Lua i załadować dynamicznie przy starcie Wireshark.

Lua to lekki język skryptowy stworzony z myślą o wbudowywaniu go w inne programy. Sam język daje duże możliwości, pomimo prostej składni, jedynie 8 typów danych i kilku dziwactw (indeksowanie od 1!). Jeśli nie mieliście z nim dotąd styczności, to do naszych celów wystarczy zaznajomić się z Learn Lua in Y minutes [3] bez sekcji 3.1 i 4. Dokładniejsze informacje można znaleźć na oficjalnej stronie [4].

Nasz dissector zdefiniujemy w pliku excom_protocol.lua. Aby go załadować, będziemy startowali Wireshark z flagą -X lua_script:excom_protocol.lua (zakładając że jesteśmy w tym samym katalogu co nasz plik), czyli:
wireshark -X lua_script:excom_protocol.lua -k -i lo -f 'port 9000'

Na początek zdefiniujmy wstępną implementację dissectora:

-- Our protocol object
excom_proto = Proto('excom-proto', 'EXample COMmunication protocol')

-- Helper function for ProtoField names
local function field(field_name)
    return string.format('%s.%s', excom_proto.name, field_name)
end

-- RequestType enum
local request_type = {
    REQ_DISPLAY = 1,
    REQ_LED = 2,
}
-- Mapping of RequestType value to name
local request_type_names = {}
for name, value in pairs(request_type) do
    request_type_names[value] = name
end

-- Define field types available in our protocol, as a table to easily reference them later
local fields = {
    id = ProtoField.uint32(field('id'), 'Request ID', base.DEC),
    -- request_t
    type = ProtoField.uint8(field('type'), 'Request type', base.HEX, request_type_names),
    -- response_t
    status = ProtoField.bool(field('status'), 'Response status'),
}

-- Add all the types to Proto.fields list
for _, proto_field in pairs(fields) do
    table.insert(excom_proto.fields, proto_field)
end

-- Dissector callback, called for each packet
excom_proto.dissector = function(buf, pinfo, root)
    -- arguments:
    -- buf: packet's buffer (https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Tvb.html#lua_class_Tvb)
    -- pinfo: packet information (https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Pinfo.html#lua_class_Pinfo)
    -- root: node of packet details tree (https://www.wireshark.org/docs/wsdg_html_chunked/lua_module_Tree.html#lua_class_TreeItem)

    -- Set name of the protocol
    pinfo.cols.protocol:set(excom_proto.name)

    -- Add new tree node for our protocol details
    local tree = root:add(excom_proto, buf())

    -- Extract message ID, this is the same for request_t and response_t
    -- `id` is of type uint32_t, so get a sub-slice: buf(offset=0, length=4)
    local id_buf = buf(0, 4)
    tree:add_le(fields.id, id_buf)

    -- request_t
    local type_data = buf(4, 1)
    tree:add_le(fields.type, type_data)
end

-- Register our protocol to be automatically used for traffic on port 9000
local tcp_port = DissectorTable.get('tcp.port')
tcp_port:add(9000, excom_proto)
Na początku tworzymy obiekt typu Proto reprezentujący nasz dissector. Następnie musimy wypełnić pole excom_proto.fields zawierające definicje pól dostępnych w naszym protokole. Dla wygody defniujemy je jako tabelę w zmiennej fields, której wartości dodajemy potem do tabeli-listy excom_proto.fields. Pola definiujemy jako ProtoField z odpowiednim typem danych na podstawie excom_protocol.h. Dodatkowo dla pola request_t.type definiujemy mapowanie wartości do nazw z enuma RequestType.

Wireshark przed wczytaniem naszego pliku ładuje definicje API które będą dostępne jako zmienne globalne. Opis dostępnego API można znaleźć na oficjalnej stronie Wireshark’s Lua API Reference Manual [5].

Logika naszego dissectora definiowana jest w funkcji którą musimy przypisać do pola excom_proto.dissector. Funkcja ta otrzymuje jako argumenty bufor z danymi pakietu, dodatkowe informacje o pakiecie oraz obiekt drzewa informacji które prezentowane będą po kliknięciu pakietu w GUI. Za pomocą metody root:add dodajemy nowy węzeł drzewa powiązany z naszym protokołem. Następnie musimy “sparsować“ dane pakietu i dodać je do drzewa. Na razie dekodujemy jedynie pola id (offset=0, długość=4) i type (offset=4, długość=1) z request_t. Dodajemy je korzystając z metody tree:add_le ponieważ nasze dane serializowane były w formacie little-endian. Na samym końcu rejestrujemy nasz protokół tak, aby Wireshark zastosował go do danych TCP przesyłanych na porcie 9000. Możemy teraz przetestować nasz dissector:
# Terminal 1
wireshark -X lua_script:excom_protocol.lua -k -i lo -f 'port 9000'
# Terminal 2
socat -x TCP-LISTEN:9000,reuseaddr,fork EXEC:./excom_server.elf
# Terminal 3
socat -x TCP:localhost:9000 EXEC:./excom_client.elf
W kolumnie Protocol powinno wyświetlić się EXCOM-PROTO, a po kliknięciu na konkretny pakiet zamiast “Data” powinniśmy zobaczyć zdekodowane pola naszego protokołu:

Sukces! Potrafimy już zdekodować pola naszego protokołu, teraz czas dodać resztę logiki.
Pełen kod dostępny poniżej w źródłach [6].

Dekodowanie odpowiedzi

Obecnie nasza implementacja jest niekompletna. Co więcej, jest niepoprawna - zawsze dekodujemy dane jako request_t, choć przecież połowa ramek to response_t. Zacznijmy od rozwiązania tego problemu. Jako że i tak założyliśmy że komunikacja przebiega na porcie 9000, to możemy skorzystać z tego faktu. Serwer będzie nasłuchiwał na porcie 9000, zaś klient będzie wysyłał z losowego portu, tak więc dane wysyłane do portu 9000 interpretujemy jako request_t, a pozostałe jako response_t:
local server_port = 9000

excom_proto.dissector = function(buf, pinfo, root)
    local tree = root:add(excom_proto, buf())

    local id_buf = buf(0, 4)
    tree:add_le(fields.id, id_buf)

    if pinfo.dst_port == 9000 then
        -- request_t
        local type_data = buf(4, 1)
        tree:add_le(fields.type, type_data)
    else
        -- response_t
        tree:add_le(fields.status, buf(4, 1))
    end
end
Następnie możemy dodać logikę parsowania całości request_t:
local fields = {
    id = ProtoField.uint32(field('id'), 'Request ID', base.DEC),
    -- request_t
    type = ProtoField.uint8(field('type'), 'Request type', base.HEX, request_type_names),
    -- response_t
    status = ProtoField.bool(field('status'), 'Response status'),
    -- display_request_t
    display_text_length = ProtoField.uint32(field('display.text_length'), 'Text length', base.DEC),
    display_text = ProtoField.string(field('display.text'), 'Text', base.ASCII),
    -- led_request_t
    led_id = ProtoField.uint16(field('led.id'), 'LED ID', base.DEC),
    led_state = ProtoField.bool(field('led.state'), 'LED state'),
}

excom_proto.dissector = function(buf, pinfo, root)
    local tree = root:add(excom_proto, buf())

    local id_buf = buf(0, 4)
    tree:add_le(fields.id, id_buf)

    if pinfo.dst_port == server_port then
        -- request_t
        local type_data = buf(4, 1)
        tree:add_le(fields.type, type_data)

        -- request_data_t depending on the `type` field
        local type = type_data:le_uint()
        if type == request_type.REQ_DISPLAY then
            -- display_request_t
            local len_buf = buf(5, 4)
            tree:add_le(fields.display_text_length, len_buf)
            tree:add_le(fields.display_text, buf(9, len_buf:le_uint()))
        elseif type == request_type.REQ_LED then
            -- led_request_t
            tree:add_le(fields.led_id, buf(5, 2))
            tree:add_le(fields.led_state, buf(7, 1))
        end
    else
        -- response_t
        tree:add_le(fields.status, buf(4, 1))
    end
end
Rodzaj zapytania rozróżniamy po polu type, przy czym długość pola display_request_t.display_text określamy dynamicznie na podstawie pola display_text_length z bajtów 5-8 (buf(5, 4)). Zamiast wyłączać i włączać Wireshark, możemy załadować ponownie nasz dissector korzystając ze skrótu klawiszowego Ctrl+Shift+L. Po wprowadzeniu tych zmian powinniśmy zobaczyć pole display_text ze zdekodowanym tekstem:

Pełen kod dostępny poniżej w źródłach [7].

Parowanie request-response

W przypadku naszego testu wszystkie wiadomości idą po kolei (request_t, response_t, request_t, response_t, …), jednak w rzeczywistości wielu klientów może wysłać request_t, a serwer może na nie odpowiedzieć z opóźnieniem. Wireshark pozwala na oznaczanie sekwencji wiadomości. Służą do tego pola ProtoField.framenum. Wireshark nadaje każdemu pakietowi unikalny identyfikator. W naszym dissectorze musimy utworzyć mapowanie od request/response ID do identyfikatoru pakietu.
local fields = {
    -- (...)
    -- special fields to provide information about matching request/response
    request = ProtoField.framenum(field('request'), 'Request', base.NONE, frametype.REQUEST),
    response = ProtoField.framenum(field('response'), 'Response', base.NONE, frametype.RESPONSE),
}

-- Mappings of request/response ID to frame numbers
local id2frame = {
    request = {}, -- request id -> request frame number
    response = {}, -- response id -> response frame number
}

excom_proto.dissector = function(buf, pinfo, root)
    local tree = root:add(excom_proto, buf())

    local id_buf = buf(0, 4)
    tree:add_le(fields.id, id_buf)

    local id = id_buf:uint()

    if pinfo.dst_port == server_port then
        --- (...)

        -- On first dissection run (pinfo.visited=false) store mapping from request id to frame number
        if not pinfo.visited then
            id2frame.request[id_buf:uint()] = pinfo.number
        end

        -- If possible add information about matching response
        if id2frame.response[id] then
            tree:add_le(fields.response, id2frame.response[id])
        end
    else
        --- (...)

        if not pinfo.visited then
            id2frame.response[id_buf:uint()] = pinfo.number
        end
        if id2frame.request[id] then
            tree:add_le(fields.request, id2frame.request[id])
        end
    end
end
Korzystamy tu z faktu że Wireshark wywołuje nasz dissector wielokrotnie. Korzystając z pola pinfo.visited sprawdzamy czy paczkę parsujemy pierwszy raz (akruat w tym przypadku nie jest to konieczne, ale ogółem jest to dobra praktyka gdy chcemy przechowywać “stan” poza naszą funkcją dissectora). Przy pierwszym parsowaniu uzupełniamy nasze tabele mapujące request/response ID do identyfikatora pakietu. Następnie (przy każdym parsowaniu) sprawdzamy czy dla danego ID zapisaliśmy już odpowiadający request/response, i jeśli tak, to dodajemy do drzewa odpowiednie pola. Po naszych zmianach powinniśmy zobaczyć pola Request/Response po kliknięciu działające jako “link” do odpowiadającej wiadomości. Dodatkowo w liście pakietów po lewej zostaną dodane strzałki (→ jako request, ← jako response):

Pełen kod dostępny poniżej w źródłach [8].

Składanie paczek TCP

Nasza implementacja dissectora ma jeden poważny problem, który dotąd pomijaliśmy - TCP nie gwarantuje, że dane otrzymamy w pojedynczym pakiecie. Może się zdarzyć że w jednej ramce TCP otrzymamy kilka kolejnych request_t, albo jedynie część danych. Nasz dissector musi być w stanie poradzić sobie również w takiej sytuacji. Aby przetestować taką sytuację skorzystamy z dodatkowego programu excom_spam_client.c, który generuje wszystkie zapytania od razu, po czym wysyła je w kawałkach po 33 bajty, każdorazowo czekając kilka milisekund, tak aby stos TCP wysyłał pojedyncze małe ramki. Sytuację testujemy w następujący sposób:
# Terminal 1
wireshark -X lua_script:excom_protocol.lua -k -i lo -f 'port 9000'
# Terminal 2
socat -x TCP-LISTEN:9000,reuseaddr,fork EXEC:./excom_server.elf
# Terminal 3
socat -x TCP:localhost:9000 EXEC:./excom_spam_client.elf

Poprawna disekcja przy wykorzystaniu TCP wymaga od nas wykonania tak zwanego “TCP Reassembly”. Szczegóły na co należy zwrócić uwagę możemy znaleźć na Wireshark wiki [9].

Aby ułatwić sobie resztę kodu tworzymy pomocniczą funkcję do pobierania kolejnych bajtów z bufora. Gdy bajtów brakuje, take_next zwróci nil i będziemy musieli przerwać parsowanie, w przeciwnym wypadku zwróci zakres bufora z którego chcemy skorzystać:
-- Helper function for taking message data from buffer and configuring pinfo in case we need more data
local function msg_consumer(buf, pinfo)
    local obj = {
        msg_offset = 0, -- offset in buf to start of the current message
        msg_taken = 0, -- number of bytes consumed from current message
        not_enough = false,
    }

    obj.next_msg = function()
        obj.msg_offset = obj.msg_offset + obj.msg_taken
        obj.msg_taken = 0
    end

    obj.take_next = function(n)
        if obj.not_enough then -- subsequent calls
            return
        end

        -- If not enough data in the buffer then wait for next packet with correct offset
        if buf:len() - (obj.msg_offset + obj.msg_taken) < n then
            pinfo.desegment_offset = obj.msg_offset
            pinfo.desegment_len = DESEGMENT_ONE_MORE_SEGMENT
            obj.not_enough = true
            return
        end

        local data = buf:range(obj.msg_offset + obj.msg_taken, n)
        obj.msg_taken = obj.msg_taken + n
        return data
    end

    obj.current_msg_buf = function()
        return buf:range(obj.msg_offset, obj.msg_taken)
    end

    return obj
end

Teraz możemy zmodyfikować dissector tak, żeby parsował wszystkie znalezione w buforze wiadomości, a przy braku danych przerywał i oczekiwał na więcej. Dodatkowo opóźniamy dodawanie do drzewa węzłów naszego protokołu, aby nie dodawać “częściowych” wiadomości. Zmodyfikowany kod dissectora wygląda tak:

excom_proto.dissector = function(buf, pinfo, root)
    -- Construct TCP reassembly helper
    local consumer = msg_consumer(buf, pinfo)

    -- TCP reasasembly - loop through all messages in the packet
    while true do
        consumer.next_msg()

        -- Deferred adding of tree fields
        local tree_add = {}

        -- Extract request/response ID
        local id_buf = consumer.take_next(4)
        if not id_buf then
            return -- not enough data, take_next has configured pinfo to request more data
        end

        table.insert(tree_add, {fields.id, id_buf})
        local id = id_buf:uint()

        -- Distinguish request/response
        if pinfo.dst_port == server_port then
            -- request_t
            local type_buf = consumer.take_next(1)
            if not type_buf then
                return
            end

            table.insert(tree_add, {fields.type, type_buf})

            -- request_data_t depending on the `type` field
            local type = type_buf:le_uint()
            if type == request_type.REQ_DISPLAY then
                -- display_request_t
                local len_buf = consumer.take_next(4)
                local text_buf = len_buf and consumer.take_next(len_buf:le_uint())
                if not text_buf then
                    return
                end
                table.insert(tree_add, {fields.display_text_length, len_buf})
                table.insert(tree_add, {fields.display_text, text_buf})
            elseif type == request_type.REQ_LED then
                -- led_request_t
                local id_buf = consumer.take_next(2)
                local state_buf = consumer.take_next(1)
                if not state_buf then
                    return
                end
                table.insert(tree_add, {fields.led_id, id_buf})
                table.insert(tree_add, {fields.led_state, state_buf})
            end

            -- On first dissection run (pinfo.visited=false) store mapping from request id to frame number
            if not pinfo.visited then
                id2frame.request[id_buf:uint()] = pinfo.number
            end

            -- If possible add information about matching response
            if id2frame.response[id] then
                table.insert(tree_add, {fields.response, id2frame.response[id]})
            end
        else
            -- response_t
            local status_buf = consumer.take_next(1)
            table.insert(tree_add, {fields.status, status_buf})

            if not pinfo.visited then
                id2frame.response[id_buf:uint()] = pinfo.number
            end
            if id2frame.request[id] then
                table.insert(tree_add, {fields.request, id2frame.request[id]})
            end
        end

        -- Add tree node for this message only if we reached this place
        local tree = root:add(excom_proto, consumer.current_msg_buf())
        for _, to_add in ipairs(tree_add) do
            tree:add_le(to_add[1], to_add[2])
        end
    end
end

Po użyciu go do podsłuchania wiadomości wysyłanych przez excom_spam_client.c powinniśmy zobaczyć:

Jak widać Wireshark dodaje notatkę [TCP segment of a reassembled PDU] żeby zaznaczyć że pakiety były składane z kilku ramek. W lewym dolnym rogu widzimy przykład dwóch wiadomości prezentowanych w jednej ramce. W prawym dolnym rogu możemy zobaczyć bufor “Reassembled TCP” zawierający dane z dwóch ramek TCP. Pełen kod dostępny poniżej w źrółach [10].

Podsumowanie

W tym artykule pokazaliśmy jak dodać obsługę własnego protokołu do programu Wireshark - jak widać nie jest to trudne zadanie. Choć zrobiliśmy to na przykładowym prościutkim protokole, to sama idea pozostaje taka sama przy znacznie bardziej skomplikowanych protokołach. W dalszej częsci zobaczymy jak wykorzystać nasze rozwiązanie z pominięciem TCP, jak parsować zagnieżdżone protokoły oraz jak wykorzystać wbudowany dekoder Google’s Protobuf w Wiresharku.

Źródła

[1] GoodByte Github - proto dissector vol1:  https://github.com/goodbyte-software/wireshark-custom-proto-dissector
[2] WIreshark, instalacja: https://www.stationx.net/how-to-install-wireshark/
[3] Learn Lua in Y minutes: https://learnxinyminutes.com/docs/lua/
[4] Lua oficjalna stronahttps://www.lua.org/pil/contents.html
[5] Wireshark's Lua API Manual: https://www.wireshark.org/docs/wsdg_html_chunked/wsluarm_modules.html
[6] GoodByte Github - proto dissector vol2: https://github.com/goodbyte-software/wireshark-custom-proto-dissector/tree/v2
[7] GoodByte Github - proto dissector vo3: https://github.com/goodbyte-software/wireshark-custom-proto-dissector/tree/v3
[8] GoodByte Github - proto dissector vo4: https://github.com/goodbyte-software/wireshark-custom-proto-dissector/tree/v4
[9] Wireshark wiki: https://wiki.wireshark.org/Lua/Dissectors
[10] GoodByte Github - proto dissector vol5: https://github.com/goodbyte-software/wireshark-custom-proto-dissector/tree/v5