Przejdź do treści

Karton

Karton to otwartoźródłowy framework, który umożliwia łatwe łączenie małych skryptów analitycznych napisanych w języku Python w złożony system działający w architekturze mikroserwisowej.

Framework powstał z myślą o analizowaniu próbek malware w ramach serwisu mwdb.cert.pl. Wybraliśmy podejście mikroserwisowe, aby łatwo rozszerzać nasze środowisko o nowe moduły i adaptować je do nowych zagrożeń. Jednocześnie zależało nam na prostocie rozwiązania, aby włączanie nowych komponentów było możliwie jak najłatwiejsze.

Obecnie stanowi fundament nie tylko dla systemów analizujących malware, ale również innych projektów, takich jak modularny skaner Artemis.

Jak działa Karton?

Systemy w Kartonie dzielą się na trzy kategorie:

  • Producer - producent wysyłający zadania (np. MWDB Core przekazujący próbkę do analizy),
  • Consumer - konsument odbierający zadania,
  • Karton - system łączący obie te role, odbierający zadania i wysyłający na ich podstawie nowe zadania (najczęstszy przypadek).

Komunikacja między systemami w Kartonie oparta jest na kolejce zadań. Każdy konsument (skrypt odbierający zadania) deklaruje filtry określające, jakie zadania chce przyjmować od producentów. Centralny broker karton-system dopasowuje nagłówki przychodzących zadań do zadeklarowanych filtrów i kieruje ich kopie do kolejek właściwych konsumentów. Jako backend kolejek wykorzystywany jest Redis.

Framework dostarcza również możliwość wymiany plików poprzez usługę S3, przekazywanie logów poprzez Redis Pub/Sub, a także prosty panel kontrolny (karton-dashboard).

Przepływ sterowany danymi (data-driven workflow)

Działanie Kartona najłatwiej zobrazować na przykładzie prostego potoku analitycznego, który składa się z repozytorium MWDB, sandboksa (w przypadku mwdb.cert.pl jest to DRAKVUF Sandbox, ale tu nazwijmy go "Sandbox") i systemu do ekstrakcji konfiguracji.

Diagram prostego potoku analitycznego

Załóżmy, że plik jest przetwarzany w następujący sposób:

  1. Plik dodany do MWDB jest wysyłany do karton.sandbox
  2. karton.sandbox dokonuje analizy pliku za pomocą rozwiązania Sandbox:
  3. wysyła plik do analizy
  4. czeka na jego wykonanie
  5. pobiera uzyskane artefakty do dalszej analizy: zrzuty pamięci i pobrane pliki w toku wykonania
  6. karton.sandbox wysyła artefakty do karton.config-extractor
  7. karton.config-extractor próbuje pozyskać konfiguracje złośliwego oprogramowania z uzyskanych plików w toku analizy

Nagłówki i filtry

Karton definiuje przepływ zadań między systemami za pomocą:

  • nagłówków zadania, którymi producenci opisują rodzaj wysyłanych danych
  • filtrów, którymi konsumenci określają rodzaj przyjmowanych danych.

Przykładowy kod producenta wygląda następująco:

from karton.core import Producer, Task, Resource
producer = Producer(identity="karton.sample-producer")

resource = Resource("file.txt", "lorem ipsum")
task = Task(
    headers={"type": "sample", "kind": "raw"},
    payload={
        "sample": resource,
        "additional_info": ["This sample has been added by simple producer example"]
    },
)
producer.send_task(task)

Tak zdefiniowany producent emituje zadanie z nagłówkami {"type": "sample", "kind": "raw"}. Oprócz tego zadanie ma dodatkowy ładunek w postaci pliku file.txt pod kluczem sample (którego zawartość będzie wymieniona za pośrednictwem S3) i klucza additional_info z listą dodatkowych informacji.

Zadanie może zostać odebrane przez następującego konsumenta:

import sys
from karton.core import Consumer, Task

class SampleConsumer(Consumer):
    identity = "karton.sample-consumer"
    filters = [
        {
            "type": "sample",
            "kind": "raw"
        },
    ]

    def process(self, task: Task):
        sample = task.get_resource("sample")
        additional_info = task.get_payload("additional_info")

if __name__ == "__main__":
    SampleConsumer.main()

