Analiza bota Mirai oraz jego wariantów

cert.pl 4 lat temu

    Mirai (jap.: 未来, dosł. „przyszłość”) jest botnetem, który powstał w 2016 r. Jego celem były publicznie osiągalne urządzenia IoT oparte o system operacyjny Linux z zainstalowanym pakietem narzędzi uniksowych o nazwie BusyBox oraz otwartym portem usługi Telnet, przez który następowała infekcja. Główną funkcjonalnością Miraia było przeprowadzanie ataków typu DDoS. Dość gwałtownie po wypuszczeniu pierwszej wersji Miraia jego kod został upubliczniony przez autora, podpisującego się jako Anna-senpai na witrynie hackforums.net (treść posta). Skala botnetu była bardzo duża – według autora maksymalny osiągany przez niego rozmiar botnetu wynosił około 380 tys. botów. Jednak prawdziwy nawał nastąpił dopiero po upublicznieniu źródeł. Każdy, kto tylko chciał, mógł stworzyć własny wariant, modyfikując oryginał na najróżniejsze sposoby.

    Zainteresowani czytelnicy znajdą więcej informacji o Miraiu i jego historii w naszych raportach z poprzednich lat:

    Wśród plików otrzymanych z honeypota działającego od stycznia 2018 do kwietnia 2019 w ramach projektu SISSDEN 8327 unikalnych próbek zidentyfikowaliśmy jako warianty Miraia. Przeważająca większość z nich (ok. 7816) została skompilowana na architekturę i386, więc to na niej się skupiliśmy.

    Celem analizy tych próbek było przede wszystkim stworzenie automatycznej metody ekstrakcji ich konfiguracji statycznych, ze szczególnym uwzględnieniem adresów serwerów C&C oraz numerów portów do komunikacji z nimi. Wyciągnięte konfiguracje są magazynowane w MWDB, skąd potem trafiają m.in. do systemu mtracker. Przyjęliśmy założenie, iż ten sam kod został skompilowany na inne architektury (zasadność tego założenia zamierzamy sprawdzić w przyszłości), więc na tym etapie postanowiliśmy zawęzić pozyskiwanie konfiguracji do jednej architektury.

    Budowa oryginalnego bota Miraia

    yes its ugly, but i dont care

    Anna-senpai, 2016

    Bot Miraia składa się z następujących modułów:

    • main
      Po przeprowadzeniu wstępnych czynności związanych z infekcją oraz mechanizmami utrudniającymi analizę wsteczną głównym zadaniem tego modułu jest utrzymywanie komunikacji z serwerem C&C.
    • attack
      Moduł ten jest odpowiedzialny za sparsowanie rozkazu przysłanego przez C&C, a następnie przeprowadzenie zleconego ataku.
    • killer
      Rolą tego modułu jest zablokowanie protokołu Telnet, dzięki którego dokonana została infekcja, oraz ciągłe wyszukiwanie i usuwanie procesów należących do konkurencyjnego malware’u.
    • scanner (w oryginalnym kodzie moduł ten jest opcjonalny)
      Moduł ten służy do szukania kolejnych podatnych urządzeń w internecie.
    • table
      Moduł ten jest magazynem przechowywanych w formie niejawnej małych porcji danych, takich jak elementy konfiguracji, wywoływane polecenia powłoki systemowej czy fragmenty payloadu wysyłane w atakach DDoS.

    Moduł table

    Opis działania zaczniemy od modułu table, gdyż jest on często używany w innych fragmentach kodu. Jego rolą jest przechowywanie małych zaszyfrowanych porcji danych (najczęściej stringów w konwencji C) oraz udostępnianie ich w odszyfrowanej postaci w trakcie działania programu. Najprawdopodobniej służy to zaciemnianiu sygnatur, żeby ew. filtry antymalware’owe nie mogły wyłapywać Miraia po prostych wzorcach, bo np. część z tych napisów to dość unikalne hasła. Dodatkowo utrudnia to (nieznacznie) analizę programu.

    Standardowy przypadek użycia zapoczątkowany jest uruchomieniem funkcji table_unlock_val z podanym w parametrze numerem wpisu, potem zawartość jest odczytywana dzięki table_retrieve_val, a na koniec ponownie szyfrowana dzięki wywołania funkcji table_lock_val. Czasem odszyfrowywane jest po kilka wartości jedna po drugiej i pozostawiane w postaci niezaszyfrowanej aż do zakończenia funkcji. Chodzi o to, żeby odszyfrowane wpisy nie pozostawały zbyt długo w pamięci procesu. Ma to na celu przeciwdzialanie wykryciu zarówno przez ew. oprogramowanie antymalware’owe, jak i przez konkurencyjny malware. Analiza tego modułu ma szczególne znaczenie w procesie wyciągania statycznej konfiguracji, ponieważ duża jej część właśnie tam się znajduje.
    W załączniku przedstawiamy zawartość przechowywaną w oryginalnej wersji bota.

    Implementacja

    Główną (i w zasadzie jedyną) strukturą modułu table jest tablica table, która przechowuje wskaźniki do wpisów oraz ich długość. Tablica ta jest inicjalizowana dzięki pojedynczego wywołania funkcji table_init – wówczas szyfrogramy znajdujące się na początku sekcji .rodata (w pojedynczym bloku) są kopiowane na stertę, a odpowiednie wskaźniki są umieszczane we wpisach tablicy table. Klucz szyfrujący jest przechowywany w czterobajtowej zmiennej table_key, której oryginalna wartość wynosi 0xdeadbeef. Za deszyfrowanie i ponowne szyfrowanie odpowiadają funkcje table_unlock_val i table_lock_val, przy czym obie funkcje zawierają ten sam kod. Znajdują one w tablicy table miejsce na stercie ze wskazanym wpisem, po czym bajt po bajcie xorują go (w miejscu) po kolei przez wszystkie cztery bajty klucza. Warto zauważyć, iż – z uwagi na łączność operacji xor – wpisy są szyfrowane de facto jednym bajtem o wartości 0xde ^ 0xad ^ 0xbe ^ 0xef = 0x22.

    Ciekawa własność: mając dowolnego stringa w konwencji C, można od razu wyciągnąć z niego jednobajtowy klucz, gdyż takie stringi są szyfrowane razem z bajtem zerowym na końcu, więc ostatni bajt szyfrogramu jest jednocześnie kluczem.

    Moduł main.c

    Infekcja

    Program zaczyna działanie od usunięcia z systemu plików binarki, z której został uruchomiony. Ma to prawdopodobnie na celu ukrycie infekcji. Warto jednak wspomnieć, iż dopóki procesy Miraia działają w systemie, można odzyskać oryginalną binarkę dzięki pseudosymlinku /proc/PID/exe, czego autor wydaje się być świadomy (patrz moduł killer).

    Bot Miraia nie posiada żadnego mechanizmu persystencji. Dlatego następnym krokiem jest próba wyłączenia watchdoga, żeby zminimalizować szanse na restart urządzenia.

    Później proces upewnia się, iż jest jedyną instancją Miraia na danym hoście. W tym celu wykorzystuje port 48101, który nazwijmy „dezaktywującym”. Mirai po fazie infekcji słucha na nim, a jeżeli wykryje jakiekolwiek połączenie TCP, to całkowicie się wyłącza. Natomiast w samej fazie infekcji próbuje połączyć się na ten port, żeby dezaktywować ew. poprzednią instancję, a potem, na wszelki wypadek, zabija jeszcze wszystkie procesy, które używają portu 48101 i dopiero potem sam zaczyna na nim słuchać.
    Należy zwrócić uwagę, iż mechanizm ten daje nam bardzo wygodną metodę na pozbycie się Miraia z urządzenia. Nie ma jednak żadnej gwarancji, iż w zmodyfikowanej wersji portem dezaktywującem przez cały czas będzie 48101, ani iż w ogóle ten mechanizm przez cały czas będzie obecny.

    Potem w oddzielnych procesach uruchamiane są samodzielne moduły killer oraz scanner.

    Po tych wstępnych przygotowaniach Mirai przystępuje do adekwatnego działania.

    Pętla główna programu

    Po uruchomieniu modułów dodatkowych proces główny wchodzi w swoją pętlę główną, która odpowiada przede wszystkim za komunikację z C&C. Większość malware’u, z którym się spotykamy, wykorzystuje w tym celu model request-response – bot co jakiś czas odpytuje serwer o aktualne rozkazy. W przypadku Miraia zostało użyte inne rozwiązanie: bot, który aktywuje się na hoście, nawiązuje połączenie TCP z serwerem i utrzymuje je. Malware może sobie na to pozwolić, gdyż i tak celem są urządzenia, które zwykle działają bez przerw. Bot co minutę wysyła dwa bajty zerowe jako tzw. heartbeat, na co serwer odpowiada mu tym samym. jeżeli bot tego nie robi, to po pewnym czasie serwer zerwie połączenie.

    Jeśli zachodzi potrzeba uruchomienia ataku DDoS, to serwer C&C wysyła wszystkim (lub części) podłączonych do niego botów komunikat, w którym opisane są szczegóły konkretnego ataku. Protokół komunikacji z C&C jest binarny. W pierwszych dwóch bajtach przesyłana jest długość rozkazu, a w kolejnych sama jego treść. Odebrany rozkaz jest następnie przekazywany do funkcji attack_parse (z modułu attack), która go odczytuje i uruchamia odpowiedni atak.

    Składnia rozkazu:

    Uruchomieniem ataku zajmuje się funkcja attack_start. dla wszystkich ataku uruchamia ona dwa dodatkowe procesy: pierwszy uruchamia wybraną implementację ataku (w założeniu przeprowadza ona atak w nieskończonej pętli), a drugi czeka duration sekund, po czym zabija proces pierwszy, a potem siebie (jest to implementacja mechanizmu timeuoutu).

    Tutaj znajduje się lista zaimplementowanych i przewidzianych w protokole ataków, a tuż pod nią – lista opcji ich konfigurowania.

    Implementacje ataków zostały podzielone na cztery kategorie i zgrupowane w odpowiednich plikach źródłowych:

    Moduł killer

    Moduł ten najpierw zabija proces nasłuchujący na tcp/23 (Telnet), a następnie sam zaczyna tam nasłuchiwać. Niczego więcej z tym portem nie robi, chodzi tylko o zablokowanie usługi oraz uniemożliwienie odnowienia jej. Dzięki temu konkurencyjny malware nie będzie już mógł zainfekować urządzenia w ten sam sposób co Mirai. Istnieją analogiczne (opcjonalne) fragmenty kodu źródłowego dla tcp/22 (SSH) oraz tcp/80 (HTTP).

    Następnie moduł przegląda (w pętli) procesy w katalogu /proc. dla wszystkich z nich pobiera ścieżkę do binarki (symlink /proc/PID/exe), a potem w kolejności:

    • jeśli ścieżka do binarki zawiera w sobie napis .anime (napis przechowywany w module table), to zabija proces;
    • jeśli jest to binarka bota, to zostawia proces w spokoju;
    • jeśli binarka procesu została usunięta z systemu plików, to zabija proces;
    • a na końcu, korzystając znów z symlinku /proc/PID/exe, skanuje całą zawartość binarki pod kątem pewnych wzorców (znów przechowywanych w module table) i jeżeli znajdzie, to zabija proces.

    Wzorce z oryginalnego kodu (odszyfrowane):

    Przedostatni wpis jest prawdopodobnie wynikiem pomyłki autora. W kodzie źródłowym jest on podpisany jako TABLE_MEM_UPX, ale tak naprawdę jest to zaszyfrowane słowo zollard, w postaci, w jakiej zostało umieszczone w pliku źródłowym. Pozostałe wpisy są tekstowymi sygnaturami konkurencyjnego malware’u.

    Moduł scanner

    Moduł ten służy do znajdowania kolejnych podatnych urządzeń w internecie i raportowania ich na oddzielny serwer zdefiniowany w konfiguracji statycznej. Rozsyła pod losowane adresy IP (z wyjątkiem kilku zakresów) pakiety TCP/SYN na porty 23 (Telnet) i 2323, a potem nasłuchuje odpowiedzi (pakietów SYN/ACK).

    Cechą charakterystyczną Miraia jest fakt, iż przy wysyłaniu wyżej wymienionych pakietów w numerze sekwencyjnym nagłówka TCP zawsze umieszczany jest docelowy adres IP. Bot weryfikuje to również w przychodzących odpowiedziach.

    Po odebraniu odpowiedzi bot nie kontynuuje rozpoczętego połączenia, tylko nawiązuje nowe z wykorzystaniem funkcji connect.

    Następnie obsługa tych połączeń trafia do zaimplementowanej maszyny stanów. Równolegle bot może połączyć się ze 128 urządzeniami. dla wszystkich z połączeń aplikacja próbuje zalogować się przez Telnet dzięki jednej (wylosowanej) z 62 predefiniowanych par login-hasło. W sumie próbuje 10 razy.

    W binarce predefiniowane dane logowania są zaszyfrowane w taki sam sposób jak w module table, ale całkowicie niezależnie od niego – kod do odszyfrowywania jest inny i jest uruchamiany tylko raz, na początku działania modułu scanner. Potem odszyfrowane pary login-hasło stale znajdują się w pamięci procesu.

    Jeśli logowanie przez Telnet się powiedzie, program wysyła po kolei cztery polecenia:

    Celem tego zabiegu jest uruchomienie shella, o ile jest to konieczne dla danego urządzenia. jeżeli nie, to nadmiarowe polecenia i tak w niczym nie przeszkadzają.

    Następnie, w celu weryfikacji dostępu do shella oraz obecności narzędzia BusyBox, wysyłane jest polecenie /bin/busybox MIRAI. Oczekiwaną reakcją jest komunikat postaci MIRAI: applet not found. jeżeli tak się stanie, to uruchamiana jest funkcja report_working, która najpierw wyciąga z modułu table domenę i port serwera do raportowania, a następnie wysyła do tego serwera informacje o podatnym hoście:

    • IP
    • port
    • użyty login
    • użyte hasło

    Analizując skrypt do budowania bota ze źródeł możemy dojść do wniosku, iż autor miał w planach rozszerzenie możliwości bota o próby zalogowania się również przez SSH.

    Mechanizmy utrudniające analizę

    Sprawdzenie argv[0]

    Bot Miraia w dość zobfuskowany sposób uzależnia swoje działanie od zawartości parametru argv[0] przekazanego funkcji main (pierwszy człon polecenia użytego do uruchomienia programu). Liczy z tego ciągu hasza (z przestrzeni {0, …, 8}) i na jego podstawie wybiera jedną z dziewięciu funkcji z tablicy, po czym uruchamia ją. Zamierzonym zachowaniem jest uruchomienie funkcji table_init – wspomniana tablica dziewięciu funkcji jest jedynym miejscem w całym kodzie, w którym występuje adres funkcji table_init. (Pozostałe elementy wskazują na przypadkowe funkcje, takie jak: table_unlock_val, util_strcmp itd.) Dodatkowo zawartość argv[0] sprawdzana jest też dokładniej i jeżeli jest nią ./dvrHelper, to program wysyła do samego siebie sygnał SIGTRAP, co ma związek z kolejnym opisywanym zabezpieczeniem (antydebug). Na końcu, w celu ukrycia prawidłowej zawartości, argv[0] jest wypełniane losowym ciągiem, a nazwa procesu jest nadpisywana innym losowym ciągiem (za pomocą funkcji prctl).

    Antydebug

    Adres i port serwera C&C dla funkcji connect przechowywany jest w strukturze struct sockaddr_in srv_addr. Jednak na początku działania programu jest ona wypełniana fałszywymi danymi, przechowywanymi w postaci jawnej.

    Za wypełnienie srv_addr prawdziwymi danymi odpowiedzialna jest funkcja o nazwie resolve_cnc_addr. Pobiera ona z modułu table domenę oraz numer portu, następnie rozwiązuje domenę do adresu IP i razem z portem wpisuje do srv_addr. Jednak funkcja ta, podobnie jak table_init, nie jest nigdzie wywoływana bezpośrednio.
    Zamiast tego zastosowany jest następujący mechanizm:
    Przed każdą próbą połączenia z serwerem C&C uruchamiany jest kod spod wskaźnika na funkcję o nazwie resolve_func. W zamierzeniu powinno to powodować wypełnienie struktury srv_addr prawidłowymi danymi, jednak wskaźnik ten na początku jest zainicjalizowany adresem funkcji util_local_addr, która w tym miejscu nie ma żadnego wpływu na działanie programu. Ale jeszcze przed uruchomieniem komunikacji z serwerem C&C program wysyła do siebie wspomniany wcześniej sygnał SIGTRAP. Pod obsługę sygnału SIGTRAP jest podpięta funkcja o sugestywnej nazwie anti_gdb_entry.
    Funkcja ta jest bardzo krótka – przypisuje ona tylko wskaźnikowi resolve_func adres prawidłowej funkcji resolve_cnc_addr:

    W normalnych okolicznościach spowoduje to, iż resolve_cnc_addr wywoła się przed każdą próbą połączenia z serwerem C&C i ustawi w srv_addr poprawne dane do połączenia. Domena jest rozwiązywana przy każdej próbie połączenia, dzięki czemu bot jest w stanie reagować na zmianę wskazywanego adresu IP. jeżeli jednak bot jest debugowany, to sygnał SIGTRAP spowoduje, iż sterowanie zostanie natychmiast przekazane do debuggera i ominie wywołanie funkcji anti_gdb_entry, wskutek czego dane w strukturze srv_addr pozostaną fałszywe.

    Tak więc utrudnienia w identyfikacji danych do połączenia z C&C dotyczą zarówno analizy statycznej (fałszywe dane dostępne w postaci jawnej), jak i dynamicznej (fałszywe dane są używane w rzeczywistości, jeżeli program został uruchomiony pod debuggerem).

    Zaobserwowane warianty Miraia

    Standardowy podział wariantów Miraia oparty jest o polecenie wydawane w trakcie detekcji podatnego urządzenia (moduł scanner, w oryginale /bin/busybox MIRAI). Przykładowo, jeżeli polecenie brzmi /bin/busybox SORA, to jako nazwa wariantu przyjmowane jest SORA. Należy zwrócić jednak uwagę na to, iż ta nazwa wariantu jest w zasadzie tylko deklaracją autora. Równie dobrze bot może się podszywać pod inny mniej lub bardziej znany wariant Miraia. Dlatego opisane wyżej kategorie będziemy określali jako „deklarowane nazwy”. Natomiast przez „wariant” będziemy rozumieli wariant kodu, czyli wyróżniający się funkcjonalnością, a nie tylko konfiguracją.

    Na potrzeby naszej analizy dokonaliśmy więc innego podziału próbek, tym razem związanego z faktyczną funkcjonalnością bota – konkretnie po budowie funkcji resolve_cnc_addr. Ma ona dla nas szczególne znaczenie w procesie wyciągania statycznej konfiguracji, bowiem to właśnie z niej można się dowiedzieć, gdzie przechowywane są dane do połączenia z serwerem C&C.

    resolve_cnc_addr_origin

    To jest oznaczenie przydzielone oryginalnej wersji funkcji resolve_cnc_addr, opublikowanej przez Anna-senpai.

    Kod tej funkcji (wyczyszczony z fragmentów związanych z debugowaniem) przedstawia się następująco:

    Źródło

    resolve_cnc_addr_simple

    Jest to zdecydowanie najczęściej spotykany przez nas wariant. W sumie zidentyfikowaliśmy go 5908 razy, co daje ponad 70% liczby wszystkich próbek. Jak sama nazwa wskazuje, funkcja ta została w duży sposób uproszczona. Cały kod związany z rozwiązywaniem domeny został z niej usunięty i zastąpiony wpisaniem adresu IP na sztywno.

    Widać tu dużą niekonsekwencję z zamiarami oryginalnego autora – tak umieszczony adres można przeczytać choćby przy użyciu komendy strings.

    resolve_cnc_addr_plain_port

    W tym wariancie pomijane jest z kolei wyciąganie numeru portu z modułu table. Zamiast tego numer portu jest umieszczany w strukturze dzięki zwyczajnego przypisania stałej liczbowej.

    resolve_cnc_addr_mod1

    Czwarty wariant natomiast rozwiązuje domenę zapisaną tekstem jawnym. jeżeli się to powiedzie, to standardowo z modułu table jest wyciągany port i zapisywany w strukturze. Natomiast w przeciwnym przypadku adres IP jest wpisywany na sztywno, a port nie jest nadpisywany w ogóle – zostanie użyty ten, który zwykle figurował jako fałszywy.

    Podział na warianty

    Liczbowy podział na warianty i deklarowane nazwy został przedstawiony w tej tabelce. Wiersze reprezentują w nim podział według deklarowanych nazw (wpis __unknown__ oznacza próbkę nieprzydzieloną do żadnej z kategorii). Natomiast kolumny reprezentują podział według budowy funkcji resolve_cnc_addr. W komórkach znajdują się liczby unikalnych próbek, które zostały zakwalifikowane do konkretnych kategorii wymienionych wyżej. Za unikalność próbek w obrębie jednej grupy (reprezentowanej przez komórkę w tabeli) odpowiada przede wszystkim różnorodność konfiguracji statycznej (głównie adres serwera C&C).

    Warto zauważyć, iż w wielu wierszach (19 na 125) deklarowany wariant jest rozbity na co najmniej dwie różne implementacje, w tym IZ1H9 oraz SEFA są rozbite na aż trzy implementacje. Biorąc pod uwagę, jak mało prawdopodobne jest, żeby dwóch autorów niezależnie zmodyfikowało funkcję resolve_cnc_addr w identyczny sposób, nasuwa się wniosek, iż nie należy zbyt mocno polegać na nazwach, jakimi boty się „przedstawiają”. Należałoby traktować je bardziej jako element konfiguracji.

    Inne zmiany

    Na przedstawionym wcześniej przykładzie funkcji resolve_cnc_addr widać tendencję następców twórcy Miraia do znacznego upraszczania oryginalnego kodu. Na przykład oba mechanizmy utrudniające analizę (opisane wcześniej) zostały całkowicie wyłączone poprzez jawne wywołanie funkcji table_init oraz bezpośrednie przypisanie resolve_func = resolve_cnc_addr.

    Na około 30 próbek, które zostały manualnie przeanalizowane statycznie:

    • 8 zostało skategoryzowanych jako RCA_origin,
    • 11 jako RCA_simple,
    • 2 jako RCA_plain_port,
    • 4 jako RCA_mod1.

    Z powyższych w RCA_simple, RCA_plain_port oraz RCA_mod1 taki sam kawałek kodu

    został dopisany dokładnie w tym samym miejscu (tuż po wypełnieniu struktury srv_addr fałszywymi danymi). Tak więc możemy przypuszczać, iż warianty te wywodzą się od jednej wspólnej modyfikacji Miraia.

    Najczęstsze zmiany dotyczą oczywiście rzeczy, które można uznać za konfigurację:

    • dane do łączenia się z serwerem C&C
    • dane do łączenia się z serwerem raportującym nowe podatne urządzenia
    • klucz do modułu table (choć efektywnie klucze te dają tylko 256 różnych możliwych szyfrowań, w tym jedno trywialne)
    • lista par login/hasło do modułu scanner
    • lista wzorców wyszukiwanych w pamięci procesów przez moduł killer
    • i oczywiście deklarowana nazwa wariantu

    Jeśli zaś chodzi o funkcjonalność bota, to najczęściej modyfikacji ulega lista ataków. Ze względu na modularną budowę kodu dopisywanie nowych jest bardzo łatwe. Oczywiście nic nie stoi na przeszkodzie, żeby funkcja z modułu atakującego robiła coś zupełnie innego (np. aktualizacja bota czy zniszczenie urządzenia), jednak nie zaobserwowaliśmy takiego przypadku w analizowanych próbkach.

    Ciekawostki

    Nie wszyscy autorzy modyfikacji byli znani z dobrego kodu i w związku z tym przekazywali różne rzeczy jako argument funkcji inet_aton wewnątrz resolve_cnc_addr. Zdarzały się tam domeny, puste napisy, czy wymieniony już wcześniej we fragmencie kodu niepoprawny adres „178.128.67.915”. Z innych niepoprawnych argumentów zdarzały się „0/0/0/0” i nasz ulubiony inet_aton(„PUT YOUR IP HERE”) ?
    Jednak najczęstszym błędem było… wstawianie przecinków zamiast kropek! (wpisy postaci inet_aton(„195,187,6,2”))
    Prawdopodobną przyczyną było pomylenie użytej funkcji inet_aton z tym makrem wykorzystywanym w wielu miejscach w kodzie, przy używaniu którego rzeczywiście oddziela się liczby przecinkami. Znaleziono aż 34 unikalne IP wpisane w ten sposób! (z 48 próbek)

    W jednej z badanych próbek deklarowaną nazwą jest CORONA (patrz podział na warianty, wiersz 27). A więc de facto jest to prawdziwy cyfrowy koronawirus! ?
    Próbka pojawiła się jednak przed odkryciem SARS-CoV-2, pierwsze zgłoszenie tej próbki na serwisie VirusTotal (jako plik o nazwie corona.x86) jest z grudnia 2018 r.

    Załączniki

Idź do oryginalnego materiału