Fuzzowanie programem AFL - jak szukać podatności w oprogramowaniu ICS
Fuzzowanie (fuzzing) programem AFL/AFL++ - technika poszukiwania podatności w oprogramowaniu ICS. Przykład analizy biblioteki DLMS do odczytu liczników energii.
W 2019 roku znaleźliśmy podatność w bibliotece obsługującej zdalny odczyt liczników energii. Błąd pozwalał na zdalne wyłączenie licznika bez żadnego uwierzytelnienia - wystarczyło nawiązać połączenie TCP. Biblioteka to GuruxDLMS.c, open source’owa implementacja protokołu DLMS. Narzędziem był AFL (American Fuzzy Lop) - fuzzer, który w ciągu kilku godzin sam znalazł dane wejściowe powodujące crash.
Podatność zgłosiliśmy producentowi i przed publikacją upewniliśmy się, że została naprawiona. Ten artykuł opisuje proces, którego użyliśmy - od przygotowania środowiska po analizę znalezionego błędu. Metodologia jest uniwersalna i stosujemy ją w testach bezpieczeństwa urządzeń OT.
Czym jest fuzzowanie
Fuzzowanie (fuzzing, fuzz testing) to technika testowania oprogramowania polegająca na dostarczaniu programowi losowo generowanych lub modyfikowanych danych wejściowych i obserwowaniu, czy któreś z nich powodują nieprawidłowe działanie - crash, wyciek pamięci, nieskończoną pętlę. Jest to jedna z najskuteczniejszych metod znajdowania błędów związanych z dostępem do pamięci (buffer overflow, use after free, out-of-bounds read/write) - potencjalnie najgroźniejszych w programach napisanych w C i C++.
Nowoczesne fuzzery - takie jak AFL++ - to znacznie więcej niż generatory losowych danych. Wykorzystują instrumentację (program informuje fuzzer, które fragmenty kodu zostały wykonane) i algorytmy genetyczne (nowe dane wejściowe są bardziej podobne do tych, które wcześniej odkryły niezbadane ścieżki w kodzie). Dzięki temu fuzzer systematycznie zwiększa pokrycie kodu zamiast losowo “strzelać”.
NOTE
Kurs Fuzzing 1001: Introductory white-box fuzzing with AFL++ (OpenSecurityTraining2, autor: Francesco Pollicino - wcześniej Siemens) to dobre wprowadzenie do fuzzingu z AFL++. Kurs obejmuje mutation-based fuzzing, coverage-guided fuzzing, ASAN i PCGUARD instrumentation.
Dlaczego fuzzowanie jest ważne dla OT
Oprogramowanie działające na urządzeniach przemysłowych - sterownikach PLC, RTU, licznikach energii, kontrolerach BMS - jest często napisane w C/C++ i przetwarza dane z sieci (protokoły Modbus, DNP3, DLMS, OPC). Podatności w parserach tych protokołów mogą umożliwić zdalne przejęcie kontroli nad urządzeniem lub jego wyłączenie. MITRE ATT&CK for ICS dokumentuje techniki takie jak Exploitation of Remote Services (T0866) i Denial of Service (T0814), które bezpośrednio wykorzystują tego typu błędy.
Skala problemu rośnie. W 2025 roku CISA opublikowała ponad 500 poradników bezpieczeństwa ICS (advisories) - rekord. Raport Forescout z 2026 roku pokazuje, że najczęściej dotknięte są urządzenia Purdue Level 1 (sterowniki polowe, RTU, PLC, IED) - dokładnie ta kategoria sprzętu, w której fuzzowanie przynosi najlepsze rezultaty.
CVE w produktach ICS opublikowanych w 2025
poradników CISA ICS w 2025 (rekord)
KEV z pierwszym dowodem eksploitacji w 2025
wzrost urządzeń ICS eksponowanych w internecie (2024-2025)
Źródło: Forescout ICS Cybersecurity in 2026, VulnCheck State of Exploitation 2026, CISA ICS Advisories
Fuzzowanie jest szczególnie wartościowe, bo:
- Znajduje błędy, których nie wykrywają testy manualne ani przegląd kodu
- Działa automatycznie - fuzzer może pracować tygodniami bez nadzoru
- Jest powtarzalny - każdy znaleziony crash można odtworzyć z tym samym plikiem wejściowym
- Skaluje się do dużych baz kodu
Studium przypadku: GuruxDLMS.c
Cel: biblioteka DLMS do odczytu liczników energii
GuruxDLMS.c to open source’owa implementacja protokołu DLMS/COSEM - standardu używanego w branży energetycznej do zdalnego odczytu liczników zużycia energii. W naszym scenariuszu atakujący próbuje połączyć się z serwerem (licznikiem) opartym na GuruxDLMS.c, aby zakłócić jego pracę lub przejąć kontrolę nad nim (np. zafałszować odczyty).
Krok 1: Przygotowanie programu do fuzzowania
AFL domyślnie dostarcza dane wejściowe przez plik - przekazuje programowi ścieżkę do wygenerowanego pliku jako argument wiersza poleceń. Musieliśmy więc przepisać przykładowy serwer (który nasłuchuje na porcie TCP 4061) tak, aby zamiast słuchać na porcie, czytał dane z pliku:
// GuruxDLMSSimpleServerExample/src/main.c (zmodyfikowany)
int main(int argc, char **argv){
unsigned char data;
FILE *f;
if(argc < 2){
puts("Error. Please provide a filename");
return 1;
}
svr_init(&settings, 1, DLMS_INTERFACE_TYPE_HDLC,
HDLC_BUFFER_SIZE, PDU_BUFFER_SIZE,
frame, HDLC_BUFFER_SIZE, pdu, PDU_BUFFER_SIZE);
svr_InitObjects(&settings);
if(svr_initialize(&settings)){
puts("Server init error");
return 2;
}
if(!(f = fopen(argv[1], "rb"))){
puts("Error reading file");
return 3;
}
while(fread(&data, sizeof(unsigned char), 1, f)){
if(svr_handleRequest3(&settings, data, &reply)){
puts("Error when handling request");
break;
}
if(reply.size != 0){
printf("Server reply: %s\n", reply.data);
bb_clear(&reply);
}
}
fclose(f);
return 0;
}
Logika przetwarzania danych pozostała identyczna jak w oryginalnym serwerze - jedyna zmiana to źródło danych (plik zamiast TCP). Dzięki temu mamy pewność, że znalezione błędy dotyczą prawdziwego kodu parsera, nie naszych modyfikacji.
Krok 2: Kompilacja z instrumentacją
AFL wymaga kompilacji programu z instrumentacją - specjalnym kodem śledzącym, które fragmenty programu są wykonywane. Dodatkowo użyliśmy AddressSanitizer (ASAN) - narzędzia, które zatrzymuje program natychmiast po wykryciu nieprawidłowego dostępu do pamięci i wskazuje dokładne miejsce w kodzie.
# Zmiana kompilatora z gcc na afl-clang
# Dodanie flagi -fsanitize=address do CFLAGS i LFLAGS
cd development && make
cp lib/libgurux_dlms_c.a ../GuruxDLMSSimpleServerExample/obj/
cd ../GuruxDLMSSimpleServerExample && make
Krok 3: Przygotowanie danych wejściowych
AFL potrzebuje katalogu z przykładowymi danymi wejściowymi, na bazie których będzie generował mutacje. W idealnym przypadku powinny to być poprawne ramki DLMS. Jeśli takich nie mamy - AFL poradzi sobie nawet z pojedynczą spacją:
mkdir input output
echo ' ' > input/1
Dzięki algorytmowi genetycznemu AFL prędzej czy później zacznie generować poprawną komunikację DLMS - choć z sensownymi danymi startowymi proces jest znacznie szybszy.
Krok 4: Uruchomienie fuzzera
afl-fuzz -i input/ -o output/ bin/gurux.dlms.simple.server.bin @@
Symbol @@ to miejsce, w które AFL podstawi ścieżkę do wygenerowanego pliku. Po kilku godzinach pracy fuzzer zwykle znajduje setki lub tysiące unikalnych ścieżek w kodzie (paths discovered) i - przy dostatecznym pokryciu kodu - pliki powodujące crash.
Problem: sumy kontrolne
Po kilku godzinach zauważyliśmy, że fuzzer nie odnajduje zbyt wielu nowych ścieżek. Okazało się, że protokół DLMS weryfikuje sumy kontrolne wiadomości. Nawet inteligentne fuzzery nie potrafią automatycznie obliczyć poprawnej sumy kontrolnej i umieścić ją w odpowiednim miejscu.
TIP
Rozwiązania problemu sum kontrolnych w fuzzingu:
- Wyłączenie weryfikacji w kodzie (najprostsze - to zastosowaliśmy)
- AFL post_library - skrypt przekształcający wygenerowane dane, dodający poprawne CRC
- Custom mutator w AFL++ - plugin generujący dane zgodne z formatem protokołu
- Libprotobuf-mutator - dla protokołów z definicją struktury (protobuf)
Wyłączenie CRC jest wystarczające do znalezienia błędów w parserze. Jeśli chcemy wykorzystać znalezione błędy do prawdziwego ataku (np. w ramach testu penetracyjnego), musimy dodać poprawne sumy kontrolne.
Krok 5: Znaleziona podatność
Po wyłączeniu weryfikacji CRC i ponownym uruchomieniu fuzzera, AFL znalazł pliki powodujące crash. Analiza z ASAN wskazała przyczynę:
==10691==ERROR: AddressSanitizer: stack-buffer-overflow
on address 0x7fffffffd5c1
at pc 0x7ffff6ee2397
READ of size 8187 at 0x7fffffffd5c1 thread T0
#0 __asan_memcpy
#1 ba_copy src/bitarray.c:203
#2 apdu_parseProtocolVersion src/apdu.c:1067
#3 apdu_parsePDU src/apdu.c:1428
#4 svr_HandleAarqRequest src/server.c:325
#5 svr_handleCommand src/server.c:2398
#6 svr_handleRequest3 src/server.c:2467
Przyczyna: w funkcji apdu_parseProtocolVersion() wartość zmiennej unusedBits (odczytana z danych wejściowych) mogła przekroczyć 8. To powodowało integer overflow - wartość przekazana do ba_copy() była większa niż rozmiar tablicy źródłowej. Wynik: out-of-bounds read (odczyt poza granicami bufora).
Ocena podatności
Podatność to nieuwierzytelniony DoS - atakujący mógł zdalnie zakłócić pracę licznika energii bez znajomości hasła. Podatna funkcja apdu_parseProtocolVersion() jest wywoływana przed sprawdzeniem uwierzytelnienia, co oznacza, że każdy kto może nawiązać połączenie TCP może spowodować crash.
Nadmiarowe dane skopiowane z bufora nie były wysyłane w odpowiedzi (nie jest to więc podatność na wyciek danych, jak Heartbleed). Ale nawet w wersji bez ASAN niektóre wywołania kończyły się błędem segmentacji - co wystarczy do wyłączenia usługi.
Narzędzia do fuzzowania oprogramowania OT
| Narzędzie | Typ | Zastosowanie w OT | Uwagi |
|---|---|---|---|
| AFL++ | Coverage-guided, mutation-based | Biblioteki protokołów (DLMS, Modbus, OPC) | Następca AFL, aktywnie rozwijany. PCGUARD, CMPLOG, RedQueen |
| libFuzzer | In-process, coverage-guided | Parsery, biblioteki | Część LLVM, szybszy od AFL dla małych targetów |
| Honggfuzz | Coverage-guided, multi-process | Serwery, demony | Obsługuje fuzzing procesów sieciowych |
| Boofuzz | Generation-based, sieciowy | Protokoły OT przez sieć (Modbus TCP, DNP3, OPC) | Następca Sulley, nie wymaga instrumentacji |
| Peach Fuzzer | Model-based, sieciowy | Złożone protokoły OT | Komercyjny, z definicjami formatów dla protokołów przemysłowych |
TIP
Dla protokołów OT z sumami kontrolnymi (DLMS, DNP3, Modbus) rozważ podejście hybrydowe: AFL++ z custom mutatorem (plugin generujący poprawne ramki) dla głębokiego fuzzingu parsera + Boofuzz do fuzzingu protokołu po sieci (z gotowymi definicjami pól i CRC). Boofuzz nie wymaga instrumentacji - może fuzzować urządzenie fizyczne (PLC, licznik) bez dostępu do kodu źródłowego.
Fuzzowanie jako element testów bezpieczeństwa OT
Fuzzowanie powinno być częścią procesu testowania bezpieczeństwa oprogramowania OT - zarówno na etapie rozwoju (SDL - Security Development Lifecycle), jak i podczas testów penetracyjnych urządzeń. Jest to jedno z narzędzi w ramach strategii defense in depth - identyfikacja podatności na poziomie kodu uzupełnia segmentację sieci i monitoring jako warstwa ochrony. W SEQRED stosujemy fuzzowanie w projektach testowania urządzeń ICS - od liczników energii po sterowniki PLC i bramy protokołowe.
Wnioski z naszych doświadczeń:
- Fuzzowanie samo w sobie nie wystarczy - musi być częścią szerszego procesu testowania (przegląd kodu, testy manualne, analiza architektury)
- Dostęp do kodu źródłowego dramatycznie zwiększa efektywność - instrumentacja AFL wymaga kompilacji z afl-clang. Bez kodu źródłowego można użyć QEMU mode (wolniejszy) lub fuzzingu sieciowego (Boofuzz)
- Sumy kontrolne są główną barierą - większość protokołów OT je implementuje. Custom mutatory lub wyłączenie CRC w kodzie to konieczność
- Znalezione błędy trzeba zgłaszać odpowiedzialnie - coordinated disclosure, kontakt z producentem, oczekiwanie na łatkę przed publikacją
Źródła
- AFL++ - American Fuzzy Lop plus plus - następca AFL, aktywnie rozwijany
- OpenSecurityTraining2: Fuzzing 1001 - Introductory white-box fuzzing with AFL++
- Boofuzz - Network Protocol Fuzzing
- AddressSanitizer (ASAN)
- DLMS/COSEM - Device Language Message Specification
- GuruxDLMS.c - open source DLMS library
- Forescout - ICS Cybersecurity in 2026: Vulnerabilities and Path Forward
- VulnCheck - State of Exploitation 2026
- Complete Guide to Industrial Protocol Fuzzing (2025)