Po uruchomieniu konsumenta, definiowana jest kolejka na którą przekazywane są wszelkie zadania, których nagłówki pasują do filtrów. Dla każdego odebranego zadania uruchamiana jest metoda process. Gdy przetwarzanie zostanie ukończone, zadanie oznaczane jest jako zakończone, a wszelkie powiązane zasoby usuwane wraz z samym zadaniem.

Opisanie przykładowego potoku nagłówkami i filtrami

Potok z poprzedniego diagramu można opisać następującymi nagłówkami i filtrami:

Diagram prostego potoku analitycznego z nagłówkami i filtrami

Przekładając karton.sandbox z powyższego diagramu na uproszczony kod wykorzystujący bibliotekę Karton, wyglądałoby to następująco:

from karton.core import Karton, Task

# Biblioteka dostarczająca interfejs API do rozwiązania Sandbox
import sandbox

class KartonSandbox(Karton):
    identity = "karton.sandbox"
    filters = [
        {"type": "sample"},
    ]

    def process(self, task: Task):
        # Pobranie pliku do wykonania
        sample = task.get_resource("sample")
        # Wysłanie go do rozwiązania Sandbox i uzyskanie rezultatu
        result = sandbox.run(sample)
        # Przekazanie rezultatu jako zadanie z nagłówkiem type: analysis
        output = Task(
            headers={"type": "analysis"},
            payload={
                "sample": sample,
                "memory_dumps": result.memory_dumps,
                "extra_files": result.extra_files,
            }
        )
        self.send_task(output)

Powyższy kod prezentuje w jaki sposób może wyglądać prosty skrypt Karton, który przyjmuje pliki i zwraca artefakty z analizy do dalszego przetwarzania. Zadania (task) są opisane typem danych wyjściowych danego komponentu. Pozwala to na łatwe podłączenie konsumenta, definiując za pomocą filtrów jaki typ danych jest oczekiwany na wejściu.

W ten sposób można w prosty sposób rozbudowywać potok analityczny.

Jak wymieniane są pliki? (Resource)

Karton wymienia zadania poprzez bazę danych Redis, która jest bazą "in memory", co oznacza, że wszystkie dane trzymane są w pamięci. W związku z tym baza nie nadaje się do przekazywania dużych plików. W związku z tym pliki przekazywane są poprzez S3 (np. Amazon S3, MinIO (end of life)) zaś referencje do obiektów śledzone są za pomocą obiektów Resource.

Po stronie producenta wystarczy stworzyć obiekt Resource. Karton automatycznie nadaje mu unikalny identyfikator (UUIDv4) i podczas wysyłania zadania do kolejki, zapisuje w S3 powiązane pliki.

resource = Resource("file.txt", "lorem ipsum")
task = Task(
    headers={"type": "sample", "kind": "raw"},
    payload={
        "sample": resource,
    },
)
producer.send_task(task)

Następnie po stronie konsumenta możemy zarówno pobrać załączony plik, jak i przekazać go dalej. W przypadku dalszego przekazania, przekazywana jest wyłącznie referencja i plik nie jest ponownie wysyłany. Resource stanowi alias na LocalResource reprezentujący lokalny plik, który w trakcie wysyłania konwertowany jest na referencję do obiektu S3: RemoteResource.

class KartonConsumer(Karton):
    identity = "karton.consumer"
    filters = [
        {
            "type": "sample",
            "kind": "raw"
        },
    ]

    def process(self, task: Task):
        # get_payload pozwala uzyskać dostęp do obiektu Resource (dokładniej RemoteResource)
        sample = task.get_payload("sample")
        # W przypadku gdy plik jest mały - można go pobrać bezpośrednio
        # do pamięci poprzez atrybut "content"
        content = sample.content
        # W przypadku gdy spodziewamy się dużego pliku, możemy pobrać go 
        # wpierw do pliku tymczasowego
        with sample.download_temporary_file() as f:
            # Odczytanie pierwszych 4KiB pliku
            content = f.read(4096)

Obiekty Resource są dokładniej opisane w dokumentacji na ReadTheDocs.

W momencie kiedy do pliku nie odnosi się już żadne nieukończone zadanie, plik jest usuwany z usługi S3 przez "garbage collector" będący częścią karton.system.

Zapisywanie rezultatów (reporter)

