Autor: Dawid Pągowski

  • sqlpackage

    sqlpackage

    sqlpackage to świetne narzędzie do m.in. eksportu czy importu baz danych z serwerów Microsoft SQL Server (albo Azure).

    Eksport

    Przykładowy eksport (dane+schema):

    sqlpackage /Action:Export \
    /SSN:host.database.windows.net,1445 \
    /SU:uzytkownik \
    /SP:haslo \
    /SDN:nazwa-bazy-na-serwie \
    /TF:plik-kopii-zapasowej.bacpac

    Port możemy dodać po przecinku, tak jak w powyższym przykładzie.

    Nazwy SSN, SU, SP itd. to skróty od pełnych nazw:

    • SSN – SourceServerName
    • SU – SourceUser
    • SP – SourcePassword
    • SDN – SourceDatabaseName
    • TF – TargetFile

    Import

    Import budujemy tak samo, tylko zamieniamy Source<->Target:

    sqlpackage /Action:Import \
    /TSN:host.database.windows.net,1445 \
    /TU:uzytkownik \
    /TP:haslo \
    /TDN:nazwa-bazy-na-serwie \
    /SF:plik-kopii-zapasowej.bacpac

    Gdzie: TSN <=> TargetServerName itd…

    Użyteczne przełączniki

    Poniżej lista przełączników, które kiedyś mi się przydały:

    /TargetTrustServerCertificate:[True|False] - pomijanie weryfikacji certyfikatu serwera docelowego (np. przy imporcie)
    /SourceTrustServerCertificate:[True|False] - pomijanie weryfikacji certyfikatu serwera źródłowego (np. przy eksporcie)
    /Diagnostics:[True|False] - włączenie szczegółowych logów
    /DiagnosticsFile:<ścieżka> - włączenie szczegółowych logów, do pliku
    /OutputPath:ściezka - definiuje, gdzie mają być wyplute pliki (domyślnie do katalogu roboczego)

    Instalacja

    Instrukcje instalacji na różnych systemach/dystro:

    ArchLinux

    Narzędzie dostępne w AURze: https://aur.archlinux.org/packages/sqlpackage

    dotnet (multiplatformowe)

    Jako globalne narzędzie:

    dotnet tool install -g microsoft.sqlpackage

    Aktualizacja:

    dotnet tool update -g microsoft.sqlpackage

    Dodatkowe akcje

    sqlpackage obsługuje jeszcze 5 dodatkowych akcji, których jeszcze nie używałem (zaktualizuję artykuł, jak mi się zdarzy):

    • Extract – eksport samego schema, bez danych, plik dacpac
    • Publish – inkrementalne aktualizowanie schema bazy danych bazując na pliku dacpac
    • DeployReport – tworzy raport ze zmianami, które wdrożyłaby akcja „Publish”
    • DriftReport – tworzy raport ze zmianami, które zostały wprowadzone od ostatniej „rejestracji” bazy
    • Script – buduje skrypt, który inkrementalnie aktualizuje bazę danych do docelowego schema

    Dokumentacja

    Wszystkie akcje i przełączniki są opisane na MSDN.

  • Włam do maszyny CNC

    Włam do maszyny CNC

    Kilka miesięcy temu dostałem obraz dysku giętarki Dynobend z prośbą o uruchomienie go i/lub odzyskanie haseł. Wiedziałem tylko tyle, że zainstalowany na maszynie system to Windows XP.

    Odtwarzanie obrazu

    Początkowym wyzwaniem było samo odtworzenie obrazu dysku. Nie wiedziałem jakim narzędziem była robiona kopia, ale po krótkim researchu doszedłem do wniosku, że było to oprogramowanie Paragonu.

    Zrzut ekranu: Pliki kopii zapasowej dysku maszyny stworzonej oprogramowaniem Paragonu
    Pliki kopii zapasowej dysku maszyny stworzonej oprogramowaniem Paragonu

    Zanim zacząłem odtwarzać kopię, dodałem kolejny dysk wirtualny do maszyny

    Zrzut ekranu: Dodawanie dysku do maszyny wirtualnej o rozmiarze 160 GB
    Dodawanie dysku do maszyny wirtualnej o rozmiarze 160 GB

    Po uzyskaniu dostępu do programu, odtworzyłem kopię jako kopię całego dysku (można też jako kopie poszczególnych woluminów, ale to utrudniłoby uruchomienie systemu).

    Zrzut ekranu: Wybór obrazu dysku z otrzymanej kopii .PBF
    Wybór obrazu dysku z otrzymanej kopii .PBF
    Zrzut ekranu: Wybór dysku docelowego i potwierdzenie rozpoczęcia operacji odtwarzania
    Wybór dysku docelowego i potwierdzenie rozpoczęcia operacji odtwarzania

    Po zakończeniu odtwarzania pojawiły się woluminy w przystawce „Zarządzanie dyskami”

    Zrzut ekranu: Pokazane dyski w maszynie wirtualnej. Widoczny "Dysk 1", na którym została odtworzona kopia zapasowa PBF
    Pokazane dyski w maszynie wirtualnej. Widoczny „Dysk 1”, na którym została odtworzona kopia zapasowa PBF

    Zostało mi tylko stworzyć nową maszynę wirtualną, spróbować uruchomić odtworzony system i trzymać kciuki.

    Walka z bluescreenem

    Windows XP jest zdecydowanie mniej przenośny, niż nowsze wersje systemu od Microsoftu. Oto jak zakończyła się moja pierwsza próba odpalenia systemu:

    Zrzut ekranu: Widoczny BSOD systemu Windows XP
    Widoczny BSOD systemu Windows XP

    Trzymanie kciuków mało co mi dało, jak widać. Na powyższym zrzucie ekranu tego nie widać, ale kod błędu to był sławny

    ☠️ 0x0000007B(INACCESSIBLE BOOT DEVICE) ☠️

    Co oznacza problem z zamontowaniem woluminu systemowego. Streszczę tutaj mój długi proces walki z tym błędem – próbowałem wyłączać różne sterowniki, modyfikować różne wartości w rejestrze, ale nic nie działało.

    Sukces

    W końcu wpadłem na pomysł spróbowania trybu naprawy z angielskiego instalatora Windowsa XP (w polskiej wersji tego nie ma z jakiegoś powodu).

    Zrzut ekranu: Początkowy etap instalatora Windowsa XP
    Początkowy etap instalatora Windowsa XP

    Na tym etapie można wybrać albo czystą instalację (klawisz ESC), albo naprawę. Wskazałem mój odtworzony system i nacisnąłem klawisz R.

    Zrzut ekranu: Pierwszy etap naprawy (pasek instalacyjny)
    Pierwszy etap naprawy

    Naprawa wygląda praktycznie tak samo jak instalacja świeżego systemu, przy czym wiele rzeczy jest zachowane. Pliki, użytkownicy i inne rzeczy zostają nietknięte.

    Po uruchomieniu ponownym system się uruchomił i rozpoczął się kolejny etap naprawy:

    Zrzut ekranu: Proces naprawy, widoczny monit o podanie klucza produktu
    Proces naprawy, widoczny monit o podanie klucza produktu

    Jakoś tak wyszło, że nie miałem klucza Windowsa XP SP3. Na szczęście w sieci dostępne są klucze, które można tutaj wpisać, żeby popchnąć instalację (naprawę) dalej: link do listy na GH.

    Zrzut ekranu: Uruchamianie się naprawionego Windowsa XP
    Uruchamianie się naprawionego Windowsa XP

    Po wpisaniu klucza system uruchomił się ponownie, tym razem bez BSODa! Jednak nie jest to koniec:

    Zrzut ekranu: Ekran logowania, automatycznie uzupełniona nazwa: operator
    Ekran logowania, automatycznie uzupełniona nazwa: operator

    Odzyskiwanie haseł

    Najprościej byłoby usunąć hasła z użytkowników, ale to wiązałoby się z modyfikacją oryginalnego systemu – do którego nie miałem dostępu.

    Uruchomiłem BlackArcha na maszynie i zamontowałem partycję systemową:

    Zrzut ekranu: Zamontowanie partycji systemowej w BlackArchu
    Zamontowanie partycji systemowej w BlackArchu

    Po zamontowaniu partycji zdumpowałem hashe haseł z rejestru przy użyciu narzędzia samdump2.

    Zrzut ekranu: Dumpowanie hashy (samdump2)
    Dumpowanie hashy

    Oto lista z powyższego screenshota, w formie tekstowej :):

    Administrator:500:42008b7e0dfe8456aad3b435b51404ee:8666cdaf3d0337cbbae422f9da6f0f52:::
    Admin:1004:42008b7e0dfe8456aad3b435b51404ee:8666cdaf3d0337cbbae422f9da6f0f52:::
    Operator:1005:7c3abc11eef22af54a3b108f3fa6cb6d:e001d7577b432fe8e745deb408cc8a62:::
    Engineer:1006:5de00f0076f9b0d64a3b108f3fa6cb6d:e907f2dd1d5d1a7f6ed20a5a44a0dd11:::
    Remote:1007:b1293b46ba30174aaad3b435b51404ee:a2fb7f991c8960bdae62b6cb12b87d56:::
    DynoBend:1008:7c3abc11eef22af54a3b108f3fa6cb6d:e001d7577b432fe8e745deb408cc8a62:::

    Wyciąłem wyłączone konta Guest, Support oraz HelpAssistant. Widać, że Guest i Support mają puste hasła (ich hashe odpowiadają pustym ciągom) a hasła HelpAssistant nigdy nie odzyskałem.

    Widać również, że niektóre konta używają tych samych haseł:

    • Administrator i Admin
    • Operator i DynoBend

    Po czym to widać? Te konta mają te same hashe!

    Łamanie hashy przy użyciu CrackStation

    Pierwsze co spróbowałem to wklejenie hashy LM (prostszych do złamania) do online-owych crackerów. Mają one duże bazy danych, które posiadają obliczone hashe różnego typu dla różnych ciągów znaków. Jeżeli hasła były krótkie, to taka baza prawdopodobnie zawiera nasz hash.

    Wpisałem hashe do CrackStation:

    Zrzut ekranu: Hashe wklejone w CrackStation
    Hashe wklejone w CrackStation

    Okazało się, że dwa hasła udało się odzyskać już na tym etapie:

    Zrzut ekranu: Kawałek zwróconej listy CrackStation. Widoczny złamany hash i jeden nie znaleziony.
    Kawałek zwróconej listy CrackStation. Widoczny złamany hash i jeden nie znaleziony.

    Pierwszym hasłem było dla kont Admin i Administrator :P. Hasło składało się wyłącznie z cyfr: „8507730„.

    Drugim hasłem było dla konta „Remote”, które miało unikalne hasło:

    Zrzut ekranu: Widoczna lista dopasowanych hashy LM dla Remote
    Widoczna lista dopasowanych hashy LM dla Remote

    Kolor zielony oznacza dokładnie odwzorowanie. Ale czemu jest ich kilka? Hashe LM są takie same dla różnych kombinacji wielkości liter. Przykładowo, hash dla „abc” jest taki sam jak dla „ABC”.

    Za to, hashe NTLM (które również są w dumpie) już takie nie są. Okazało się, że dla konta Remote nawet odwzorowanie NTLM znajduje się w CrackStation:

    Zrzut ekranu: Rezultat crackowania hasha NTLM dla "Remote". Widoczne dokładnie odwzorowanie
    Rezultat crackowania hasha NTLM dla „Remote”. Widoczne dokładnie odwzorowanie

    Okazało się, że hasło dla „Remote” to… „Remote”.

    Łamanie hashcatem

    W bazie CrackStation nie było hashy dla kont „DynoBend” i „Operator”. Użyłem hashcata do szybkiego sprawdzenia hashy LM dla pozostałych kont:

    Zrzut ekranu: Uruchomienie hashcata
    Uruchomienie hashcata
    hashcat --potfile-disable -m 3000 -a 3 hashe.txt -1 ?l?u ?1?1?1?1?1?1?1

    Gdzie:

    • --potfile-disable – wyłącznie pliku przechowywania rezultatów (w ramach testów i zrobienia SS)
    • -m 3000 – tryb łamania hashy LM
    • -a 3 – tryb bruteforce (kombinacje znaków)
    • -1 ?l?u – zdefiniowanie customowego zestawu znaków (małe i duże litery)
    • ?1?1?1… – maska do bruteforce-a, 7 liter

    Po jakichś dwóch sekundach hashcat znalazł dwa kolejne (niepełne) hasła:

    Zrzut ekranu: Rezultat crackowania hashcatem. Widoczne odwzorowanie DYNOBEN i DNEBONY
    Rezultat crackowania hashcatem

    Więc dwa kolejne hasła zaczynają się od wariancji ciągu „DYNOBEN” i „DNEBONY”.

    Szukanie dokładnych haseł

    Hashe LM z dumpa to tak naprawdę dwa oddzielne hashe. Przykładowo dla konta DynoBend, ciąg LM skopiowany prosto z dumpa:

    7c3abc11eef22af54a3b108f3fa6cb6d

    Powyższy ciąg znaków to dwa hashe LM:

    7c3abc11eef22af5 4a3b108f3fa6cb6d

    Pierwszy podciąg to hash pierwszych 7 liter hasła, a drugi to hash kolejnych 7 liter hasła, więc maksymalna długość hasła dla hashy LM to 14 znaków.

    DynoBend i Operator mają ten sam hash (powyższy), a co z Engineer?:

    7c3abc11eef22af5 4a3b108f3fa6cb6d # DynoBend i Operator
    5de00f0076f9b0d6 4a3b108f3fa6cb6d # Engineer

    Pierwsze podciągi są różne (ale ich odwzorowanie znalazł hashcat). Widać jednak, że drugie podciagi są takie same!

    Oznacza to, że hasła dla tych kont to przykładowo (drugie 7 liter są takie same dla kont):

    DYNOBENxxxx # DynoBend i Operator
    AAAAAAAxxxx # Engineer

    Po wklejeniu drugiego podciągu do CrackStation okazało się, że hash „4a3b108f3fa6cb6d” to najprawdopodobniej hash litery „d”:

    Zrzut ekranu: Rezultat crackowania hasha 4a3b108f3fa6cb6d
    Rezultat crackowania hasha 4a3b108f3fa6cb6d

    Czyli nasze „xxxx” to „d”, a hasła to wariancje „DNEBONYD” oraz „DYNOBEND”.

    Teraz wystarczyło zrobić słownik takich wariancji: DNEBONYD, dnebonyd, Dnebonyd itd. itd.

    Albo po prostu zgadnąć ręcznie w CyberChefie:

    Zrzut ekranu: Ręczny wpis w CyberChefie
    Ręczny wpis w CyberChefie

    Po dodaniu „NT Hash” do receptury i wpisaniu „dnebonyd” (małymi literami), CyberChef wypluł dokładnie taki sam hash NTLM, jaki był w dumpie. Oznacza to, że znalazłem hasło dla użytkownika „Engineer”.

    Podobnie postąpiłem dla DynoBend i Operator: ich hasła to „Dynobend”.

    Próba logowania jako operator

    Zrzut ekranu: Wpisywanie hasła "Dynobend" dla konta "operator"
    Wpisywanie hasła „Dynobend” dla konta „operator”

    Okazało się, że… działa!

    Zrzut ekranu: Ekran po zalogowaniu do systemu jako Operator. Uruchamiająca się aplikacja giętarki
    Ekran po zalogowaniu do systemu jako Operator. Uruchamiająca się aplikacja giętarki

    Podsumowanie

    No i nawet się udało!!

    Po naprawie systemu za pomocą instalatora Windowsa XP, zdumpowałem hashe za pomocą BlackArcha (samdump2) i zcrackowałem je przy użyciu CrackStation oraz hashcata.

  • Magnetometr QMC5883L

    Magnetometr QMC5883L

    QMC5883L to układ mierzący natężenie pola magnetycznego w 3 osiach, który umożliwia określenie orientacji (np. względem magnetycznej północy).

    Kupiłem go na Allegro, myśląc że dostanę HMC5883L (taki napis widniał na płytce). Okazało się, że dostałem odrobinę inny układ: QMC5883L. Jest on niekompatybilnym zamiennikiem, ze względu na małe różnice w rejestrach I2C.

    Podłączenie do STM32

    Jednym z wyzwań było samo podłączenie układu do mikrokontrolera.

    Wybrałem I2C1, pin SCL podłączyłem do PB8, a pin SDA do PB7 a prędkość GPIO ustawiłem na Bardzo szybką.

    Połączenie I2C wymaga rezystorów pull-up, których płytka z układem nie posiadała. Spróbowałem użyć wbudowanych pull-upów w mój STM32U083MC, ale niestety połączenie często się rozrywało. Zdecydowałem się zbudować podłączyć własne pull-upy.

    Ostatecznie, kod konfiguracyjny wygląda tak:

    /**
      * @brief I2C MSP Initialization
      * @param hi2c: I2C handle pointer
      * @retval None
      */
    void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
    {
      GPIO_InitTypeDef GPIO_InitStruct = {0};
      RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
      if(hi2c->Instance==I2C1)
      {
      /** Initializes the peripherals clocks
      */
        PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_I2C1;
        PeriphClkInit.I2c1ClockSelection = RCC_I2C1CLKSOURCE_PCLK1;
        if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
        {
          Error_Handler();
        }
    
        __HAL_RCC_GPIOB_CLK_ENABLE();
        /**I2C1 GPIO Configuration
        PB7     ------> I2C1_SDA
        PB8     ------> I2C1_SCL
        */
        GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_8;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
        /* Peripheral clock enable */
        __HAL_RCC_I2C1_CLK_ENABLE();
      }
    }

    Pull-upy

    Pod ręką miałem rezystory 2,2 kΩ (choć słyszałem, że lepsze byłyby 4,7 kΩ), dlatego takich użyłem. Próbowałem podłączyć moduł z kontrolerem przez płytkę stykową, ale najwyraźniej rezystory słabo łączyły. Ulepiłem połączenie z kabli, podłączając pull-upy w środku przewodów do VCC (3.3V).

    Zdjęcie przedstawiające połączenie I2C kontrolera STM32 z QMC5883L. W środku przewodów podłączone są rezystory do napięcia 3.3V.
    Zdjęcie przedstawiające połączenie I2C

    Po ulepieniu czegoś takiego, połączenie działało zadowalająco, nawet z I2C w trybie szybkim (400 kHz).

    Azymut, kalibracja i deklinacja

    QMC5883L nie daje nam gotowego azymutu, a wartość natężenia pola magnetycznego w trzech osiach. Azymut możemy obliczyć używając funkcji arcus tangens:

    float QMC5883L_CalculateAzimuth(
    const QMC5883L_Handle_t* hqmc, 
    QMC5883L_Data_t* data)
    {
        float azimuth = atan2f(data->y, data->x) * 180.0f / M_PI;
        
        // Uwzględnij korekcję wynikającą z deklinacji
        azimuth += hqmc->declination;
        
        // Normalizuj do zakresu 0-360
        // (zamiast -180 - 180)
        if (azimuth < 0) {
            azimuth += 360.0f;
        } else if (azimuth >= 360.0f) {
            azimuth -= 360.0f;
        }
        
        return azimuth;
    }
    

    Gdzie podziała się oś Z? Powyższa implementacja zakłada, że układ jest położony idealnie płasko. Nie miałem zamontowanego akcelerometru, którym mógłbym dodatkowo zmierzyć odchylenie układu. Z tego powodu nie stworzyłem implementacji, która uwzględniałaby oś Z (może jak zamontuję, to zaktualizuję post).

    Niestety, bez kalibracji i korekcji wynikającej z deklinacji obliczony azymut będzie w najlepszym wypadku niedokładny – w moim kompletnie mijał się z prawdą.

    Deklinację w stopniach możemy łatwo pozyskać z kalkulatorów online, np. tego od amerykańskiego NCEI.

    Kalibracja QMC5883L

    Skalibrowałem mój moduł obracając go we wszystkich osiach: X, Y oraz Z, jednocześnie notując minimalne i maksymalne wartości w każdej osi.

    Pozwoliło mi to na obliczenie dwóch korekt: hard-iron oraz soft-iron. Więcej o tych korektach możesz przeczytać tutaj. W skrócie:

    • hard-iron: korekta offsetowa, dodawana do odczytanej wartości
    • soft-iron: korekta iloczynowa – mnożymy odczytaną wartość przez korektę

    A to fragmenty kodu odpowiadające za kalibrację:

    /**
     * @brief Dodaje próbkę kalibracyjną
     */
    HAL_StatusTypeDef QMC5883L_AddCalibrationSample(
    const QMC5883L_Handle_t* hqmc, 
    QMC5883L_CalibrationData_t* cal_data)
    {
        QMC5883L_RawData_t raw_data;
        HAL_StatusTypeDef status;
        
        status = QMC5883L_ReadRaw(hqmc, &raw_data);
        if (status != HAL_OK) return status;
        
        // Konwertuj na Gaussa
        float x = (float)raw_data.x / hqmc->scale_factor;
        float y = (float)raw_data.y / hqmc->scale_factor;
        float z = (float)raw_data.z / hqmc->scale_factor;
        
        if (x < cal_data->x_min) cal_data->x_min = x;
        if (x > cal_data->x_max) cal_data->x_max = x;
        if (y < cal_data->y_min) cal_data->y_min = y;
        if (y > cal_data->y_max) cal_data->y_max = y;
        if (z < cal_data->z_min) cal_data->z_min = z;
        if (z > cal_data->z_max) cal_data->z_max = z;
        
        cal_data->sample_count++;
        
        return HAL_OK;
    }
    /**
     * @brief Kończy kalibrację i oblicza korekty
     */
    HAL_StatusTypeDef QMC5883L_FinishCalibration(
    QMC5883L_Handle_t* hqmc, 
    QMC5883L_CalibrationData_t* cal_data)
    {
        if (cal_data->sample_count < 100) {
            // Za mało próbek
            return HAL_ERROR;
        }
        
        // Oblicz korektę hard-iron
        hqmc->calibration.x_offset = (cal_data->x_max + cal_data->x_min) / 2.0f;
        hqmc->calibration.y_offset = (cal_data->y_max + cal_data->y_min) / 2.0f;
        hqmc->calibration.z_offset = (cal_data->z_max + cal_data->z_min) / 2.0f;
        
        // Oblicz korektę soft-iron
        float x_range = cal_data->x_max - cal_data->x_min;
        float y_range = cal_data->y_max - cal_data->y_min;
        float z_range = cal_data->z_max - cal_data->z_min;
        
        float avg_range = (x_range + y_range + z_range) / 3.0f;
        
        hqmc->calibration.x_scale = avg_range / x_range;
        hqmc->calibration.y_scale = avg_range / y_range;
        hqmc->calibration.z_scale = avg_range / z_range;
        
        hqmc->calibration.is_calibrated = true;
        
        return HAL_OK;
    }
    /**
     * @brief Zaaplikuj dane kalibracyjne do odczytanych danych
     */
    void QMC5883L_ApplyCalibration(
    const QMC5883L_Handle_t* hqmc, 
    QMC5883L_Data_t* data)
    {
        if (!hqmc->calibration.is_calibrated) return;
        
        // Aplikuj korekcję hard-iron
        data->x -= hqmc->calibration.x_offset;
        data->y -= hqmc->calibration.y_offset;
        data->z -= hqmc->calibration.z_offset;
        
        // Aplikuj korekcję soft-iron
        data->x *= hqmc->calibration.x_scale;
        data->y *= hqmc->calibration.y_scale;
        data->z *= hqmc->calibration.z_scale;
    }
    

    Niezbyt idealny wynik

    Po całej tej kalibracji i obliczeniach skierowałem oś X modułu QMC5883L na północ, wtedy mój azymut powinien wynosić 0 stopni. Ku mojemu zdziwieniu odczyt był całkiem dokładny (wahał się o 2, 3 stopnie). Obracając moduł, dalej dawał zadowalające wyniki.

    Zatem czemu „niezbyt” idealny wynik? Gdy przymocowałem magnetometr do mojego samochodzika, który był blisko podłogi, odczyt nagle stał się niedokładny. Rozbiegał się o blisko 15, czasami 20 stopni. Okazało się, że coś w podłodze zakłóca pole magnetyczne Ziemi, przez co odczyt jest błędny.

    Dodatkowo, na odczyt wpływa również temperatura, którą pominąłem w obliczeniach azymutu (moduł ma funkcję pomiaru temperatury).

    Problemy z komunikacją

    Oprócz niedokładnego pomiaru wystąpiły również problemy z samym modułem QMC5883L.

    Gdy odczytywałem moduł zbyt często (biorąc pod uwagę stan pinu gotowości danych), przestawał on odczytywać natężenie. Ciągle zwracał te same wartości (sama komunikacja przez magistralę I2C działała poprawnie).

    W związku z tym, odczytywałem dane z QMC5883L co 50 milisekund:

    // [...]
    while (1)
    {
    	  if (HAL_GetTick() >= next_read_tick && g_qmc_drdy) {
    		  next_read_tick = HAL_GetTick() + 50;
    		  if (QMC5883L_AppReadData()) {
    			  // Jeśli QMC nadal utrzymuje wysoki poziom pinu lub operacja ponownego odczytu była zbyt szybka, aby EXTI mogło się uruchomić,
            // to po prostu ponownie odczytaj rejestry QMC (w przeciwnym razie g_qmc_drdy pozostanie fałszywe, podczas gdy pin będzie miał wysoki poziom).
    			  g_qmc_drdy = HAL_GPIO_ReadPin(QMC_DRDY_GPIO_Port, QMC_DRDY_Pin);
    		  }
    		  else {
    			  printf("i2c error\r\n");
    			  uint32_t err = HAL_I2C_GetError(&hi2c1);
    			  printf("error code: %lu\r\n", err);
    		  }
    	  }
    }
    // [...]

    Tak czy inaczej, spróbuję ulepić lepsze pull-upy i dam update, czy to pomogło.

    Podsumowanie

    Wdrożenie magnetometru QMC5883L okazało się ciekawym wyzwaniem, obfitującym w szereg praktycznych problemów i naukę. Pomimo początkowych trudności, udało mi się osiągnąć w miarę działający system.

    Moje wnioski:

    1. Zamienniki: Kupiony moduł, oznaczony jako HMC5883L, w rzeczywistości zawierał układ QMC5883L, który jest niekompatybilny na poziomie rejestrów, co wymusiło samodzielną implementację sterownika.
    2. Fizyczne podłączenie: Stabilna praca I2C jest niemożliwa bez odpowiednich rezystorów pull-up. Wbudowane w STM32 okazały się niewystarczające, a dopiero dodanie zewnętrznych rezystorów 2,2 kΩ zapewniło względnie stabilną komunikację nawet przy 400 kHz.
    3. Kalibracja jest niezbędna: Samo odczytanie surowych wartości z osi X, Y i Z jest niewystarczające. Aby uzyskać wiarygodny azymut, konieczne było przeprowadzenie kalibracji (usuwającej zakłócenia hard-iron i soft-iron) oraz uwzględnienie lokalnej deklinacji magnetycznej.
    4. Środowisko ma ogromne znaczenie: Nawet doskonała kalibracja może zostać unieważniona przez zewnętrzne zakłócenia magnetyczne (np. zbrojenie w podłodze), które znacząco wpływają na dokładność odczytu.
    5. Opóźnienie odczytu: Układ wymagał ostrożnego zarządzania czasem odczytu. Zbyt częste pollowanie prowadziło do „zawieszenia” się czujnika, dlatego konieczne było wprowadzenie opóźnień i ręczny odczyt pinu DRDY w połączeniu z obsługą przerwania EXTI.
  • The Game – TryHackMe (PL)

    The Game – TryHackMe (PL)

    „The Game” na TryHackMe to proste zadanie polegające na zhakowaniu kopii Tetrisa zrobionej na silniku Godot. Niestety, dostępna jest jedynie wersja na Windowsa.

    Jeżeli nigdy wcześniej nie miałeś styczności z cheatowaniem, czy też hakowaniem gier, to jest to świetny pokój na początek :)

    Okno kopii Tetrisa z pokoju The Game na TryHackMe
    Zrzut ekranu przedstawiający okno gry

    Znalazłem dwa banalne rozwiązania.

    Rozwiązanie klasyczne

    Pierwsze co rzuciło mi się w oczy to cel gry: Score more than 999 999 – „Zdobądź więcej niż 999 999 punktów”. Używając (chyba) najbardziej znanego edytora pamięci, jakim jest Cheat Engine, przyznałem sobie adekwatną ilość punktów.

    Wybacz moje okropne umiejętności grania w Tetrisa :(

    Na początku przeskanowałem pamięć procesu, żeby znaleźć wszystkie 32-bitowe (4-bajtowe – rozmiar zgadywałem) wartości równe 0.

    Po zdobyciu 100 punktów, ponownie przeszukałem pamięć. Na tym etapie Cheat Engine nie przeszukuje całej pamięci, tylko sprawdza, które znalezione wcześniej adresy mają wartość, którą wskazaliśmy.

    Na końcu został mi jeden adres, mający wartość 200, którą zmieniłem na 999 999. Kolejne 100 punktów przeniosło mnie nad próg wymagany przez grę, dzięki czemu dostałem flagę.

    Rozwiązanie „meta”

    Znaczna większość flag w pokojach na TryHackMe (pewnie jakieś 99,99%) jest w następującym formacie:

    THM{....................}

    Zakładając, że autor gry nie zaszyfrował flagi, możemy przeskanować pamięć aplikacji właśnie pod kątem ciągu znaków. Na szczęście Cheat Engine to obsługuje:

    Zrzut ekranu przedstawiający okno gry i programu Cheat Engine. Wyszukany string THM{.
    Zrzut ekranu przedstawiający okno gry i programu Cheat Engine. Wyszukany string THM{.

    Po ustawieniu kodowania na UTF-16, Cheat Engine znalazł dwa miejsca, w których znajduje się ciąg znaków o przedrostku THM{.

    Wyświetlając region pamięci, w którym znajduje się którykolwiek z adresów, możemy odczytać całą flagę – bez grania w Tetrisa:

    Zrzut ekranu przedstawiający opcję, która umożliwia wyświetlenie region pamięci wokół adresu.
    Zrzut ekranu przedstawiający opcję, która umożliwia wyświetlenie region pamięci wokół adresu.
    Zrzut ekranu przedstawiający region pamięci i ocenzurowaną flagę.
    Zrzut ekranu przedstawiający region pamięci i ocenzurowaną flagę.
  • 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).