W ciągu ostatnich dwóch tygodni widzieliśmy serię artykułów omawiających coś, co zostało opisane jako „łamanie hasła głównego” w popularnym menedżerze haseł typu open source KeePass.
Błąd został uznany za wystarczająco ważny, aby uzyskać oficjalny identyfikator rządu USA (znany jako CVE-2023-32784, jeśli chcesz go upolować), a biorąc pod uwagę, że hasło główne do menedżera haseł jest właściwie kluczem do całego cyfrowego zamku, możesz zrozumieć, dlaczego ta historia wywołała wiele emocji.
Dobrą wiadomością jest to, że osoba atakująca, która chciałaby wykorzystać ten błąd, prawie na pewno musiałaby już zainfekować Twój komputer złośliwym oprogramowaniem, a zatem i tak byłaby w stanie szpiegować naciśnięcia klawiszy i uruchomione programy.
Innymi słowy, błąd można uznać za łatwe do opanowania ryzyko, dopóki twórca KeePass nie wyda aktualizacji, która powinna pojawić się wkrótce (najwyraźniej na początku czerwca 2023 r.).
Jako osoba ujawniająca błąd dba o to zwrocic uwage:
Jeśli używasz pełnego szyfrowania dysku z silnym hasłem, a Twój system jest [wolny od złośliwego oprogramowania], wszystko powinno być w porządku. Dzięki temu odkryciu nikt nie może zdalnie ukraść twoich haseł przez Internet.
Ryzyko wyjaśnione
Krótko mówiąc, błąd sprowadza się do trudności w upewnieniu się, że wszystkie ślady poufnych danych zostaną usunięte z pamięci, gdy już z nimi skończysz.
Pominiemy tutaj problemy, jak w ogóle uniknąć przechowywania tajnych danych w pamięci, nawet na krótko.
W tym artykule chcemy tylko przypomnieć programistom na całym świecie, że kod zatwierdzony przez świadomego bezpieczeństwa recenzenta z komentarzem typu „wydaje się, że poprawnie czyści się po sobie”…
…w rzeczywistości może wcale nie zostać całkowicie oczyszczony, a potencjalny wyciek danych może nie być oczywisty z bezpośredniego przestudiowania samego kodu.
Mówiąc najprościej, luka w zabezpieczeniach CVE-2023-32784 oznacza, że hasło główne KeePass można odzyskać z danych systemowych nawet po zamknięciu programu KeyPass, ponieważ wystarczające informacje o haśle (choć w rzeczywistości nie surowe hasło, na którym skupimy się za chwilę) może zostać w plikach wymiany systemu lub plikach uśpienia, gdzie przydzielona pamięć systemowa może zostać zachowana na później.
Na komputerze z systemem Windows, na którym funkcja BitLocker nie jest używana do szyfrowania dysku twardego, gdy system jest wyłączony, dałoby to oszustowi, który ukradł Twój laptop, szansę na uruchomienie systemu z napędu USB lub CD i odzyskanie hasła głównego nawet chociaż sam program KeyPass dba o to, aby nigdy nie zapisywać go na stałe na dysku.
Długoterminowy wyciek hasła w pamięci oznacza również, że hasło można teoretycznie odzyskać ze zrzutu pamięci programu KeyPass, nawet jeśli ten zrzut został przechwycony długo po wpisaniu hasła i długo po KeePass sam nie miał już potrzeby trzymania go w pobliżu.
Oczywiście powinieneś założyć, że złośliwe oprogramowanie już w twoim systemie może odzyskać prawie każde wpisane hasło za pomocą różnych technik szpiegowania w czasie rzeczywistym, o ile były aktywne w momencie pisania. Można jednak rozsądnie oczekiwać, że czas narażony na niebezpieczeństwo będzie ograniczony do krótkiego okresu pisania na klawiaturze, a nie wydłużony do wielu minut, godzin lub dni później, a być może dłuższy, w tym po wyłączeniu komputera.
Co zostaje w tyle?
Dlatego pomyśleliśmy, że przyjrzymy się na wysokim poziomie, w jaki sposób tajne dane mogą zostać pozostawione w pamięci w sposób, który nie jest bezpośrednio widoczny w kodzie.
Nie martw się, jeśli nie jesteś programistą — uprościmy to i wyjaśnimy na bieżąco.
Zaczniemy od przyjrzenia się użyciu i czyszczeniu pamięci w prostym programie C, który symuluje wprowadzanie i tymczasowe przechowywanie hasła, wykonując następujące czynności:
- Przydzielanie dedykowanej porcji pamięci specjalnie do przechowywania hasła.
- Wstawianie znanego ciągu tekstowego więc w razie potrzeby możemy łatwo znaleźć go w pamięci.
- Dołączanie 16 pseudolosowych 8-bitowych znaków ASCII z zakresu AP.
- Wydrukować symulowany bufor haseł.
- Zwalnianie pamięci w nadziei na wymazanie bufora haseł.
- Wyjście program.
Znacznie uproszczony kod C może wyglądać mniej więcej tak, bez sprawdzania błędów, przy użyciu pseudolosowych liczb niskiej jakości z funkcji wykonawczej C rand()
i ignorując wszelkie kontrole przepełnienia bufora (nigdy nie rób tego w prawdziwym kodzie!):
// Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);
W rzeczywistości kod, którego ostatecznie użyliśmy w naszych testach, zawiera kilka dodatkowych fragmentów pokazanych poniżej, dzięki czemu mogliśmy zrzucić całą zawartość naszego tymczasowego bufora haseł, gdy go używaliśmy, w celu wyszukania niechcianych lub pozostawionych treści.
Zauważ, że celowo zrzucamy bufor po wywołaniu free()
, co z technicznego punktu widzenia jest błędem polegającym na używaniu po zwolnieniu, ale robimy to tutaj jako podstępny sposób sprawdzenia, czy coś krytycznego nie zostanie pozostawione po zwróceniu naszego bufora, co może prowadzić do niebezpiecznej dziury wycieku danych w prawdziwym życiu.
Wprowadziliśmy również dwa Waiting for [Enter]
monity w kodzie, aby dać sobie szansę na utworzenie zrzutów pamięci w kluczowych punktach programu, dając nam surowe dane do późniejszego wyszukiwania, aby zobaczyć, co pozostało po uruchomieniu programu.
Aby wykonać zrzuty pamięci, użyjemy narzędzia Microsoft Narzędzie Sysinternals procdump
z -ma
opcja (zrzuć całą pamięć), co pozwala uniknąć konieczności pisania własnego kodu w celu korzystania z systemu Windows DbgHelp
system i jest dość złożony MiniDumpXxxx()
Funkcje.
Aby skompilować kod C, wykorzystaliśmy naszą własną, małą i prostą kompilację bezpłatnego i otwartego kodu źródłowego Fabrice'a Bellarda Mały kompilator C, dostępne dla 64-bitowego systemu Windows w forma źródłowa i binarna bezpośrednio z naszej strony GitHub.
Tekst całego kodu źródłowego przedstawionego w artykule, który można skopiować i wkleić, pojawia się na dole strony.
Oto, co się stało, gdy skompilowaliśmy i uruchomiliśmy program testowy:
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard Rozebrany przez Paula Ducklina do użytku jako narzędzie do nauki Wersja petcc64-0.9.27 [0006] - Generuje 64-bit Tylko środowiska PE -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- ----------------------- rozmiar pliku virt sekcja 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 bajtów) C:UsersduckKEYPASS> unl1.exe Zrzucanie „nowego” bufora przy starcie 00F51390: 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P....... 00F513A0: 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 trzpień32cmd. exe.D 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F 513F0: 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF 00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6 7C 56A 4 F3 4C AC 00B 00 00 netto ExplzV.< .K.. Pełny ciąg to: mało prawdopodobny tekstJHKNEJJCPOMDJHAN 51390F75: 6 6E 69C 6 65B 6 79C 74 65 78 74 4 48A 4 4B 00E mało prawdopodobny tekstJHKN 513F0A45: 4 4A 43A 50 4 4F 44D 4 48A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72 : 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00E riverData=C:Win 513F0C64: 6 77F 73 5 53C 79 73 74 65 6 33D 32 5 44C 72 32 dowsSystem00D r 513F0D69: 76 65 72 73 5 44C 72 69 76 65 72 44 61 74 61 00 iversDriverData 513F0E00: 45 46 43 5 34F 33 37 32 3 31D 00 46 50 53 5 4372F .EFC_1=00.FPS_ 513F0F42: 52 4 57P 53 45 52 5 41P 50 50 5 50P 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=Inter 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F3 4C AC 00B 00 00 netto ExplzV.<.K.. Oczekiwanie, aż [ENTER] zwolni bufor... Zrzut bufora po zwolnieniu () 51390F0: A67 5 F00 00 00 00 00 50 01 5 F00 00 00 00 00 00 .g......P...... 513F0A45: 4 4A 43A 50 4 4F 44D 4 48A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72: 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00E riverData=C:Win 513F0C64: 6 77F 73 iversDriverData 5F53E79: 73 74 65 6 33F 32 5 44 72 32D 00 513 0 69 76 65F .EFC_72=73.FPS_ 5F44F72: 69 76 65F 72 44 61 74 61F 00 513 0 00F 45 46 43F 5 BROWSER_APP_PROF 34F33: 37 32C 3 31F 00 46 50 53 5E 4372 1D 00 513E 0 42 52 ILE_STRING=Inter 4F57: 53E 45 52 5 41 50 50 5C 50D 52 4 46D AC 00B 51400 49 netto ExplM..MK. Oczekiwanie na wyjście [ENTER] z funkcji main()... C:UsersduckKEYPASS>
W tym przebiegu nie zawracaliśmy sobie głowy pobieraniem żadnych zrzutów pamięci procesu, ponieważ od razu mogliśmy zobaczyć na wyjściu, że ten kod powoduje wyciek danych.
Zaraz po wywołaniu funkcji biblioteki wykonawczej systemu Windows C malloc()
, widzimy, że bufor, który otrzymujemy, zawiera coś, co wygląda jak dane zmiennych środowiskowych pozostałe po kodzie startowym programu, z pierwszymi 16 bajtami najwyraźniej zmienionymi tak, aby wyglądały jak jakiś pozostały nagłówek alokacji pamięci.
(Zauważ, jak te 16 bajtów wygląda jak dwa 8-bajtowe adresy pamięci, 0xF55790
i 0xF50150
, które znajdują się odpowiednio po i tuż przed naszym własnym buforem pamięci.)
Kiedy hasło ma znajdować się w pamięci, możemy wyraźnie zobaczyć cały ciąg w buforze, tak jak byśmy tego oczekiwali.
Ale po wezwaniu free()
, zwróć uwagę, jak pierwsze 16 bajtów naszego bufora zostało przepisane z czymś, co wygląda jak pobliskie adresy pamięci, prawdopodobnie po to, aby alokator pamięci mógł śledzić bloki w pamięci, których może ponownie użyć…
… ale reszta naszego „usuniętego” tekstu hasła (ostatnie 12 losowych znaków EJJCPOMDJHAN
) zostało w tyle.
Nie tylko musimy zarządzać własnymi alokacjami i de-alokacjami pamięci w C, ale musimy również upewnić się, że wybieramy odpowiednie funkcje systemowe dla buforów danych, jeśli chcemy precyzyjnie nimi sterować.
Na przykład, przełączając się na ten kod, uzyskujemy nieco większą kontrolę nad tym, co jest w pamięci:
Przełączając się z malloc()
i free()
do korzystania z funkcji alokacji systemu Windows niższego poziomu VirtualAlloc()
i VirtualFree()
bezpośrednio, uzyskujemy lepszą kontrolę.
Płacimy jednak cenę w szybkości, ponieważ każde połączenie do VirtualAlloc()
wykonuje więcej pracy niż wezwanie malloc()
, który działa poprzez ciągłe dzielenie i dzielenie bloku wstępnie przydzielonej pamięci niskiego poziomu.
Korzystanie z VirtualAlloc()
wielokrotnie dla małych bloków również zużywa ogólnie więcej pamięci, ponieważ każdy blok jest wyrzucany przez VirtualAlloc()
zwykle zajmuje wielokrotność 4 KB pamięci (lub 2 MB, jeśli używasz tzw duże strony pamięci), więc nasz 128-bajtowy bufor powyżej jest zaokrąglany w górę do 4096 bajtów, marnując 3968 bajtów na końcu bloku pamięci 4 KB.
Ale, jak widać, pamięć, którą odzyskujemy, jest automatycznie wyczyszczona (ustawiona na zero), więc nie możemy zobaczyć, co było tam wcześniej, i tym razem program ulega awarii, gdy próbujemy skorzystać z funkcji use-after-free sztuczka, ponieważ system Windows wykrywa, że próbujemy zajrzeć do pamięci, której już nie posiadamy:
C:UsersduckKEYPASS> unl2 Zrzucanie „nowego” bufora przy starcie 0000000000 0000 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0010 00 00 00 00 00 00 00 00 00 00 00 00 .. .............. 00EA00: 00 00 0000000000 0020 00 00 00 00 00 00 00 00 00 00 00 00 ........... 00EA00: 00 00 0000000000 0030 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 00 ............... 00EA00: 00 00 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 00 ........... 00EA00: 00 00 0000000000 0060 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ Pełny ciąg znaków to: mało prawdopodobny tekst IBIPJPPHEOPOIDLL 00EA00: 00 0000000000E 0080C 00 00B 00 00C 00 00 00 00 00 00 00 00 00 mało prawdopodobny tekst IBIP 00EA00: 00A 0000000000 0000 75 6 6F 69 6F 65 6 79C 74C 65 78 74 49 JPPHEOPOIDLL .... 42EA49: 50 0000000000 0010 4 50 50 48 45 4 50 4 49 44 4 ................ 4EA00 : 00 00 00 0000000000 0020 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 00 0000000000 0030 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 00 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 ............... 00EA00: 00 00 00 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 00 0000000000 0060 00 00 00 00 00 00 00 00 00 00 00 .............. 00EA00: 00 00 00 0000000000 0070 00 00 00 00 00 00 00 00 00 00 00 ............... ... Oczekiwanie na [ENTER] w celu zwolnienia bufora... Zrzut bufora po free() 00EA00: [Program został tu zakończony, ponieważ system Windows przechwycił nasze użycie po zwolnieniu]
Ponieważ pamięć, którą zwolniliśmy, będzie wymagała ponownej alokacji VirtualAlloc()
zanim będzie można go ponownie użyć, możemy założyć, że zostanie wyzerowany, zanim zostanie poddany recyklingowi.
Gdybyśmy jednak chcieli się upewnić, że jest wymazany, moglibyśmy wywołać specjalną funkcję systemu Windows RtlSecureZeroMemory()
tuż przed zwolnieniem, aby zagwarantować, że Windows najpierw zapisze zera w naszym buforze.
Powiązana funkcja RtlZeroMemory()
, gdybyś się zastanawiał, robi coś podobnego, ale bez gwarancji, że faktycznie działa, ponieważ kompilatory mogą usunąć go jako teoretycznie zbędny, jeśli zauważą, że bufor nie jest później ponownie używany.
Jak widać, musimy bardzo uważać, aby korzystać z odpowiednich funkcji systemu Windows, jeśli chcemy zminimalizować czas, przez który sekrety przechowywane w pamięci mogą leżeć na później.
W tym artykule nie będziemy przyglądać się, jak zapobiegasz przypadkowemu zapisywaniu tajemnic w pliku wymiany, blokując je w fizycznej pamięci RAM. (Wskazówka: VirtualLock()
samo w sobie nie wystarcza.) Jeśli chcesz dowiedzieć się więcej o niskim poziomie zabezpieczeń pamięci systemu Windows, daj nam znać w komentarzach, a przyjrzymy się temu w przyszłym artykule.
Korzystanie z automatycznego zarządzania pamięcią
Sprytnym sposobem na uniknięcie konieczności samodzielnego przydzielania, zarządzania i zwalniania pamięci jest użycie języka programowania, który zajmuje się malloc()
i free()
lub VirtualAlloc()
i VirtualFree()
, automatycznie.
Język skryptowy, np Perl, Python, Luka, JAVASCRIPT a inni pozbywają się najczęstszych błędów związanych z bezpieczeństwem pamięci, które nękają kod C i C++, śledząc wykorzystanie pamięci w tle.
Jak wspomnieliśmy wcześniej, nasz źle napisany przykładowy kod w C działa teraz dobrze, ale tylko dlatego, że nadal jest to bardzo prosty program ze strukturami danych o stałym rozmiarze, w których możemy zweryfikować, sprawdzając, czy nie nadpiszemy naszych 128- bufor bajtowy i że istnieje tylko jedna ścieżka wykonania, która zaczyna się od malloc()
i kończy się odpowiednim free()
.
Ale jeśli zaktualizujemy go, aby umożliwić generowanie haseł o zmiennej długości lub dodaliśmy dodatkowe funkcje do procesu generowania, wtedy my (lub ktokolwiek inny będzie zajmował się kodem) możemy łatwo skończyć z przepełnieniami bufora, błędami użycia po zwolnieniu lub pamięcią, która nigdy nie zostaje uwolniony i dlatego pozostawia tajne dane długo po tym, jak nie są już potrzebne.
W języku takim jak Lua możemy pozwolić środowisku uruchomieniowemu Lua, które robi to, co w żargonie nazywa się automatyczne zbieranie śmieci, zajmować się pobieraniem pamięci z systemu i zwracaniem jej, gdy wykryje, że przestaliśmy jej używać.
Program C, który wymieniliśmy powyżej, staje się znacznie prostszy, gdy zadbamy o alokację i de-alokację pamięci:
Przydzielamy pamięć do przechowywania łańcucha s
po prostu przypisując ciąg 'unlikelytext'
do niego.
Możemy później albo wyraźnie zasugerować Lua, że nie jesteśmy już zainteresowani s
przypisując mu wartość nil
(wszystko nils
są zasadniczo tym samym obiektem Lua) lub przestań ich używać s
i poczekaj, aż Lua wykryje, że nie jest już potrzebny.
Tak czy inaczej, pamięć używana przez s
zostaną ostatecznie odzyskane automatycznie.
Aby zapobiec przepełnieniu bufora lub niewłaściwemu zarządzaniu rozmiarem podczas dołączania do ciągów tekstowych (operator Lua ..
wymawiane konkat, zasadniczo dodaje dwa ciągi razem, na przykład +
w Pythonie) za każdym razem, gdy wydłużamy lub skracamy łańcuch, Lua magicznie przydziela miejsce na zupełnie nowy ciąg, zamiast modyfikować lub zastępować oryginalny w jego istniejącej lokalizacji w pamięci.
Takie podejście jest wolniejsze i prowadzi do szczytowego wykorzystania pamięci, które jest wyższe niż w C ze względu na pośrednie łańcuchy alokowane podczas manipulacji tekstem, ale jest znacznie bezpieczniejsze pod względem przepełnienia bufora.
Ale ten rodzaj automatycznego zarządzania łańcuchami znaków (znany w żargonie jako niezmienność, ponieważ łańcuchy nigdy nie dostają zmutowany, lub modyfikowane na miejscu, gdy już zostaną utworzone), same w sobie powodują nowe problemy z cyberbezpieczeństwem.
Uruchomiliśmy powyższy program Lua w systemie Windows, aż do drugiej przerwy, tuż przed zakończeniem programu:
C:UsersduckKEYPASS> lua s1.lua Pełny ciąg to: mało prawdopodobnetekstHLKONBOJILAGLNLN Oczekiwanie na [ENTER] przed zwolnieniem łańcucha... Oczekiwanie na [ENTER] przed wyjściem...
Tym razem zrobiliśmy zrzut pamięci procesu, taki jak ten:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 — narzędzie do zrzutu procesu Sysinternals zainicjowano: C:UsersduckKEYPASSlua-s2009.dmp [2022:00:00] Zapis zrzutu 00: Szacowany rozmiar pliku zrzutu wynosi 1 MB. [1:00:00] Zrzut 00 zakończony: 1 MB zapisanych w 10 sekundy [00:00:00] Osiągnięto liczbę zrzutów.
Następnie uruchomiliśmy ten prosty skrypt, który wczytuje z powrotem plik zrzutu, znajduje wszędzie w pamięci, gdzie znajduje się znany ciąg znaków unlikelytext
pojawił się i drukuje go wraz z jego położeniem w pliku zrzutu i znakami ASCII, które nastąpiły bezpośrednio po nim:
Nawet jeśli wcześniej używałeś języków skryptowych lub pracowałeś w jakimkolwiek ekosystemie programistycznym, który oferuje tzw zarządzane ciągi, gdzie system śledzi alokacje i zwalniania pamięci i obsługuje je według własnego uznania…
… możesz być zaskoczony, widząc dane wyjściowe, które generuje to skanowanie pamięci:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: mało prawdopodobny tekstALJBNGOAPLLBDEB 006D8B3C: mało prawdopodobny tekstALJBNGOA 006D8B7C: mało prawdopodobny tekstALJBNGO 006D8BFC: mało prawdopodobny tekstALJBNGOAPLLBDEBJ 006D8CBC: mało prawdopodobny tekstALJBN 006 8D7D006C: nieprawdopodobny tekstALJBNGOAP 903D006C: nieprawdopodobny tekstALJBNGOAPL 90D006BC: nieprawdopodobny tekstALJBNGOAPLL 90D006FC: nieprawdopodobny tekstALJBNG 913D006C: nieprawdopodobny tekstALJBNGOAPLLB 91D006BC: nieprawdopodobny tekstALJB 91D006FC: nieprawdopodobny tekstALJBNGOAPLLBD 923D006C : nieprawdopodobny tekstALJBNGOAPLLBDE 70DB006C: nieprawdopodobny tekstALJ 8DBB006C: nieprawdopodobny tekstAL 0DBDXNUMXC: nieprawdopodobny tekstA
No i oto, w tym czasie złapaliśmy nasz zrzut pamięci, mimo że skończyliśmy ze sznurkiem s
(i powiedział Lua, że już go nie potrzebujemy, mówiąc s = nil
), wszystkie ciągi, które kod utworzył po drodze, nadal były obecne w pamięci RAM, nie zostały jeszcze odzyskane ani usunięte.
Rzeczywiście, jeśli posortujemy powyższe dane wyjściowe według samych ciągów znaków, zamiast postępować zgodnie z kolejnością, w jakiej pojawiały się w pamięci RAM, będziesz w stanie wyobrazić sobie, co wydarzyło się podczas pętli, w której łączyliśmy jeden znak na raz z naszym ciągiem hasła:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: mało prawdopodobny tekst A 006D8D006C: nieprawdopodobny tekstALJBNGOAP 70D006C: nieprawdopodobny tekstALJBNGOAPL 91D006BC: nieprawdopodobny tekstALJBNGOAPLL 8D006C: nieprawdopodobny tekstALJBNGOAPLLB 90D006FC: nieprawdopodobny tekstALJBNGOAPLLBD 8D7C: nieprawdopodobny tekstALJBNGOAPLLBDE 006D8 AFC: mało prawdopodobny tekst ALJBNGOAPLLBDEB 3D006BFC : mało prawdopodobnytekstALJBNGOAPLLBDEBJ
Wszystkie te tymczasowe, pośrednie ciągi nadal tam są, więc nawet gdybyśmy pomyślnie wymazali końcową wartość s
, nadal ujawnialibyśmy wszystko oprócz ostatniego znaku.
W rzeczywistości w tym przypadku, nawet gdy celowo zmusiliśmy nasz program do usunięcia wszystkich niepotrzebnych danych, wywołując specjalną funkcję Lua collectgarbage()
(większość języków skryptowych ma coś podobnego), większość danych w tych brzydkich tymczasowych ciągach i tak utknęła w pamięci RAM, ponieważ skompilowaliśmy Lua, aby automatycznie zarządzała pamięcią przy użyciu starego, dobrego malloc()
i free()
.
Innymi słowy, nawet po tym, jak sam Lua odzyskał swoje tymczasowe bloki pamięci, aby użyć ich ponownie, nie mogliśmy kontrolować, jak i kiedy te bloki pamięci zostaną ponownie użyte, a tym samym jak długo będą leżeć wewnątrz procesu z ich lewym-- nad danymi czekającymi na wywęszenie, wyrzucenie lub wyciek w inny sposób.
Wpisz .NET
Ale co z KeePass, od którego zaczął się ten artykuł?
KeePass jest napisany w języku C# i wykorzystuje środowisko wykonawcze .NET, dzięki czemu unika problemów związanych z niewłaściwym zarządzaniem pamięcią, które niosą ze sobą programy C…
… ale C# zarządza własnymi łańcuchami tekstowymi, podobnie jak Lua, co rodzi pytanie:
Nawet jeśli programista uniknął przechowywania całego hasła głównego w jednym miejscu po tym, jak z nim skończył, atakujący, mający dostęp do zrzutu pamięci, może mimo to znaleźć wystarczająco dużo pozostałych danych tymczasowych, aby i tak odgadnąć lub odzyskać hasło główne, nawet jeśli te atakujący uzyskali dostęp do Twojego komputera w ciągu minut, godzin lub dni po wpisaniu hasła?
Mówiąc najprościej, czy istnieją wykrywalne, upiorne pozostałości Twojego hasła głównego, które przetrwają w pamięci RAM, nawet po tym, jak można by się spodziewać, że zostały usunięte?
Irytująco, jako użytkownik Github odkrył Vdohney, odpowiedź (przynajmniej dla wersji KeePass wcześniejszych niż 2.54) brzmi: „Tak”.
Żeby było jasne, nie sądzimy, aby rzeczywiste hasło główne można było odzyskać jako pojedynczy ciąg tekstowy ze zrzutu pamięci KeePass, ponieważ autor stworzył specjalną funkcję do wprowadzania hasła głównego, która robi wszystko, aby uniknąć przechowywania pełnego hasło, gdzie można je łatwo wykryć i wywąchać.
Zadowoliliśmy się tym, ustawiając nasze hasło główne na SIXTEENPASSCHARS
, wpisując go, a następnie wykonując natychmiastowe zrzuty pamięci, krótko i długo potem.
Przeszukaliśmy zrzuty za pomocą prostego skryptu Lua, który szukał tekstu hasła wszędzie, zarówno w 8-bitowym formacie ASCII, jak i w 16-bitowym formacie UTF-16 (Windows widechar), na przykład:
Wyniki były zachęcające:
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Odczyt pliku zrzutu... GOTOWE. Wyszukiwanie SIXTEENPASSCHARS jako 8-bitowego ASCII... nie znaleziono. Wyszukiwanie SIXTEENPASSCHARS jako UTF-16... nie znaleziono.
Ale Vdohney, odkrywca CVE-2023-32784, zauważył, że podczas wpisywania hasła głównego KeePass przekazuje wizualną informację zwrotną, konstruując i wyświetlając ciąg zastępczy składający się ze znaków „blob” Unicode, do długości twojego hasła włącznie. hasło:
W ciągach znaków szerokokątnych w systemie Windows (które składają się z dwóch bajtów na znak, a nie tylko jednego bajtu, jak w ASCII), znak „kropelki” jest kodowany w pamięci RAM jako bajt szesnastkowy 0xCF
następnie 0x25
(co akurat jest znakiem procentu w ASCII).
Tak więc, nawet jeśli KeePass bardzo dba o surowe znaki, które wpisujesz podczas wprowadzania samego hasła, możesz skończyć z pozostałymi ciągami znaków „blob”, łatwo wykrywalnymi w pamięci jako powtarzające się przebiegi, takie jak CF25CF25
or CF25CF25CF25
...
… a jeśli tak, najdłuższy ciąg znalezionych znaków blob prawdopodobnie zdradziłby długość twojego hasła, co byłoby skromną formą wycieku informacji o haśle, jeśli nic więcej.
Użyliśmy następującego skryptu Lua, aby wyszukać znaki pozostałych łańcuchów znaków zastępczych hasła:
Wynik był zaskakujący (usunęliśmy kolejne wiersze z taką samą liczbą obiektów typu blob lub z mniejszą liczbą obiektów typu blob niż poprzedni wiersz, aby zaoszczędzić miejsce):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******** [kontynuacja podobnie dla 8 obiektów typu blob, 9 obiektów typu blob itd.] [aż do dwóch końcowych wierszy po dokładnie 16 obiektów typu blob w każdym] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [wszystkie pozostałe dopasowania mają długość jednego obiektu blob] 0123B058: *
Na blisko położonych, ale stale rosnących adresach pamięci, znaleźliśmy systematyczną listę 3 obiektów typu blob, następnie 4 obiektów typu blob i tak dalej, aż do 16 obiektów typu blob (długość naszego hasła), po których następuje wiele losowo rozproszonych wystąpień ciągów pojedynczych obiektów typu blob .
Tak więc te zastępcze łańcuchy „blob” rzeczywiście wydają się przeciekać do pamięci i pozostają w tyle, aby ujawnić długość hasła, długo po tym, jak oprogramowanie KeePass zakończy pracę z twoim hasłem głównym.
Następny krok
Postanowiliśmy kopać dalej, tak jak zrobił to Vdohney.
Zmieniliśmy nasz kod dopasowujący wzorce, aby wykrywał łańcuchy znaków blob, po których następuje pojedynczy znak ASCII w formacie 16-bitowym (znaki ASCII są reprezentowane w UTF-16 jako ich zwykły 8-bitowy kod ASCII, po którym następuje bajt zerowy).
Tym razem, aby zaoszczędzić miejsce, pominęliśmy wyjście dla każdego dopasowania, które dokładnie pasuje do poprzedniego:
Niespodzianka niespodzianka:
C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp 00BE581B: *I 00BE621B: **X 00BE6BD3: ***T 00BE769B: ****E 00BE822B: *****E 00BE8C6B: ******N 00BE974B: *******P 00BEA25B: ********A 00BEAD33: *********S 00BEB81B: **********S 00BEC383: ***********C 00BECEEB: ************H 00BEDA5B: *************A 00BEE623: **************R 00BEF1A3: ***************S 03E97CF2: *N 0AA6F0AF: *W 0D8AF7C8: *X 0F27BAF8: *S
Zobacz, co uzyskujemy z zarządzanego regionu pamięci ciągów .NET!
Zwarty zestaw tymczasowych „ciągów blob”, które ujawniają kolejne znaki w naszym haśle, zaczynając od drugiego znaku.
Po tych nieszczelnych ciągach następują szeroko rozpowszechnione dopasowania pojedynczych znaków, które, jak zakładamy, powstały przypadkowo. (Plik zrzutu KeePass ma rozmiar około 250 MB, więc jest dużo miejsca na pojawienie się znaków „blobowych”, jakby przez przypadek).
Nawet jeśli weźmiemy pod uwagę te dodatkowe cztery dopasowania, zamiast odrzucić je jako prawdopodobne niezgodności, możemy zgadnąć, że hasło główne jest jednym z:
?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
Oczywiście ta prosta technika nie znajduje pierwszego znaku w haśle, ponieważ pierwszy „łańcuch blob” jest tworzony dopiero po wpisaniu tego pierwszego znaku
Zauważ, że ta lista jest ładna i krótka, ponieważ odfiltrowaliśmy dopasowania, które nie kończyły się na znakach ASCII.
Jeśli szukasz znaków z innego zakresu, takich jak znaki chińskie lub koreańskie, możesz otrzymać więcej przypadkowych trafień, ponieważ jest o wiele więcej możliwych znaków do dopasowania…
…ale podejrzewamy, że i tak zbliżysz się do hasła głównego, a „łańcuchy blob”, które odnoszą się do hasła, wydają się być zgrupowane w pamięci RAM, prawdopodobnie dlatego, że zostały przydzielone mniej więcej w tym samym czasie przez tę samą część środowisko wykonawcze .NET.
I oto, w niewątpliwie długiej i dyskursywnej pigułce, jest fascynująca historia CVE-2023-32784.
Co robić?
- Jeśli jesteś użytkownikiem KeePass, nie panikuj. Chociaż jest to błąd i technicznie możliwa do wykorzystania luka w zabezpieczeniach, osoby atakujące zdalnie, które chciałyby złamać hasło za pomocą tego błędu, musiałyby najpierw zaimplementować złośliwe oprogramowanie na komputerze. Dałoby im to wiele innych sposobów bezpośredniej kradzieży haseł, nawet gdyby ten błąd nie istniał, na przykład poprzez rejestrowanie naciśnięć klawiszy podczas pisania. W tym momencie możesz po prostu uważać na nadchodzącą aktualizację i pobrać ją, gdy będzie gotowa.
- Jeśli nie używasz szyfrowania całego dysku, rozważ jego włączenie. Aby wyodrębnić pozostałe hasła z pliku wymiany lub pliku hibernacji (pliki dysku systemu operacyjnego używane do tymczasowego zapisywania zawartości pamięci podczas dużego obciążenia lub gdy komputer jest „uśpiony”), atakujący musieliby mieć bezpośredni dostęp do dysku twardego. Jeśli masz aktywną funkcję BitLocker lub jej odpowiednik dla innych systemów operacyjnych, nie będą one mogły uzyskać dostępu do pliku wymiany, pliku hibernacji ani żadnych innych danych osobistych, takich jak dokumenty, arkusze kalkulacyjne, zapisane wiadomości e-mail itd.
- Jeśli jesteś programistą, informuj się na bieżąco o problemach z zarządzaniem pamięcią. Nie zakładaj, że tylko dlatego, że każdy
free()
odpowiada jego odpowiednikowimalloc()
że Twoje dane są bezpieczne i dobrze zarządzane. Czasami może być konieczne podjęcie dodatkowych środków ostrożności, aby uniknąć pozostawienia tajnych danych leżących w pobliżu, a te środki ostrożności są bardzo różne w różnych systemach operacyjnych. - Jeśli jesteś testerem kontroli jakości lub recenzentem kodu, zawsze myśl „za kulisami”. Nawet jeśli kod zarządzania pamięcią wygląda na uporządkowany i dobrze wyważony, miej świadomość tego, co dzieje się za kulisami (ponieważ oryginalny programista mógł tego nie wiedzieć) i przygotuj się do wykonania prac w stylu pentestingu, takich jak monitorowanie czasu wykonywania i pamięć dumping, aby sprawdzić, czy bezpieczny kod naprawdę zachowuje się tak, jak powinien.
KOD Z ARTYKUŁU: UNL1.C
#włączać #włączać #włączać void hexdump(unsigned char* buff, int len) { // Drukuj bufor w 16-bajtowych porcjach dla (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Pokaż 16 bajtów jako wartości szesnastkowe dla (int j = 0; j < 16; j = j+1) { printf("%02X",buff[i+j]); } // Powtórz te 16 bajtów jako znaki for (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Uzyskaj pamięć do przechowywania hasła i pokaż, co // znajduje się w buforze, gdy jest oficjalnie „nowy”... char* buff = malloc(128); printf("Zrzucanie 'nowego' bufora przy starcie"); hexdump(buff,128); // Użyj pseudolosowego adresu bufora jako random seed srand((unsigned)buff); // Rozpocznij hasło od ustalonego, możliwego do przeszukiwania tekstu strcpy(buff,"nieprawdopodobnytekst"); // Dodaj 16 pseudolosowych liter, po jednej for (int i = 1; i <= 16; i++) { // Wybierz literę od A (65+0) do P (65+15) char ch = 65 + (rand() i 15); // Następnie zmodyfikuj ciąg buff w miejscu strncat(buff,&ch,1); } // Pełne hasło jest teraz w pamięci, więc wypisz je // jako ciąg i pokaż cały bufor... printf("Pełny ciąg to: %sn",buff); hexdump(buff,128); // Wstrzymaj teraz, aby zrzucić pamięć RAM procesu (spróbuj: 'procdump -ma') puts("Oczekiwanie na [ENTER], aby zwolnić bufor..."); getchar(); // Formalnie zwolnij() pamięć i wyświetl bufor // ponownie, aby zobaczyć, czy coś zostało... free(buff); printf("Zrzucanie bufora po free()n"); hexdump(buff,128); // Wstrzymaj, aby ponownie zrzucić pamięć RAM, aby sprawdzić różnice puts("Oczekiwanie na wyjście [ENTER] z main()..."); getchar(); zwróć 0; }
KOD Z ARTYKUŁU: UNL2.C
#włączać #włączać #włączać #włączać void hexdump(unsigned char* buff, int len) { // Drukuj bufor w 16-bajtowych porcjach dla (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Pokaż 16 bajtów jako wartości szesnastkowe dla (int j = 0; j < 16; j = j+1) { printf("%02X",buff[i+j]); } // Powtórz te 16 bajtów jako znaki for (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Uzyskaj pamięć do przechowywania hasła i pokaż, co // znajduje się w buforze, gdy jest oficjalnie „nowy”... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Zrzucanie 'nowego' bufora przy starcie"); hexdump(buff,128); // Użyj pseudolosowego adresu bufora jako random seed srand((unsigned)buff); // Rozpocznij hasło od ustalonego, możliwego do przeszukiwania tekstu strcpy(buff,"nieprawdopodobnytekst"); // Dodaj 16 pseudolosowych liter, po jednej for (int i = 1; i <= 16; i++) { // Wybierz literę od A (65+0) do P (65+15) char ch = 65 + (rand() i 15); // Następnie zmodyfikuj ciąg buff w miejscu strncat(buff,&ch,1); } // Pełne hasło jest teraz w pamięci, więc wypisz je // jako ciąg i pokaż cały bufor... printf("Pełny ciąg to: %sn",buff); hexdump(buff,128); // Wstrzymaj teraz, aby zrzucić pamięć RAM procesu (spróbuj: 'procdump -ma') puts("Oczekiwanie na [ENTER], aby zwolnić bufor..."); getchar(); // Formalnie zwolnij() pamięć i wyświetl bufor // ponownie, aby zobaczyć, czy coś zostało... VirtualFree(buff,0,MEM_RELEASE); printf("Zrzucanie bufora po free()n"); hexdump(buff,128); // Wstrzymaj, aby ponownie zrzucić pamięć RAM, aby sprawdzić różnice puts("Oczekiwanie na wyjście [ENTER] z main()..."); getchar(); zwróć 0; }
KOD Z ARTYKUŁU: S1.LUA
-- Zacznij od ustalonego, możliwego do wyszukania tekstu s = 'unprawdopodobny tekst' -- Dołącz 16 losowych znaków od 'A' do 'P' dla i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Pełny łańcuch to:',s,'n') -- Wstrzymaj proces zrzutu RAM print('Oczekiwanie na [ENTER] przed zwolnieniem ciągu...') io.read() - - Wyczyść ciąg znaków i zaznacz zmienną jako nieużywaną s = nil -- Ponownie zrzuć pamięć RAM, aby wyszukać różnice print('Oczekiwanie na [ENTER] przed wyjściem...') io.read()
KOD Z ARTYKUŁU: FINDIT.LUA
-- odczyt w pliku zrzutu local f = io.open(arg[1],'rb'):read('*a') -- szukanie tekstu znacznika, po którym następuje jeden -- lub więcej losowych znaków ASCII lokalny b,e ,m = 0,0,nil podczas gdy prawda do -- poszukaj następnego dopasowania i zapamiętaj przesunięcie b,e,m = f:find('(nieprawdopodobnytekst[AZ]+)',e+1) -- wyjdź, gdy nie będzie więcej pasuje, jeśli nie b, to łam koniec -- zgłoś pozycję i znaleziony łańcuch print(string.format('%08X: %s',b,m)) end
KOD Z ARTYKUŁU: SEARCHKNOWN.LUA
io.write('Odczyt pliku zrzutu... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Wyszukiwanie SZEŚĆTEENPASSCHARS jako 8-bitowego ASCII... ') local p08 = f:find('SZEŚĆSTEENPASSCHARS') io.write(p08 i 'ZNALEZIONO' lub 'nie znaleziono','.n') io.write („Wyszukiwanie SZEŚCIASTENPASSCHARS jako UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 and 'FOUND' lub 'not found', '.n')
KOD Z ARTYKUŁU: FINDBLOBS.LUA
-- wczytaj plik zrzutu określony w wierszu poleceń local f = io.open(arg[1],'rb'):read('*a') -- Poszukaj jednego lub więcej obiektów blob haseł, po których następuje dowolny obiekt niebędący obiektem blob -- Zwróć uwagę, że znaki typu blob (●) są kodowane w znakach szerokokątnych systemu Windows -- jako kody litte-endian UTF-16, wychodzące jako CF 25 w formacie szesnastkowym. local b,e,m = 0,0,nil podczas gdy true do -- Chcemy jednego lub więcej blobów, po których następuje dowolny non-blob. -- Upraszczamy kod, szukając wyraźnego CF25 -- po którym następuje dowolny ciąg, który zawiera tylko CF lub 25 -- więc znajdziemy CF25CFCF lub CF2525CF, a także CF25CF25. -- Później odfiltrujemy „fałszywe alarmy”, jeśli takie się pojawią. -- Musimy napisać '%%' zamiast x25, ponieważ znak x25 -- (znak procentu) jest specjalnym znakiem wyszukiwania w Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- wyjdź, gdy nie ma więcej dopasowań, jeśli nie b, to przerwaj koniec -- CMD.EXE nie może wydrukować plamy, więc zamieniamy je na gwiazdy. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) koniec
KOD Z ARTYKUŁU: SEARCHKP.LUA
-- wczytaj plik zrzutu określony w wierszu poleceń local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Teraz chcemy, aby jeden lub więcej blobów (CF25), po których następował kod -- dla A..Z, po którym następuje bajt 0, aby przekonwertować ACSCII na UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- wyjdź, gdy nie będzie więcej pasujących, jeśli nie b, a następnie złam koniec -- CMD.EXE nie może drukować obiektów blob, więc konwertujemy je na gwiazdy. -- Aby zaoszczędzić miejsce pomijamy kolejne dopasowania, jeśli m ~= p to print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m koniec koniec
- Dystrybucja treści i PR oparta na SEO. Uzyskaj wzmocnienie już dziś.
- PlatoAiStream. Analiza danych Web3. Wiedza wzmocniona. Dostęp tutaj.
- Wybijanie przyszłości w Adryenn Ashley. Dostęp tutaj.
- Kupuj i sprzedawaj akcje spółek PRE-IPO z PREIPO®. Dostęp tutaj.
- Źródło: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- :ma
- :Jest
- :nie
- :Gdzie
- ][P
- $W GÓRĘ
- 1
- 10
- 12
- 15%
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- Zdolny
- O nas
- powyżej
- bezwzględny
- AC
- dostęp
- Konto
- nabyć
- nabywanie
- aktywny
- rzeczywisty
- faktycznie
- w dodatku
- Dodatkowy
- adres
- Adresy
- Dodaje
- Po
- potem
- ponownie
- Wszystkie kategorie
- przydzielony
- przydziela
- przydział
- przydziały
- dopuszczać
- sam
- wzdłuż
- już
- również
- zmieniony
- Chociaż
- zawsze
- an
- i
- Andrew
- odpowiedź
- każdy
- wszystko
- cokolwiek krytycznego
- zjawić się
- pojawił się
- podejście
- zatwierdzony
- SĄ
- na około
- artykuł
- towary
- AS
- At
- autor
- samochód
- automatycznie
- automatycznie
- dostępny
- uniknąć
- unikany
- świadomy
- z dala
- z powrotem
- tło
- background-image
- BE
- bo
- staje się
- być
- zanim
- Początek
- za
- za kulisami
- poniżej
- Ulepsz Swój
- Bit
- Blokować
- Bloki
- granica
- obie
- Dolny
- marka
- Brand New
- przerwa
- krótko
- przynieść
- bufor
- przepełnienie bufora
- Bug
- błędy
- budować
- ale
- by
- C + +
- wezwanie
- powołanie
- CAN
- Może uzyskać
- który
- walizka
- złapany
- CD
- Centrum
- na pewno
- więzy
- szansa
- zmieniony
- charakter
- znaków
- kontrola
- Wykrywanie urządzeń szpiegujących
- chiński
- Dodaj
- jasny
- wyraźnie
- Zamknij
- kod
- kolor
- COM
- byliśmy spójni, od początku
- przyjście
- komentarz
- komentarze
- wspólny
- kompletny
- kompleks
- komputer
- Rozważać
- znaczny
- za
- Składający się
- budowy
- zawartość
- treść
- nieustannie
- ciągły
- kontrola
- konwertować
- prawo autorskie
- Odpowiedni
- mógłby
- pokrywa
- pęknięcie
- Stwórz
- stworzony
- twórca
- krytyczny
- Bezpieczeństwo cybernetyczne
- ZAGROŻENIE
- Niebezpieczny
- dane
- wyciek danych
- Dni
- sprawa
- postanowiła
- dedykowane
- opisane
- ZROBIŁ
- Różnice
- różne
- Trudność
- KOPAĆ
- cyfrowy
- kierować
- Dostęp bezpośredni
- bezpośrednio
- Wyświetlacz
- wyświetlanie
- rozporządzać
- do
- dokumenty
- robi
- Nie
- robi
- zrobić
- nie
- na dół
- napęd
- z powodu
- zrzucać
- podczas
- e
- każdy
- Wcześniej
- z łatwością
- Ekosystem
- bądź
- więcej
- e-maile
- umożliwiając
- zachęcający
- szyfrowanie
- zakończenia
- kończy się
- dość
- zapewnić
- zapewnienie
- Wchodzę
- wprowadzenie
- Cały
- wejście
- Środowisko
- Równoważny
- błąd
- istotnie
- szacunkowa
- itp
- Eter (ETH)
- Parzyste
- ostatecznie
- stale rosnący
- Każdy
- wszystko
- dokładnie
- przykład
- Z wyjątkiem
- Podniecenie
- egzekucja
- istnieć
- Przede wszystkim system został opracowany
- Wyjście
- Wyjście
- oczekiwać
- Wyjaśniać
- Wykorzystać
- narażony
- rozciągać się
- dodatkowy
- wyciąg
- fakt
- fałszywy
- fascynujący
- Korzyści
- informacja zwrotna
- mniej
- walczący
- filet
- Akta
- filtrować
- finał
- W końcu
- Znajdź
- znalezieniu
- znajduje
- w porządku
- i terminów, a
- ustalony
- Skupiać
- następnie
- następujący
- W razie zamówieenia projektu
- Nasz formularz
- Formalnie
- format
- nadchodzący
- znaleziono
- cztery
- Darmowy
- od
- pełny
- w pełni
- funkcjonować
- Funkcje
- dalej
- przyszłość
- generuje
- generacja
- otrzymać
- miejsce
- GitHub
- Dać
- dany
- daje
- Dający
- Go
- Goes
- będzie
- dobry
- Rząd
- chwycić
- wspaniały
- gwarancja
- miał
- Uchwyty
- się
- Wydarzenie
- dzieje
- Ciężko
- Have
- mający
- bóle głowy
- ciężki
- wysokość
- tutaj
- HEX
- na wysokim szczeblu
- wyższy
- Odsłon
- przytrzymaj
- Otwór
- nadzieję
- GODZINY
- unosić
- W jaki sposób
- How To
- HTTPS
- polowanie
- i
- identyfikator
- if
- natychmiast
- ważny
- in
- obejmuje
- Włącznie z
- Informacja
- poinformowany
- zamiast
- zainteresowany
- Pośredni
- Internet
- najnowszych
- problemy
- IT
- JEGO
- samo
- żargon
- czerwiec
- właśnie
- tylko jeden
- Trzymać
- Klawisz
- Wiedzieć
- znany
- koreański
- język
- Języki
- laptopa
- Nazwisko
- później
- prowadzić
- Wyprowadzenia
- przeciec
- Wycieki
- UCZYĆ SIĘ
- nauka
- najmniej
- pozostawiając
- lewo
- Długość
- list
- Biblioteka
- życie
- lubić
- Prawdopodobnie
- Ograniczony
- Linia
- linie
- Lista
- Katalogowany
- ll
- załadować
- miejscowy
- lokalizacja
- zalogowaniu
- długo
- długoterminowy
- dłużej
- Popatrz
- wygląda jak
- wyglądał
- poszukuje
- WYGLĄD
- Partia
- szczęście
- utrzymuje
- robić
- malware
- zarządzanie
- zarządzane
- i konserwacjami
- kierownik
- zarządza
- Manipulacja
- wiele
- Margines
- znak
- znacznik
- mistrz
- Mecz
- dopasowywanie
- Maksymalna szerokość
- Może..
- znaczy
- Pamięć
- wzmiankowany
- Microsoft
- może
- minuty
- skromny
- zmodyfikowano
- modyfikować
- moment
- monitorowanie
- jeszcze
- większość
- dużo
- wielokrotność
- Schludny
- Potrzebować
- potrzebne
- netto
- nigdy
- Niemniej jednak
- Nowości
- aktualności
- Następny
- miło
- Nie
- normalna
- nic
- Zauważyć..
- już dziś
- numer
- z naszej
- przedmiot
- oczywista
- of
- poza
- urzędnik
- Oficjalnie
- offset
- Stary
- on
- pewnego razu
- ONE
- tylko
- open source
- operacyjny
- system operacyjny
- system operacyjny
- operator
- Option
- or
- zamówienie
- oryginalny
- Inne
- Pozostałe
- Inaczej
- ludzkiej,
- sobie
- na zewnątrz
- wydajność
- koniec
- ogólny
- własny
- strona
- Panika
- część
- Hasło
- Password Manager
- hasła
- ścieżka
- Wzór
- Paweł
- pauza
- Zapłacić
- procent
- może
- okres
- na stałe
- osobisty
- dane personalne
- fizyczny
- obraz
- sztuk
- Miejsce
- zastępczy
- Plaga
- plato
- Analiza danych Platona
- PlatoDane
- Volcano Plenty Vaporizer Storz & Bickel
- punkt
- zwrotnica
- Popularny
- position
- możliwy
- Wiadomości
- potencjał
- precyzyjnie
- teraźniejszość
- bardzo
- zapobiec
- poprzedni
- Cena
- wydruki
- prawdopodobnie
- problemy
- wygląda tak
- Program
- Programista
- Programiści
- Programowanie
- Programy
- wyraźny
- położyć
- Python
- Q & A
- pytanie
- podnosi
- RAM
- przypadkowy
- zasięg
- raczej
- Surowy
- surowe dane
- RE
- osiągnięty
- Czytaj
- Czytający
- gotowy
- real
- prawdziwe życie
- w czasie rzeczywistym
- naprawdę
- rozpoznać
- Recover
- odzyskiwanie
- związane z
- pozostały
- pamiętać
- zdalny
- usunąć
- powtarzać
- powtórzony
- WIELOKROTNIE
- raport
- reprezentowane
- poszanowanie
- odpowiednio
- REST
- Efekt
- powrót
- powrót
- ujawniać
- Pozbyć się
- prawo
- Ryzyko
- ryzyko
- Pokój
- run
- bieganie
- monitorowanie czasu pracy
- s
- "bezpiecznym"
- bezpieczniej
- taki sam
- zadowolony
- Zapisz
- powiedzenie
- skanować
- rozrzucone
- Sceny
- Szukaj
- poszukiwania
- druga
- sekund
- Tajemnica
- Sekcja
- bezpieczne
- bezpieczeństwo
- widzieć
- nasienie
- widzenie
- wydać się
- widziany
- widzi
- Serie
- poważny
- zestaw
- ustawienie
- Short
- Wkrótce
- powinien
- pokazać
- pokazane
- znak
- znaki
- podobny
- Podobnie
- Prosty
- uproszczony
- upraszczać
- po prostu
- pojedynczy
- Rozmiar
- spać
- mały
- Podstępny
- IGMP
- So
- Tworzenie
- solidny
- kilka
- coś
- Wkrótce
- Źródło
- Kod źródłowy
- Typ przestrzeni
- specjalny
- specjalnie
- określony
- prędkość
- Gwiazdy
- początek
- rozpoczęty
- Startowy
- rozpocznie
- startup
- Nadal
- Ukradłem
- Stop
- zatrzymany
- sklep
- przechowywany
- Historia
- sznur
- silny
- Badanie
- Z powodzeniem
- taki
- wystarczający
- domniemany
- niespodzianka
- zdziwiony
- zaskakujący
- przetrwać
- SVG
- zamiana
- system
- systemy
- Brać
- Zadania
- trwa
- biorąc
- rozmawiać
- technicznie
- Techniki
- tymczasowy
- test
- Testy
- niż
- że
- Połączenia
- Źródło
- ich
- Im
- sami
- następnie
- teoria
- Tam.
- w związku z tym
- one
- rzecz
- myśleć
- to
- tych
- chociaż?
- myśl
- czas
- Tytuł
- do
- razem
- wziął
- narzędzie
- Top
- śledzić
- Śledzenie
- przejście
- przezroczysty
- prawdziwy
- próbować
- Obrócony
- drugiej
- rodzaj
- zazwyczaj
- zrozumieć
- unicode
- aż do
- nieużywana
- niepożądany
- Aktualizacja
- zaktualizowane
- URL
- us
- rząd Stanów Zjednoczonych
- Stosowanie
- usb
- posługiwać się
- używać po wolnym
- używany
- Użytkownik
- zastosowania
- za pomocą
- użyteczność
- wartość
- Wartości
- różnorodność
- zweryfikować
- wersja
- początku.
- przez
- wrażliwość
- W
- czekać
- Czekanie
- chcieć
- poszukiwany
- była
- Oglądaj
- Droga..
- sposoby
- we
- tygodni
- DOBRZE
- były
- Co
- jeśli chodzi o komunikację i motywację
- czy
- który
- Podczas
- KIM
- ktokolwiek
- cały
- dlaczego
- będzie
- wygrać
- okna
- Przetrzyj
- w
- bez
- pełen zdumienia
- słowa
- Praca
- pracował
- pracujący
- działa
- martwić się
- by
- dałbym
- napisać
- pisanie
- napisany
- jeszcze
- ty
- Twój
- siebie
- zefirnet
- zero