W poprzednim podrozdziale przedstawiono przykład potoku analitycznego, którego wyjściem jest konfiguracja złośliwego oprogramowania. Niestety w tak zdefiniowanym potoku, rezultat analizy zostanie usunięty, ponieważ nie ma zdefiniowanego żadnego konsumenta dla takiego zadania.

Karton jest bezstanowy - wszelkie zadania i zasoby wymieniane w systemie są z definicji ulotne. Za trwały zapis informacji odpowiadają konsumenci typu "reporter" (np. karton.mwdb-reporter), które odpowiadają za zwrotne zapisywanie wszelkich zmian w docelowej bazie danych.

Uzupełnienie potoku analitycznego o reporter

W ten sposób dane są zwrotnie zapisywane w bazie danych MWDB. Jest to rzeczywisty komponent, którego kod można znaleźć na Githubie: CERT-Polska/karton-mwdb-reporter

Jedynym trwałym elementem są kolejki konsumentów, które przyjmują zadania nawet jeśli wszyscy konsumenci są wyłączeni. Pozwala to np. zaktualizować konsumenta bez utraty dotychczas zakolejkowanych zadań, o ile nadal pasują do zadeklarowanego filtra. Aby trwale usunąć konsumenta z systemu, należy jawnie usunąć jego kolejkę np. poleceniem CLI karton delete.

Zrównoleglanie analizy

Często okazuje się, że statyczną konfigurację można pozyskać bezpośrednio z pliku, bez konieczności wykonywania go w sandboksie. Aby obsłużyć taki przypadek, możemy kierować pliki równolegle do sandboksa, jak i komponentu karton.config-extractor. W tym celu wystarczy dodać dodatkowy filtr do karton.config-extractor:

Kierowanie zadań równolegle do extractora i sandboksa

Na poziomie kodu karton.config-extractor filtry są zdefiniowane następująco:

class KartonConfigExtractor(Karton):
    identity = "karton.config-extractor"
    filters = [
        {"type": "sample"},
        {"type": "analysis"},
    ]

Tak zdefiniowany filtr oznacza, że zadanie musi mieć header type="sample" LUB type="analysis", aby być przekazanym do karton.config-extractor.

Każde zadanie może być przyjęte przez więcej niż jednego konsumenta. Zadanie przed umieszczeniem na kolejkach konsumentów jest kopiowane - każdy konsument otrzymuje swoją kopię zadania. Dzięki temu można równolegle skierować plik do analizy statycznej i do analizy dynamicznej, co pozwala na szybkie uzyskanie rezultatów.

Przetwarzanie plików w zależności od typu pliku

Do systemu MWDB wpadają pliki różnych typów: archiwa, wyeksportowane e-maile, skrypty, pliki wykonywalne itd. Każdy typ wymaga innego rodzaju przetwarzania: archiwa należy uprzednio wypakować, z e-maili pozyskać załączniki, zaś skrypty należy zawsze wpierw zdetonować w sandboksie.

W związku z tym pierwszym etapem przetwarzania jest klasyfikacja typu pliku, za pomocą komponentu karton.classifier. Typ pliku określają dodatkowe nagłówki kind, platform i extension.

{
    # Nagłówek określający, że zadanie stanowi plik
    "type":      "sample",

    # Określa, że plik ma rozpoznany typ
    "stage":     "recognized",

    # Typ pliku
    "kind":      "runnable"  # Plik wykonywalny, natywny dla danej platformy
              || "document"  # Dokument pakietu biurowego, mogący zawierać makra
              || "archive"   # Plik archiwum do wypakowania
              || "dump"      # Surowy zrzut pamięci z sandboksa
              || "script"    # Skrypt (JS/VBS/BAT/PHP/Python...)
              || "misc",     # Inny typ pliku (określony przez "extension")

    # Platforma docelowa dla pliku
    "platform":  "win32"     # Plik natywnie uruchamiany na 32-bitowym i 64-bitowym systemie Windows 
              || "win64"     # Plik natywnie uruchamiany wyłącznie na 64-bitowym systemie Windows
              || "linux" 
              || "android"
              || "macos"
              || "freebsd"
              || "netbsd"
              || "openbsd"
              || "solaris",

    # Rozszerzenie pliku (dokładny typ pliku)
    "extension": "*",        # Rozszerzenie pliku

    # Typ MIME pliku (rzadko używany w praktyce)
    "mime": "*",
}

Wejście karton.classifier stanowią zadania {"type: "sample", "kind": "raw"}, które są emitowane przez MWDB i określają plik, który wpierw trzeba sklasyfikować pod kątem typu zawartości.

Rozszerzony przykładowy potok analityczny o karton.classifier prezentuje się następująco:

Dodanie classifiera do potoku

Wszystkie pliki z karton.classifier są dodatkowo kierowane do karton.mwdb-reporter ponieważ classifier emituje również tag dla systemu MWDB. Na przykład dla pliku w języku VBScript będzie określony tagiem script:win32:vbs na podstawie dopasowania do następujących nagłówków:

{
    "type": "sample",
    "stage": "recognized",
    "kind": "script",       # Plik skryptowy
    "platform": "win32",    # Plik natywnie wykonywalny na 32/64-bitowym Windows przez wbudowany interpreter
    "extension": "vbs"      # Plik o rozszerzeniu VBS, w języku VBScript
}

Finalny potok MWDB

W ten sposób krok po kroku można rozbudować potok, aż do osiągnięcia takiej postaci:

Finalny potok z dokumentacji projektu Karton

Taki potok składa się z uniwersalnych elementów, które znajdują się w każdym systemie przetwarzającym złośliwe oprogramowanie. W związku z tym, razem z biblioteką Karton, udostępniamy również wszystkie podstawowe komponenty - dostarczając tym samym solidny fundament dla własnych systemów analitycznych, opartych na MWDB i Kartonie.

Udostępniamy takie komponenty jak:

Powyższe projekty definiują również pewien "standard" dla podstawowych nagłówków i konwencji używanych w systemie.

Aby ułatwić integrację podstawowych komponentów i podłączenie Kartona do MWDB, udostępniamy również projekt Karton Playground, który stanowi minimalne, zintegrowane środowisko oparte na Docker Compose.

Istnieje również wiele projektów stworzonych przez społeczność, które można zintegrować z własnym systemem:

Komponenty systemowe

Karton Dashboard - panel administracyjny systemu Karton

Karton Dashboard to niewielka aplikacja webowa oparta na frameworku Flask, która pozwala na inspekcję kolejek i zadań w systemie Karton. Jej kod jest dostępny w repozytorium karton-dashboard na Githubie.

Widok aplikacji Karton Dashboard

Główny widok aplikacji przedstawia stan zarejestrowanych kolejek konsumentów. W kolejnych kolumnach można znaleźć:

  • nazwę, wersję biblioteki Karton i wersję konsumenta,
  • listę filtrów,
  • liczbę oczekujących zadań,
  • liczbę zadań zakończonych błędem,
  • liczbę replik danego konsumenta.

Po wejściu w widok danej kolejki można zobaczyć również opis konsumenta i szczegóły oczekujących zadań

Szczegóły karton-classifier w Karton Dashboard

Zarówno wersja konsumenta jak i opis są deklarowane na poziomie kodu konsumenta. Na przykład główna klasa modułu karton-classifier wygląda następująco:

class Classifier(Karton):
    """
    File type classifier for the Karton framework.

    Entrypoint for samples. Classifies type of samples labeled as `kind: raw`,
    which makes them available for subsystems that receive samples with specific
    type only (e.g. `raw` => `runnable:win32:exe`)
    """

    identity = "karton.classifier"
    version = __version__
    filters = [
        {"type": "sample", "kind": "raw"},
    ]

Dokumentacja klasy jest przekładana na opis (description) i wyświetlana w formacie Markdown, natomiast wersja własna pobierana jest z pola version.

Karton Dashboard pozwala również zrestartować lub usunąć zadania zakończone błędem tj. podczas ich przetwarzania rzucony został nieobsłużony wyjątek. Dashboard pozwala zbadać szczegóły zadania i przyczynę błędu.

Przykład scrashowanego zadania

Bardzo użyteczną funkcją jest graf relacji między usługami. Jest on budowany na podstawie m.in. historii zadań jakie wymieniane były w systemie. Dla podstawowego środowiska Karton Playground, po przepuszczeniu paru plików przez system, graf wygląda następująco:

Graf Karton Dashboard

Natomiast graf naszego środowiska produkcyjnego prezentuje się następująco:

Graf Karton Dashboard dla środowiska produkcyjnego

Nazwy usług zostały ukryte, zaś niepołączone części grafu reprezentują niezależne systemy.

Najechanie na dany element grafu pokazuje wejścia i wyjścia danego węzła, co pozwala zbadać interakcje z danym elementem systemu.

Graf Karton Dashboard - szczegóły danego węzła

Karton System - broker zadań i garbage collector

Karton System stanowi najważniejszy element systemu. Jego kod jest częścią głównego repozytorium CERT-Polska/karton i znajduje się w katalogu karton/system.

Karton System ma dwa podstawowe zadania:

  • routing tasków, czyli ich pobieranie z kolejki wejściowej i kopiowanie na kolejki właściwych konsumentów według dopasowania do filtrów
  • garbage collection, czyli sprzątanie zakończonych zadań, powiązanych z zakończonymi zadaniami zasobów S3, a także usuwanie osieroconych zadań np. w wyniku niespodziewanego zamknięcia procesu.

Tak jak wszystkie elementy systemu Karton: Karton System również można skalować na wiele replik w zależności od obciążenia. Przy dużym przepływie zadań, pierwszą rzeczą jaką należy wykonać jest oddzielenie systemu wykonującego routing od systemu obsługującego garbage collection. Domyślnie Karton System robi to naprzemiennie co 3 minuty, jednakże wtedy na czas sprzątania wstrzymywane jest przekazywanie zadań.

Rozdzielenie routingu od garbage collection można wykonać poprzez:

  • uruchomienie jednej instancji z flagą --disable-gc (tylko router)
  • uruchomienie drugiej instancji z flagą --disable-router (tylko garbage collector)

Analogiczna konfiguracja znajduje się w Karton Playground, co można odczytać w pliku docker-compose.yml.

Rozwój projektu: kompatybilność, asyncio, Karton Gateway

Projekt Karton jest nieustannie rozwijany, zarówno przez CERT Polska, jak i dzięki pracy niezależnych kontrybutorów. Nieustannie adaptujemy projekt do coraz to nowszych zastosowań, co wiąże się z koniecznością wprowadzania nowych funkcji i poprawek.

Karton, tak jak większość projektów w CERT Polska jest wersjonowany semantycznie (SemVer). Główny numer wersji (major) jest podbijany w momencie zmian łamiących kompatybilność tj. gdy aktualizacja wymaga zmian w kodzie usługi Kartonowej, aby działała w taki sam sposób. Sam system utrzymuje jednak kompatybilność z wcześniejszymi wersjami, o ile brak kompatybilności nie jest jawnie odnotowany w liście zmian (changelogu).

Wsparcie dla asyncio

Wparcie dla asyncio wprowadzono w wersji 5.8.0 i wymaga Pythona 3.11+. Sposób użycia jest opisany dokładnie w dedykowanym rozdziale w dokumentacji ReadTheDocs. Głównym zastosowaniem są tzw. autoskalowalni konsumenci, którzy większość czasu czekają na zakończenie zadania zewnętrznego (np. wykonanie analizy w sandboksie) oraz producenci osadzeni w projektach opartych o asyncio.

Implementacja jest oznaczona jako eksperymentalna, ponieważ obejmuje na razie podzbiór API - brakuje m.in. narzędzi do testów, inspekcji stanu i hooków pre/post. Posiada również znane problemy np. zgłaszana liczba replik może być niedokładna (framework szacuje ją na podstawie liczby połączeń do Redisa). Jest to jednak implementacja sprawdzona produkcyjnie - używamy jej z powodzeniem w mwdb.cert.pl.

Karton Gateway

Dużym ograniczeniem Kartona jest to, że wszystkie operacje są wykonywane bezpośrednio na bazie Redis. Karton od początku był projektowany jako "prosty wrapper na Redisa i S3", ale taki zamysł projektowy wiąże się z dwoma konsekwencjami:

  • ograniczone bezpieczeństwo: każda usługa ma pełny dostęp do bazy Redis i nie da się zapewnić usługom minimalnych uprawnień
  • ograniczone możliwości zmian: kompatybilność musi być zachowana na poziomie kluczy i operacji Redis, co utrudnia wykonywanie dużych zmian poprawiających wydajność i dodających nowe funkcje.

W związku z tym trwają prace nad dodatkowym komponentem Karton Gateway, który udostępniałby operacje na kolejkach za pośrednictwem dobrze zdefiniowanych protokołów HTTP REST API i WebSocket. Szczegóły opisane są w issue 301 na Githubie.

FAQ

Czy mogę uruchomić własną instalację Kartona?

Tak. Karton jest oprogramowaniem otwartoźródłowym udostępnianym na licencji BSD-3-Clause, a instalacja biblioteki sprowadza się do pip install karton-core. Do działania potrzebny jest jeszcze serwer Redis (kolejka zadań) oraz magazyn obiektów zgodny z API S3 (np. Amazon S3 lub MinIO) do wymiany plików. W repozytorium znajdziesz minimalne środowisko Docker Compose (katalog dev/), a projekt Karton Playground dostarcza zintegrowane środowisko z MWDB Core i z zestawem gotowych komponentów. Szczegóły konfiguracji opisuje dokumentacja.

Czy Karton nadaje się wyłącznie do analizy malware?

Nie tylko! Co prawda powstał z myślą o analizie złośliwego oprogramowania i stanowi fundament serwisu mwdb.cert.pl, ale jest to ogólny framework do łączenia skryptów analitycznych w architekturze mikroserwisowej. W CERT Polska jest wykorzystywany również poza analizą malware - np. jako podstawa modułowego skanera Artemis. Sprawdza się w każdym projekcie opartym o wymianę zadań między niezależnymi komponentami.

Czy komponenty Kartona muszą być pisane w Pythonie?

Oficjalna biblioteka (karton-core) oraz wszystkie udostępniane przez nas komponenty są napisane w Pythonie i nie udostępniamy bibliotek dla innych języków. Technicznie sama komunikacja opiera się na standardowych protokołach - Redis do przekazywania zadań i S3 do wymiany plików, jednak wymagałoby to samodzielnej reimplementacji protokołu Kartona (cykl życia zadań, dopasowywanie nagłówków do filtrów, garbage collection zasobów).

Skąd nazwa Karton?

W cyberbezpieczeństwie wiele komponentów w złożonych systemach określanych jest mianem "pudełek". Wraz z wieloletnią ewolucją naszych systemów, w CERT Polska mieliśmy mnóstwo komponentów, które ściśle ze sobą współpracowały, ale każdy na swój unikalny sposób. Pojawił się więc pomysł, żeby opakować te "pudełka" w Karton - zunifikowaną warstwę, która pozwoli je łatwiej ze sobą połączyć.

Co się dzieje, gdy konsument ulegnie awarii lub jest wyłączony?

Kolejki konsumentów są trwałe - przyjmują zadania nawet wtedy, gdy wszystkie instancje danego konsumenta są wyłączone. Dzięki temu analiza zostanie dokończona po ponownym uruchomieniu usługi, a aktualizacja komponentu nie powoduje utraty zakolejkowanych zadań (o ile nadal pasują do zadeklarowanego filtra). Zadania, które zakończą się nieobsługiwanym wyjątkiem, trafiają w stan Crashed i są utrzymywane przez określony czas (domyślnie 72 godziny), co umożliwia ich inspekcję i ponowienie po naprawieniu błędu. Jest to możliwe z poziomu Karton Dashboard. Do bezpiecznego testowania nowych wersji komponentów na środowisku produkcyjnym służy tryb "debug" - nadaje on usłudze losowy sufiks, wyłącza trwałość kolejki i nie przekazuje logów do kanału pub-sub.

Czy Karton obsługuje asynchroniczność (asyncio)?

Tak, ale jest jeszcze eksperymentalne. Więcej o wsparciu asyncio można znaleźć w sekcji Wsparcie dla asyncio.

Jak zgłosić błąd lub pomysł do projektu?

Zgłoszenia dotyczące samego frameworka Karton można kierować w sekcji Issues repozytorium CERT-Polska/karton. W sprawach serwisu mwdb.cert.pl lub analiz prowadzonych przez CERT Polska prosimy o kontakt pod adresem info@cert.pl.

Ostatnia aktualizacja artykułu: 2026-09-01.